3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-13 02:35:52 +00:00

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.listeners.WalletCoinEventListener;
import org.bitcoinj.core.TransactionConfidence.*; import org.bitcoinj.core.TransactionConfidence.*;
import org.bitcoinj.crypto.*; import org.bitcoinj.crypto.*;
import org.bitcoinj.params.*;
import org.bitcoinj.script.*; import org.bitcoinj.script.*;
import org.bitcoinj.signers.*; import org.bitcoinj.signers.*;
import org.bitcoinj.store.*; import org.bitcoinj.store.*;
@ -3643,6 +3642,24 @@ public class Wallet extends BaseTaggableObject
return req; 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. */ /** Copy data from payment request. */
public SendRequest fromPaymentDetails(PaymentDetails paymentDetails) { public SendRequest fromPaymentDetails(PaymentDetails paymentDetails) {
if (paymentDetails.hasMemo()) if (paymentDetails.hasMemo())
@ -4120,6 +4137,19 @@ public class Wallet extends BaseTaggableObject
if (key != null && (key.isEncrypted() || key.hasPrivKey())) if (key != null && (key.isEncrypted() || key.hasPrivKey()))
return true; 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; 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. * 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 * 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; 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) { private static boolean equalsRange(byte[] a, int start, byte[] b) {
if (start + b.length > a.length) if (start + b.length > a.length)
return false; return false;

View File

@ -23,6 +23,7 @@ import org.bitcoinj.core.Utils;
import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.crypto.TransactionSignature;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -435,4 +436,32 @@ public class ScriptBuilder {
checkArgument(data.length <= 40); checkArgument(data.length <= 40);
return new ScriptBuilder().op(OP_RETURN).data(data).build(); 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; package org.bitcoinj.core;
import org.bitcoinj.core.TransactionConfidence.*; import org.bitcoinj.core.TransactionConfidence.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.params.*; import org.bitcoinj.params.*;
import org.bitcoinj.script.*; import org.bitcoinj.script.*;
import org.bitcoinj.testing.*; import org.bitcoinj.testing.*;
import org.easymock.*; import org.easymock.*;
import org.junit.*; import org.junit.*;
import java.math.BigInteger;
import java.util.*; import java.util.*;
import static org.bitcoinj.core.BlockTest.params; import static org.bitcoinj.core.BlockTest.params;
import static org.bitcoinj.core.Utils.HEX; import static org.bitcoinj.core.Utils.HEX;
@ -205,6 +207,86 @@ public class TransactionTest {
assertEquals(tx.isMature(), false); 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 @Test
public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() { public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() {
Transaction tx = newTransaction(); 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 @Test
public void getToAddress() throws Exception { public void getToAddress() throws Exception {
// pay to pubkey // pay to pubkey

View File

@ -19,10 +19,7 @@ package org.bitcoinj.tools;
import org.bitcoinj.core.*; import org.bitcoinj.core.*;
import org.bitcoinj.core.Wallet.BalanceType; import org.bitcoinj.core.Wallet.BalanceType;
import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.*;
import org.bitcoinj.crypto.KeyCrypterException;
import org.bitcoinj.crypto.MnemonicCode;
import org.bitcoinj.crypto.MnemonicException;
import org.bitcoinj.net.discovery.DnsDiscovery; import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.RegTestParams;
@ -30,6 +27,7 @@ import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.protocols.payments.PaymentProtocol; import org.bitcoinj.protocols.payments.PaymentProtocol;
import org.bitcoinj.protocols.payments.PaymentProtocolException; import org.bitcoinj.protocols.payments.PaymentProtocolException;
import org.bitcoinj.protocols.payments.PaymentSession; import org.bitcoinj.protocols.payments.PaymentSession;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.store.*; import org.bitcoinj.store.*;
import org.bitcoinj.uri.BitcoinURI; import org.bitcoinj.uri.BitcoinURI;
import org.bitcoinj.uri.BitcoinURIParseException; import org.bitcoinj.uri.BitcoinURIParseException;
@ -173,6 +171,9 @@ public class WalletTool {
SYNC, SYNC,
RESET, RESET,
SEND, SEND,
SEND_CLTVPAYMENTCHANNEL,
SETTLE_CLTVPAYMENTCHANNEL,
REFUND_CLTVPAYMENTCHANNEL,
ENCRYPT, ENCRYPT,
DECRYPT, DECRYPT,
MARRY, MARRY,
@ -228,6 +229,8 @@ public class WalletTool {
parser.accepts("no-pki"); parser.accepts("no-pki");
parser.accepts("tor"); parser.accepts("tor");
parser.accepts("dump-privkeys"); parser.accepts("dump-privkeys");
OptionSpec<String> refundFlag = parser.accepts("refund-to").withRequiredArg();
OptionSpec<String> txHashFlag = parser.accepts("txhash").withRequiredArg();
options = parser.parse(args); options = parser.parse(args);
if (args.length == 0 || options.has("help") || if (args.length == 0 || options.has("help") ||
@ -366,6 +369,59 @@ public class WalletTool {
return; return;
} }
break; break;
case SEND_CLTVPAYMENTCHANNEL: {
if (!options.has(outputFlag)) {
System.err.println("You must specify a --output=addr:value");
return;
}
Coin fee = Coin.ZERO;
if (options.has("fee")) {
fee = parseCoin((String) options.valueOf("fee"));
}
if (!options.has("locktime")) {
System.err.println("You must specify a --locktime");
return;
}
String lockTime = (String) options.valueOf("locktime");
boolean allowUnconfirmed = options.has("allow-unconfirmed");
if (!options.has(refundFlag)) {
System.err.println("You must specify an address to refund money to after expiry: --refund-to=addr");
return;
}
sendCLTVPaymentChannel(refundFlag.value(options), outputFlag.value(options), fee, lockTime, allowUnconfirmed);
} break;
case SETTLE_CLTVPAYMENTCHANNEL: {
if (!options.has(outputFlag)) {
System.err.println("You must specify a --output=addr:value");
return;
}
Coin fee = Coin.ZERO;
if (options.has("fee")) {
fee = parseCoin((String) options.valueOf("fee"));
}
boolean allowUnconfirmed = options.has("allow-unconfirmed");
if (!options.has(txHashFlag)) {
System.err.println("You must specify the transaction to spend: --txhash=tx-hash");
return;
}
settleCLTVPaymentChannel(txHashFlag.value(options), outputFlag.value(options), fee, allowUnconfirmed);
} break;
case REFUND_CLTVPAYMENTCHANNEL: {
if (!options.has(outputFlag)) {
System.err.println("You must specify a --output=addr:value");
return;
}
Coin fee = Coin.ZERO;
if (options.has("fee")) {
fee = parseCoin((String) options.valueOf("fee"));
}
boolean allowUnconfirmed = options.has("allow-unconfirmed");
if (!options.has(txHashFlag)) {
System.err.println("You must specify the transaction to spend: --txhash=tx-hash");
return;
}
refundCLTVPaymentChannel(txHashFlag.value(options), outputFlag.value(options), fee, allowUnconfirmed);
} break;
case ENCRYPT: encrypt(); break; case ENCRYPT: encrypt(); break;
case DECRYPT: decrypt(); break; case DECRYPT: decrypt(); break;
case MARRY: marry(); break; case MARRY: marry(); break;
@ -513,36 +569,25 @@ public class WalletTool {
// Convert the input strings to outputs. // Convert the input strings to outputs.
Transaction t = new Transaction(params); Transaction t = new Transaction(params);
for (String spec : outputs) { for (String spec : outputs) {
String[] parts = spec.split(":");
if (parts.length != 2) {
System.err.println("Malformed output specification, must have two parts separated by :");
return;
}
String destination = parts[0];
try { try {
Coin value; OutputSpec outputSpec = new OutputSpec(spec);
if ("ALL".equalsIgnoreCase(parts[1])) if (outputSpec.isAddress()) {
value = wallet.getBalance(BalanceType.ESTIMATED); t.addOutput(outputSpec.value, outputSpec.addr);
else
value = parseCoin(parts[1]);
if (destination.startsWith("0")) {
// Treat as a raw public key.
byte[] pubKey = new BigInteger(destination, 16).toByteArray();
ECKey key = ECKey.fromPublicOnly(pubKey);
t.addOutput(value, key);
} else { } else {
// Treat as an address. t.addOutput(outputSpec.value, outputSpec.key);
Address addr = Address.fromBase58(params, destination);
t.addOutput(value, addr);
} }
} catch (WrongNetworkException e) { } catch (WrongNetworkException e) {
System.err.println("Malformed output specification, address is for a different network: " + parts[0]); System.err.println("Malformed output specification, address is for a different network: " + spec);
return; return;
} catch (AddressFormatException e) { } catch (AddressFormatException e) {
System.err.println("Malformed output specification, could not parse as address: " + parts[0]); System.err.println("Malformed output specification, could not parse as address: " + spec);
return; return;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
System.err.println("Malformed output specification, could not parse as value: " + parts[1]); System.err.println("Malformed output specification, could not parse as value: " + spec);
return;
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
return;
} }
} }
Wallet.SendRequest req = Wallet.SendRequest.forTx(t); Wallet.SendRequest req = Wallet.SendRequest.forTx(t);
@ -605,6 +650,335 @@ public class WalletTool {
} }
} }
static class OutputSpec {
public final Coin value;
public final Address addr;
public final ECKey key;
public OutputSpec(String spec) throws IllegalArgumentException {
String[] parts = spec.split(":");
if (parts.length != 2) {
throw new IllegalArgumentException("Malformed output specification, must have two parts separated by :");
}
String destination = parts[0];
if ("ALL".equalsIgnoreCase(parts[1]))
value = wallet.getBalance(BalanceType.ESTIMATED);
else
value = parseCoin(parts[1]);
if (destination.startsWith("0")) {
// Treat as a raw public key.
byte[] pubKey = new BigInteger(destination, 16).toByteArray();
key = ECKey.fromPublicOnly(pubKey);
addr = null;
} else {
// Treat as an address.
addr = Address.fromBase58(params, destination);
key = null;
}
}
public boolean isAddress() {
return addr != null;
}
}
private static void sendCLTVPaymentChannel(String refund, String output, Coin fee, String lockTimeStr, boolean allowUnconfirmed) throws VerificationException {
try {
// Convert the input strings to outputs.
ECKey outputKey, refundKey;
Coin value;
try {
OutputSpec outputSpec = new OutputSpec(output);
if (outputSpec.isAddress()) {
System.err.println("Output specification must be a public key");
return;
}
outputKey = outputSpec.key;
value = outputSpec.value;
byte[] refundPubKey = new BigInteger(refund, 16).toByteArray();
refundKey = ECKey.fromPublicOnly(refundPubKey);
} catch (WrongNetworkException e) {
System.err.println("Malformed output specification, address is for a different network.");
return;
} catch (AddressFormatException e) {
System.err.println("Malformed output specification, could not parse as address.");
return;
} catch (NumberFormatException e) {
System.err.println("Malformed output specification, could not parse as value.");
return;
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
return;
}
long lockTime;
try {
lockTime = parseLockTimeStr(lockTimeStr);
} catch (ParseException e) {
System.err.println("Could not understand --locktime of " + lockTimeStr);
return;
} catch (ScriptException e) {
throw new RuntimeException(e);
}
Wallet.SendRequest req = Wallet.SendRequest.toCLTVPaymentChannel(params, lockTime, refundKey, outputKey, value);
if (req.tx.getOutputs().size() == 1 && req.tx.getOutput(0).getValue().equals(wallet.getBalance())) {
log.info("Emptying out wallet, recipient may get less than what you expect");
req.emptyWallet = true;
}
req.fee = fee;
if (allowUnconfirmed) {
wallet.allowSpendingUnconfirmedTransactions();
}
if (password != null) {
req.aesKey = passwordToKey(true);
if (req.aesKey == null)
return; // Error message already printed.
}
wallet.completeTx(req);
System.out.println(req.tx.getHashAsString());
if (options.has("offline")) {
wallet.commitTx(req.tx);
return;
}
setup();
peers.start();
// Wait for peers to connect, the tx to be sent to one of them and for it to be propagated across the
// network. Once propagation is complete and we heard the transaction back from all our peers, it will
// be committed to the wallet.
peers.broadcastTransaction(req.tx).future().get();
// Hack for regtest/single peer mode, as we're about to shut down and won't get an ACK from the remote end.
List<Peer> peerList = peers.getConnectedPeers();
if (peerList.size() == 1)
peerList.get(0).ping().get();
} catch (BlockStoreException e) {
throw new RuntimeException(e);
} catch (KeyCrypterException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InsufficientMoneyException e) {
System.err.println("Insufficient funds: have " + wallet.getBalance().toFriendlyString());
}
}
/**
* Settles a CLTV payment channel transaction given that we own both private keys (ie. for testing).
* @param txHash
* @param output
* @param fee
* @param allowUnconfirmed
*/
private static void settleCLTVPaymentChannel(String txHash, String output, Coin fee, boolean allowUnconfirmed) {
try {
OutputSpec outputSpec;
Coin value;
try {
outputSpec = new OutputSpec(output);
value = outputSpec.value;
} catch (WrongNetworkException e) {
System.err.println("Malformed output specification, address is for a different network.");
return;
} catch (AddressFormatException e) {
System.err.println("Malformed output specification, could not parse as address.");
return;
} catch (NumberFormatException e) {
System.err.println("Malformed output specification, could not parse as value.");
return;
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
return;
}
Wallet.SendRequest req = outputSpec.isAddress() ?
Wallet.SendRequest.to(outputSpec.addr, value) :
Wallet.SendRequest.to(params, outputSpec.key, value);
req.fee = fee;
Transaction lockTimeVerify = wallet.getTransaction(Sha256Hash.wrap(txHash));
if (lockTimeVerify == null) {
System.err.println("Couldn't find transaction with given hash");
return;
}
TransactionOutput lockTimeVerifyOutput = null;
for (TransactionOutput out : lockTimeVerify.getOutputs()) {
if (out.getScriptPubKey().isSentToCLTVPaymentChannel()) {
lockTimeVerifyOutput = out;
}
}
if (lockTimeVerifyOutput == null) {
System.err.println("TX to spend wasn't sent to LockTimeVerify");
return;
}
if (!req.fee.add(value).equals(lockTimeVerifyOutput.getValue())) {
System.err.println("You must spend all the money in the input transaction");
}
if (allowUnconfirmed) {
wallet.allowSpendingUnconfirmedTransactions();
}
if (password != null) {
req.aesKey = passwordToKey(true);
if (req.aesKey == null)
return; // Error message already printed.
}
ECKey key1 = wallet.findKeyFromPubKey(
lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelSenderPubKey());
ECKey key2 = wallet.findKeyFromPubKey(
lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelRecipientPubKey());
if (key1 == null || key2 == null) {
System.err.println("Don't own private keys for both pubkeys");
return;
}
TransactionInput input = new TransactionInput(
params, req.tx, new byte[] {}, lockTimeVerifyOutput.getOutPointFor());
req.tx.addInput(input);
TransactionSignature sig1 =
req.tx.calculateSignature(0, key1, lockTimeVerifyOutput.getScriptPubKey(), Transaction.SigHash.SINGLE, false);
TransactionSignature sig2 =
req.tx.calculateSignature(0, key2, lockTimeVerifyOutput.getScriptPubKey(), Transaction.SigHash.SINGLE, false);
input.setScriptSig(ScriptBuilder.createCLTVPaymentChannelInput(sig1, sig2));
System.out.println(req.tx.getHashAsString());
if (options.has("offline")) {
wallet.commitTx(req.tx);
return;
}
setup();
peers.start();
// Wait for peers to connect, the tx to be sent to one of them and for it to be propagated across the
// network. Once propagation is complete and we heard the transaction back from all our peers, it will
// be committed to the wallet.
peers.broadcastTransaction(req.tx).future().get();
// Hack for regtest/single peer mode, as we're about to shut down and won't get an ACK from the remote end.
List<Peer> peerList = peers.getConnectedPeers();
if (peerList.size() == 1)
peerList.get(0).ping().get();
} catch (BlockStoreException e) {
throw new RuntimeException(e);
} catch (KeyCrypterException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* Refunds a CLTV payment channel transaction after the lock time has expired.
* @param txHash
* @param output
* @param fee
* @param allowUnconfirmed
*/
private static void refundCLTVPaymentChannel(String txHash, String output, Coin fee, boolean allowUnconfirmed) {
try {
OutputSpec outputSpec;
Coin value;
try {
outputSpec = new OutputSpec(output);
value = outputSpec.value;
} catch (WrongNetworkException e) {
System.err.println("Malformed output specification, address is for a different network.");
return;
} catch (AddressFormatException e) {
System.err.println("Malformed output specification, could not parse as address.");
return;
} catch (NumberFormatException e) {
System.err.println("Malformed output specification, could not parse as value.");
return;
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
return;
}
Wallet.SendRequest req = outputSpec.isAddress() ?
Wallet.SendRequest.to(outputSpec.addr, value) :
Wallet.SendRequest.to(params, outputSpec.key, value);
req.fee = fee;
Transaction lockTimeVerify = wallet.getTransaction(Sha256Hash.wrap(txHash));
if (lockTimeVerify == null) {
System.err.println("Couldn't find transaction with given hash");
return;
}
TransactionOutput lockTimeVerifyOutput = null;
for (TransactionOutput out : lockTimeVerify.getOutputs()) {
if (out.getScriptPubKey().isSentToCLTVPaymentChannel()) {
lockTimeVerifyOutput = out;
}
}
if (lockTimeVerifyOutput == null) {
System.err.println("TX to spend wasn't sent to LockTimeVerify");
return;
}
req.tx.setLockTime(lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelExpiry().longValue());
if (!req.fee.add(value).equals(lockTimeVerifyOutput.getValue())) {
System.err.println("You must spend all the money in the input transaction");
}
if (allowUnconfirmed) {
wallet.allowSpendingUnconfirmedTransactions();
}
if (password != null) {
req.aesKey = passwordToKey(true);
if (req.aesKey == null)
return; // Error message already printed.
}
ECKey key = wallet.findKeyFromPubKey(
lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelSenderPubKey());
if (key == null) {
System.err.println("Don't own private key for pubkey");
return;
}
TransactionInput input = new TransactionInput(
params, req.tx, new byte[] {}, lockTimeVerifyOutput.getOutPointFor());
input.setSequenceNumber(0);
req.tx.addInput(input);
TransactionSignature sig =
req.tx.calculateSignature(0, key, lockTimeVerifyOutput.getScriptPubKey(), Transaction.SigHash.SINGLE, false);
input.setScriptSig(ScriptBuilder.createCLTVPaymentChannelRefund(sig));
System.out.println(req.tx.getHashAsString());
if (options.has("offline")) {
wallet.commitTx(req.tx);
return;
}
setup();
peers.start();
// Wait for peers to connect, the tx to be sent to one of them and for it to be propagated across the
// network. Once propagation is complete and we heard the transaction back from all our peers, it will
// be committed to the wallet.
peers.broadcastTransaction(req.tx).future().get();
// Hack for regtest/single peer mode, as we're about to shut down and won't get an ACK from the remote end.
List<Peer> peerList = peers.getConnectedPeers();
if (peerList.size() == 1)
peerList.get(0).ping().get();
} catch (BlockStoreException e) {
throw new RuntimeException(e);
} catch (KeyCrypterException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
/** /**
* Parses the string either as a whole number of blocks, or if it contains slashes as a YYYY/MM/DD format date * Parses the string either as a whole number of blocks, or if it contains slashes as a YYYY/MM/DD format date
* and returns the lock time in wire format. * and returns the lock time in wire format.

View File

@ -62,6 +62,30 @@ Usage: wallet-tool --flags action-name
If --date is specified, that's the creation date. If --date is specified, that's the creation date.
If --unixtime is specified, that's the creation time and it overrides --date. If --unixtime is specified, that's the creation time and it overrides --date.
If you omit both options, the creation time is being cleared (set to 0). If you omit both options, the creation time is being cleared (set to 0).
send-cltvpaymentchannel
Creates and broadcasts a transaction paying to a CHECKLOCKTIMEVERIFY micropayment channel.
Requires a public key for the money recipient, public key to create the transactions (the
"return" address) and an expiry time.
Options:
--output=pubkey:value sets the amount to lock and the recipient
--refund-to=pubkey sets "our" public key
--fee=value sets the mining fee
--locktime=YYYY/MM/DD sets the expiry time for the channel
settle-cltvpaymentchannel
Creates and broadcasts a transaction settling a previous micropayment channel.
This tool, for testing, requires the presence of both private keys.
Options:
--output=pubkey:value sets the destination for the money
--fee=value sets the mining fee
--txhash=hash sets the transaction to spend
refund-cltvpaymentchannel
Creates and broadcasts a transaction refunding a previous micropayment channel.
This command can only be called once the expiry for the micropayment channel has passed -
the created transaction won't be accepted into the mempool until that point.
Options:
--output=pubkey:value sets the destination for the money
--fee=value sets the mining fee
--txhash=hash sets the transaction to spend
>>> GENERAL OPTIONS >>> GENERAL OPTIONS
--debuglog Enables logging from the core library. --debuglog Enables logging from the core library.