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:
parent
25db735b3a
commit
c9cce47962
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user