Support CLTV micropayment channels

Also extend WalletTool to send via, settle and refund these channels.
This commit is contained in:
Will Shackleton
2015-11-16 19:51:43 +00:00
committed by Andreas Schildbach
parent 25db735b3a
commit c9cce47962
7 changed files with 619 additions and 27 deletions

View File

@@ -33,7 +33,6 @@ import org.bitcoinj.core.listeners.WalletChangeEventListener;
import org.bitcoinj.core.listeners.WalletCoinEventListener;
import org.bitcoinj.core.TransactionConfidence.*;
import org.bitcoinj.crypto.*;
import org.bitcoinj.params.*;
import org.bitcoinj.script.*;
import org.bitcoinj.signers.*;
import org.bitcoinj.store.*;
@@ -3643,6 +3642,24 @@ public class Wallet extends BaseTaggableObject
return req;
}
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, Date releaseTime, ECKey from, ECKey to, Coin value) {
long time = releaseTime.getTime() / 1000L;
checkArgument(time >= Transaction.LOCKTIME_THRESHOLD, "Release time was too small");
return toCLTVPaymentChannel(params, BigInteger.valueOf(time), from, to, value);
}
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, long lockTime, ECKey from, ECKey to, Coin value) {
return toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), from, to, value);
}
private static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) {
SendRequest req = new SendRequest();
Script output = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
req.tx = new Transaction(params);
req.tx.addOutput(value, output);
return req;
}
/** Copy data from payment request. */
public SendRequest fromPaymentDetails(PaymentDetails paymentDetails) {
if (paymentDetails.hasMemo())
@@ -4120,6 +4137,19 @@ public class Wallet extends BaseTaggableObject
if (key != null && (key.isEncrypted() || key.hasPrivKey()))
return true;
}
} else if (script.isSentToCLTVPaymentChannel()) {
// Any script for which we are the recipient or sender counts.
byte[] sender = script.getCLTVPaymentChannelSenderPubKey();
ECKey senderKey = findKeyFromPubKey(sender);
if (senderKey != null && (senderKey.isEncrypted() || senderKey.hasPrivKey())) {
return true;
}
byte[] recipient = script.getCLTVPaymentChannelRecipientPubKey();
ECKey recipientKey = findKeyFromPubKey(sender);
if (recipientKey != null && (recipientKey.isEncrypted() || recipientKey.hasPrivKey())) {
return true;
}
return false;
}
return false;
}

View File

