mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-11-03 14:07:14 +00:00
Support CLTV micropayment channels
Also extend WalletTool to send via, settle and refund these channels.
This commit is contained in:
committed by
Andreas Schildbach
parent
25db735b3a
commit
c9cce47962
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user