@@ -302,6 +302,37 @@ public class Script {
}
}
/**
* Retrieves the sender public key from a LOCKTIMEVERIFY transaction
* @return
* @throws ScriptException
*/
public byte[] getCLTVPaymentChannelSenderPubKey() throws ScriptException {
if (!isSentToCLTVPaymentChannel()) {
throw new ScriptException("Script not a standard CHECKLOCKTIMVERIFY transaction: " + this);
}
return chunks.get(8).data;
}
/**
* Retrieves the recipient public key from a LOCKTIMEVERIFY transaction
* @return
* @throws ScriptException
*/
public byte[] getCLTVPaymentChannelRecipientPubKey() throws ScriptException {
if (!isSentToCLTVPaymentChannel()) {
throw new ScriptException("Script not a standard CHECKLOCKTIMVERIFY transaction: " + this);
}
return chunks.get(1).data;
}
public BigInteger getCLTVPaymentChannelExpiry() {
if (!isSentToCLTVPaymentChannel()) {
throw new ScriptException("Script not a standard CHECKLOCKTIMEVERIFY transaction: " + this);
}
return castToBigInteger(chunks.get(4).data, 5);
}
/**
* For 2-element [input] scripts assumes that the paid-to-address can be derived from the public key.
* The concept of a "from address" isn't well defined in Bitcoin and you should not assume the sender of a
@@ -686,6 +717,22 @@ public class Script {
return true;
}
public boolean isSentToCLTVPaymentChannel() {
if (chunks.size() != 10) return false;
// Check that opcodes match the pre-determined format.
if (!chunks.get(0).equalsOpCode(OP_IF)) return false;
// chunk[1] = recipient pubkey
if (!chunks.get(2).equalsOpCode(OP_CHECKSIGVERIFY)) return false;
if (!chunks.get(3).equalsOpCode(OP_ELSE)) return false;
// chunk[4] = locktime
if (!chunks.get(5).equalsOpCode(OP_CHECKLOCKTIMEVERIFY)) return false;
if (!chunks.get(6).equalsOpCode(OP_DROP)) return false;
if (!chunks.get(7).equalsOpCode(OP_ENDIF)) return false;
// chunk[8] = sender pubkey
if (!chunks.get(9).equalsOpCode(OP_CHECKSIG)) return false;
return true;
}
private static boolean equalsRange(byte[] a, int start, byte[] b) {
if (start + b.length > a.length)
return false;

View File

@@ -23,6 +23,7 @@ import org.bitcoinj.core.Utils;
import org.bitcoinj.crypto.TransactionSignature;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -435,4 +436,32 @@ public class ScriptBuilder {
checkArgument(data.length <= 40);
return new ScriptBuilder().op(OP_RETURN).data(data).build();
}
public static Script createCLTVPaymentChannelOutput(BigInteger time, ECKey from, ECKey to) {
byte[] timeBytes = Utils.reverseBytes(Utils.encodeMPI(time, false));
if (timeBytes.length > 5) {
throw new RuntimeException("Time too large to encode as 5-byte int");
}
return new ScriptBuilder().op(OP_IF)
.data(to.getPubKey()).op(OP_CHECKSIGVERIFY)
.op(OP_ELSE)
.data(timeBytes).op(OP_CHECKLOCKTIMEVERIFY).op(OP_DROP)
.op(OP_ENDIF)
.data(from.getPubKey()).op(OP_CHECKSIG).build();
}
public static Script createCLTVPaymentChannelRefund(TransactionSignature signature) {
ScriptBuilder builder = new ScriptBuilder();
builder.data(signature.encodeToBitcoin());
builder.data(new byte[] { 0 }); // Use the CHECKLOCKTIMEVERIFY if branch
return builder.build();
}
public static Script createCLTVPaymentChannelInput(TransactionSignature from, TransactionSignature to) {
ScriptBuilder builder = new ScriptBuilder();
builder.data(from.encodeToBitcoin());
builder.data(to.encodeToBitcoin());
builder.smallNum(1); // Use the CHECKLOCKTIMEVERIFY if branch
return builder.build();
}
}

View File

@@ -1,12 +1,14 @@
package org.bitcoinj.core;
import org.bitcoinj.core.TransactionConfidence.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.params.*;
import org.bitcoinj.script.*;
import org.bitcoinj.testing.*;
import org.easymock.*;
import org.junit.*;
import java.math.BigInteger;
import java.util.*;
import static org.bitcoinj.core.BlockTest.params;
import static org.bitcoinj.core.Utils.HEX;
@@ -205,6 +207,86 @@ public class TransactionTest {
assertEquals(tx.isMature(), false);
}
@Test
public void testCLTVPaymentChannelTransactionSpending() {
BigInteger time = BigInteger.valueOf(20);
ECKey from = new ECKey(), to = new ECKey(), incorrect = new ECKey();
Script outputScript = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
Transaction tx = new Transaction(PARAMS);
tx.addInput(new TransactionInput(PARAMS, tx, new byte[] {}));
tx.getInput(0).setSequenceNumber(0);
tx.setLockTime(time.subtract(BigInteger.ONE).longValue());
TransactionSignature fromSig =
tx.calculateSignature(0, from, outputScript, Transaction.SigHash.SINGLE, false);
TransactionSignature toSig =
tx.calculateSignature(0, to, outputScript, Transaction.SigHash.SINGLE, false);
TransactionSignature incorrectSig =
tx.calculateSignature(0, incorrect, outputScript, Transaction.SigHash.SINGLE, false);
Script scriptSig =
ScriptBuilder.createCLTVPaymentChannelInput(fromSig, toSig);
Script refundSig =
ScriptBuilder.createCLTVPaymentChannelRefund(fromSig);
Script invalidScriptSig1 =
ScriptBuilder.createCLTVPaymentChannelInput(fromSig, incorrectSig);
Script invalidScriptSig2 =
ScriptBuilder.createCLTVPaymentChannelInput(incorrectSig, toSig);
try {
scriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
} catch (ScriptException e) {
e.printStackTrace();
fail("Settle transaction failed to correctly spend the payment channel");
}
try {
refundSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
fail("Refund passed before expiry");
} catch (ScriptException e) { }
try {
invalidScriptSig1.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
fail("Invalid sig 1 passed");
} catch (ScriptException e) { }
try {
invalidScriptSig2.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
fail("Invalid sig 2 passed");
} catch (ScriptException e) { }
}
@Test
public void testCLTVPaymentChannelTransactionRefund() {
BigInteger time = BigInteger.valueOf(20);
ECKey from = new ECKey(), to = new ECKey(), incorrect = new ECKey();
Script outputScript = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
Transaction tx = new Transaction(PARAMS);
tx.addInput(new TransactionInput(PARAMS, tx, new byte[] {}));
tx.getInput(0).setSequenceNumber(0);
tx.setLockTime(time.add(BigInteger.ONE).longValue());
TransactionSignature fromSig =
tx.calculateSignature(0, from, outputScript, Transaction.SigHash.SINGLE, false);
TransactionSignature incorrectSig =
tx.calculateSignature(0, incorrect, outputScript, Transaction.SigHash.SINGLE, false);
Script scriptSig =
ScriptBuilder.createCLTVPaymentChannelRefund(fromSig);
Script invalidScriptSig =
ScriptBuilder.createCLTVPaymentChannelRefund(incorrectSig);
try {
scriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
} catch (ScriptException e) {
e.printStackTrace();
fail("Refund failed to correctly spend the payment channel");
}
try {
invalidScriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
fail("Invalid sig passed");
} catch (ScriptException e) { }
}
@Test
public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() {
Transaction tx = newTransaction();

View File

@@ -406,6 +406,12 @@ public class ScriptTest {
}
}
@Test
public void testCLTVPaymentChannelOutput() {
Script script = ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(20), new ECKey(), new ECKey());
assertTrue("script is locktime-verify", script.isSentToCLTVPaymentChannel());
}
@Test
public void getToAddress() throws Exception {
// pay to pubkey