From 3ee7cd11eb31da06d79132f0023e6da7e534906d Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 3 Jul 2020 11:00:27 +0200 Subject: [PATCH] tx input construction from script type --- .../sparrowwallet/drongo/address/Address.java | 3 + .../drongo/address/P2PKAddress.java | 8 +- .../drongo/address/P2PKHAddress.java | 5 + .../drongo/address/P2SHAddress.java | 7 + .../drongo/address/P2WPKHAddress.java | 6 + .../drongo/address/P2WSHAddress.java | 5 + .../drongo/protocol/ChildMessage.java | 6 +- .../drongo/protocol/Message.java | 6 +- .../sparrowwallet/drongo/protocol/Script.java | 13 +- .../drongo/protocol/ScriptChunk.java | 36 +- .../drongo/protocol/ScriptType.java | 277 ++++++++++++++ .../drongo/protocol/Transaction.java | 46 +++ .../drongo/protocol/TransactionInput.java | 17 + .../drongo/protocol/TransactionOutPoint.java | 6 + .../drongo/protocol/TransactionOutput.java | 33 +- .../drongo/protocol/TransactionWitness.java | 41 ++- .../drongo/wallet/PriorityUtxoSelector.java | 59 +++ .../sparrowwallet/drongo/wallet/Wallet.java | 17 + .../drongo/protocol/TransactionTest.java | 340 +++++++++++++++++- 19 files changed, 890 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/drongo/wallet/PriorityUtxoSelector.java diff --git a/src/main/java/com/sparrowwallet/drongo/address/Address.java b/src/main/java/com/sparrowwallet/drongo/address/Address.java index 6ea15a1..44802d3 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/Address.java +++ b/src/main/java/com/sparrowwallet/drongo/address/Address.java @@ -2,6 +2,7 @@ package com.sparrowwallet.drongo.address; import com.sparrowwallet.drongo.protocol.Base58; import com.sparrowwallet.drongo.protocol.Bech32; +import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; import java.util.Arrays; @@ -31,6 +32,8 @@ public abstract class Address { public abstract ScriptType getScriptType(); + public abstract Script getOutputScript(); + public abstract byte[] getOutputScriptData(); public abstract String getOutputScriptDataType(); diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java index c68f67c..f168bdf 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java +++ b/src/main/java/com/sparrowwallet/drongo/address/P2PKAddress.java @@ -1,11 +1,9 @@ package com.sparrowwallet.drongo.address; import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; -import java.util.ArrayList; -import java.util.List; - public class P2PKAddress extends Address { private byte[] pubKey; @@ -22,6 +20,10 @@ public class P2PKAddress extends Address { return ScriptType.P2PK; } + public Script getOutputScript() { + return getScriptType().getOutputScript(pubKey); + } + @Override public byte[] getOutputScriptData() { return pubKey; diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2PKHAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2PKHAddress.java index d18f888..51b331a 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/P2PKHAddress.java +++ b/src/main/java/com/sparrowwallet/drongo/address/P2PKHAddress.java @@ -1,5 +1,6 @@ package com.sparrowwallet.drongo.address; +import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; public class P2PKHAddress extends Address { @@ -15,6 +16,10 @@ public class P2PKHAddress extends Address { return ScriptType.P2PKH; } + public Script getOutputScript() { + return getScriptType().getOutputScript(hash); + } + @Override public byte[] getOutputScriptData() { return hash; diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2SHAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2SHAddress.java index 86ddc68..d7465f8 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/P2SHAddress.java +++ b/src/main/java/com/sparrowwallet/drongo/address/P2SHAddress.java @@ -1,6 +1,7 @@ package com.sparrowwallet.drongo.address; import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; public class P2SHAddress extends Address { @@ -12,10 +13,16 @@ public class P2SHAddress extends Address { return 5; } + @Override public ScriptType getScriptType() { return ScriptType.P2SH; } + @Override + public Script getOutputScript() { + return getScriptType().getOutputScript(hash); + } + @Override public byte[] getOutputScriptData() { return hash; diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2WPKHAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2WPKHAddress.java index 679dded..d7a02f6 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/P2WPKHAddress.java +++ b/src/main/java/com/sparrowwallet/drongo/address/P2WPKHAddress.java @@ -1,6 +1,7 @@ package com.sparrowwallet.drongo.address; import com.sparrowwallet.drongo.protocol.Bech32; +import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; public class P2WPKHAddress extends Address { @@ -22,6 +23,11 @@ public class P2WPKHAddress extends Address { return ScriptType.P2WPKH; } + @Override + public Script getOutputScript() { + return getScriptType().getOutputScript(hash); + } + @Override public byte[] getOutputScriptData() { return hash; diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2WSHAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2WSHAddress.java index d23dc07..19d2e9c 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/P2WSHAddress.java +++ b/src/main/java/com/sparrowwallet/drongo/address/P2WSHAddress.java @@ -21,6 +21,11 @@ public class P2WSHAddress extends Address { return ScriptType.P2WSH; } + @Override + public Script getOutputScript() { + return getScriptType().getOutputScript(hash); + } + @Override public byte[] getOutputScriptData() { return hash; diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ChildMessage.java b/src/main/java/com/sparrowwallet/drongo/protocol/ChildMessage.java index e99e634..21f02ec 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ChildMessage.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ChildMessage.java @@ -4,7 +4,11 @@ public abstract class ChildMessage extends Message { protected Message parent; - public ChildMessage(byte[] rawtx, int offset) { + protected ChildMessage() { + super(); + } + + protected ChildMessage(byte[] rawtx, int offset) { super(rawtx, offset); } diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Message.java b/src/main/java/com/sparrowwallet/drongo/protocol/Message.java index cd575a5..4c1975a 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Message.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Message.java @@ -23,7 +23,11 @@ public abstract class Message { protected int length = UNKNOWN_LENGTH; - public Message(byte[] payload, int offset) { + protected Message() { + + } + + protected Message(byte[] payload, int offset) { this.payload = payload; this.cursor = this.offset = offset; diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java index f04331b..05435f4 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java @@ -41,10 +41,10 @@ public class Script { } private static final ScriptChunk[] STANDARD_TRANSACTION_SCRIPT_CHUNKS = { - new ScriptChunk(ScriptOpCodes.OP_DUP, null, 0), - new ScriptChunk(ScriptOpCodes.OP_HASH160, null, 1), - new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null, 23), - new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null, 24), + new ScriptChunk(ScriptOpCodes.OP_DUP, null), + new ScriptChunk(ScriptOpCodes.OP_HASH160, null), + new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null), + new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null), }; void parse() { @@ -52,7 +52,6 @@ public class Script { ByteArrayInputStream bis = new ByteArrayInputStream(program); int initialSize = bis.available(); while (bis.available() > 0) { - int startLocationInProgram = initialSize - bis.available(); int opcode = bis.read(); long dataToRead = -1; @@ -75,7 +74,7 @@ public class Script { ScriptChunk chunk; if (dataToRead == -1) { - chunk = new ScriptChunk(opcode, null, startLocationInProgram); + chunk = new ScriptChunk(opcode, null); } else { if (dataToRead > bis.available()) throw new ProtocolException("Push of data element that is larger than remaining data"); @@ -84,7 +83,7 @@ public class Script { throw new ProtocolException(); } - chunk = new ScriptChunk(opcode, data, startLocationInProgram); + chunk = new ScriptChunk(opcode, data); } // Save some memory by eliminating redundant copies of the same chunk objects. for (ScriptChunk c : STANDARD_TRANSACTION_SCRIPT_CHUNKS) { diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java index b84a8e2..5a2e015 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java @@ -22,16 +22,38 @@ public class ScriptChunk { */ public final byte[] data; - private int startLocationInProgram; - public ScriptChunk(int opcode, byte[] data) { - this(opcode, data, -1); - } - - public ScriptChunk(int opcode, byte[] data, int startLocationInProgram) { this.opcode = opcode; this.data = data; - this.startLocationInProgram = startLocationInProgram; + } + + public static ScriptChunk fromOpcode(int opcode) { + return new ScriptChunk(opcode, null); + } + + public static ScriptChunk fromData(byte[] data) { + byte[] copy = Arrays.copyOf(data, data.length); + int opcode; + if (data.length == 0) { + opcode = OP_0; + } else if (data.length == 1) { + byte b = data[0]; + if (b >= 1 && b <= 16) { + opcode = Script.encodeToOpN(b); + } else { + opcode = 1; + } + } else if (data.length < OP_PUSHDATA1) { + opcode = data.length; + } else if (data.length < 256) { + opcode = OP_PUSHDATA1; + } else if (data.length < 65536) { + opcode = OP_PUSHDATA2; + } else { + opcode = OP_PUSHDATA4; + } + + return new ScriptChunk(opcode, copy); } public boolean equalsOpCode(int opcode) { diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java index d74765c..96cd790 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java @@ -92,6 +92,33 @@ public enum ScriptType { return ECKey.fromPublicOnly(script.chunks.get(0).data); } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + byte[] signatureBytes = signature.encodeToBitcoin(); + ScriptChunk signatureChunk = ScriptChunk.fromData(signatureBytes); + return new Script(List.of(signatureChunk)); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + @Override public List getAllowedPolicyTypes() { return List.of(SINGLE); @@ -171,6 +198,35 @@ public enum ScriptType { return script.chunks.get(2).data; } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + byte[] signatureBytes = signature.encodeToBitcoin(); + ScriptChunk signatureChunk = ScriptChunk.fromData(signatureBytes); + byte[] pubKeyBytes = pubKey.getPubKey(); + ScriptChunk pubKeyChunk = ScriptChunk.fromData(pubKeyBytes); + return new Script(List.of(signatureChunk, pubKeyChunk)); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + @Override public List getAllowedPolicyTypes() { return List.of(SINGLE); @@ -218,6 +274,10 @@ public enum ScriptType { @Override public Script getOutputScript(int threshold, List pubKeys) { + if(threshold > pubKeys.size()) { + throw new ProtocolException("Threshold of " + threshold + " is greater than number of pubKeys provided (" + pubKeys.size() + ")"); + } + List pubKeyBytes = new ArrayList<>(); for(ECKey key : pubKeys) { pubKeyBytes.add(key.getPubKey()); @@ -310,6 +370,42 @@ public enum ScriptType { return decodeFromOpN(script.chunks.get(0).opcode); } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException(getName() + " is a multisig script type"); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException(getName() + " is a multisig script type"); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + if(threshold != signatures.size()) { + throw new ProtocolException("Only " + signatures.size() + " signatures provided to meet a multisig threshold of " + threshold); + } + + List chunks = new ArrayList<>(signatures.size() + 1); + ScriptChunk opZero = ScriptChunk.fromOpcode(OP_0); + chunks.add(opZero); + for(TransactionSignature signature : signatures) { + byte[] signatureBytes = signature.encodeToBitcoin(); + chunks.add(ScriptChunk.fromData(signatureBytes)); + } + + return new Script(chunks); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig); + } + @Override public List getAllowedPolicyTypes() { return List.of(MULTI); @@ -395,6 +491,41 @@ public enum ScriptType { return script.chunks.get(1).data; } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys"); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys"); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + Script redeemScript = MULTISIG.getOutputScript(threshold, pubKeys); + if(!scriptPubKey.equals(getOutputScript(redeemScript))) { + throw new ProtocolException("P2SH scriptPubKey hash does not match constructed redeem script hash"); + } + + Script multisigScript = MULTISIG.getMultisigScriptSig(redeemScript, threshold, pubKeys, signatures); + List chunks = new ArrayList<>(multisigScript.getChunks()); + ScriptChunk redeemScriptChunk = ScriptChunk.fromData(redeemScript.getProgram()); + chunks.add(redeemScriptChunk); + + return new Script(chunks); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig); + } + @Override public List getAllowedPolicyTypes() { return List.of(MULTI); @@ -461,6 +592,38 @@ public enum ScriptType { return P2SH.getHashFromScript(script); } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + Script redeemScript = P2WPKH.getOutputScript(pubKey); + if(!scriptPubKey.equals(P2SH.getOutputScript(redeemScript))) { + throw new ProtocolException(getName() + " scriptPubKey hash does not match constructed redeem script hash"); + } + + ScriptChunk redeemScriptChunk = ScriptChunk.fromData(redeemScript.getProgram()); + return new Script(List.of(redeemScriptChunk)); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature); + TransactionWitness witness = new TransactionWitness(transaction, pubKey, signature); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + @Override public List getAllowedPolicyTypes() { return List.of(SINGLE); @@ -523,6 +686,40 @@ public enum ScriptType { return P2SH.getHashFromScript(script); } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys"); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys"); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys); + Script redeemScript = P2WSH.getOutputScript(witnessScript); + if(!scriptPubKey.equals(P2SH.getOutputScript(redeemScript))) { + throw new ProtocolException("P2SH scriptPubKey hash does not match constructed redeem script hash"); + } + + ScriptChunk redeemScriptChunk = ScriptChunk.fromData(redeemScript.getProgram()); + return new Script(List.of(redeemScriptChunk)); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures); + Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys); + TransactionWitness witness = new TransactionWitness(transaction, signatures, witnessScript); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness); + } + @Override public List getAllowedPolicyTypes() { return List.of(MULTI, CUSTOM); @@ -593,6 +790,36 @@ public enum ScriptType { return script.chunks.get(1).data; } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + if(!scriptPubKey.equals(getOutputScript(pubKey))) { + throw new ProtocolException("P2WPKH scriptPubKey hash does not match constructed pubkey script hash"); + } + + return new Script(new byte[0]); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature); + TransactionWitness witness = new TransactionWitness(transaction, pubKey, signature); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + throw new ProtocolException(getName() + " is not a multisig script type"); + } + @Override public List getAllowedPolicyTypes() { return List.of(SINGLE); @@ -667,6 +894,38 @@ public enum ScriptType { return script.chunks.get(1).data; } + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys"); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + throw new ProtocolException("Only multisig scriptSigs supported for " + getName() + " scriptPubKeys"); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys); + if(!scriptPubKey.equals(P2WSH.getOutputScript(witnessScript))) { + throw new ProtocolException("P2WSH scriptPubKey hash does not match constructed witness script hash"); + } + + return new Script(new byte[0]); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures) { + Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures); + Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys); + TransactionWitness witness = new TransactionWitness(transaction, signatures, witnessScript); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness); + } + @Override public List getAllowedPolicyTypes() { return List.of(MULTI, CUSTOM); @@ -761,12 +1020,30 @@ public enum ScriptType { throw new ProtocolException("Script type " + this + " is not a multisig script"); } + public abstract Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature); + + public abstract TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature); + + public abstract Script getMultisigScriptSig(Script scriptPubKey, int threshold, List pubKeys, List signatures); + + public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List pubKeys, List signatures); + public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH}; public static List getScriptTypesForPolicyType(PolicyType policyType) { return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList()); } + public static ScriptType getType(Script script) { + for(ScriptType type : values()) { + if(type.isScriptType(script)) { + return type; + } + } + + return null; + } + @Override public String toString() { return name; diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java index 6fff614..b152f8d 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java @@ -33,6 +33,13 @@ public class Transaction extends ChildMessage { private ArrayList inputs; private ArrayList outputs; + public Transaction() { + version = 1; + inputs = new ArrayList<>(); + outputs = new ArrayList<>(); + length = 8; + } + public Transaction(byte[] rawtx) { super(rawtx, 0); } @@ -125,6 +132,11 @@ public class Transaction extends ChildMessage { } public void setSegwitVersion(int segwitVersion) { + if(!segwit) { + adjustLength(2); + this.segwit = true; + } + this.segwitVersion = segwitVersion; } @@ -272,10 +284,44 @@ public class Transaction extends ChildMessage { return Collections.unmodifiableList(inputs); } + public TransactionInput addInput(Sha256Hash spendTxHash, long outputIndex, Script script) { + return addInput(new TransactionInput(this, new TransactionOutPoint(spendTxHash, outputIndex), script.getProgram())); + } + + public TransactionInput addInput(Sha256Hash spendTxHash, long outputIndex, Script script, TransactionWitness witness) { + return addInput(new TransactionInput(this, new TransactionOutPoint(spendTxHash, outputIndex), script.getProgram(), witness)); + } + + public TransactionInput addInput(TransactionInput input) { + input.setParent(this); + inputs.add(input); + adjustLength(inputs.size(), input.length); + return input; + } + public List getOutputs() { return Collections.unmodifiableList(outputs); } + public TransactionOutput addOutput(long value, Script script) { + return addOutput(new TransactionOutput(this, value, script)); + } + + public TransactionOutput addOutput(long value, Address address) { + return addOutput(new TransactionOutput(this, value, address.getOutputScript())); + } + + public TransactionOutput addOutput(long value, ECKey pubkey) { + return addOutput(new TransactionOutput(this, value, ScriptType.P2PK.getOutputScript(pubkey))); + } + + public TransactionOutput addOutput(TransactionOutput output) { + output.setParent(this); + outputs.add(output); + adjustLength(outputs.size(), output.length); + return output; + } + public void verify() throws VerificationException { if (inputs.size() == 0 || outputs.size() == 0) throw new VerificationException.EmptyInputsOrOutputs(); diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java index a531aef..8938052 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java @@ -25,6 +25,23 @@ public class TransactionInput extends ChildMessage { private TransactionWitness witness; + public TransactionInput(Transaction transaction, TransactionOutPoint outpoint, byte[] scriptBytes) { + this(transaction, outpoint, scriptBytes, null); + } + + public TransactionInput(Transaction transaction, TransactionOutPoint outpoint, byte[] scriptBytes, TransactionWitness witness) { + setParent(transaction); + this.sequence = SEQUENCE_LOCKTIME_DISABLED; + this.outpoint = outpoint; + this.outpoint.setParent(this); + this.scriptBytes = scriptBytes; + this.witness = witness; + length = 40 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length); + if(witness != null) { + transaction.adjustLength(witness.getLength()); + } + } + public TransactionInput(Transaction transaction, byte[] rawtx, int offset) { super(rawtx, offset); setParent(transaction); diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutPoint.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutPoint.java index f7c60d5..935673f 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutPoint.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutPoint.java @@ -18,6 +18,12 @@ public class TransactionOutPoint extends ChildMessage { private Address[] addresses = new Address[0]; + public TransactionOutPoint(Sha256Hash hash, long index) { + this.hash = hash; + this.index = index; + length = MESSAGE_LENGTH; + } + public TransactionOutPoint(byte[] rawtx, int offset, Message parent) { super(rawtx, offset); setParent(parent); diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java index 209c489..32559bc 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java @@ -3,7 +3,6 @@ package com.sparrowwallet.drongo.protocol; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -19,27 +18,22 @@ public class TransactionOutput extends ChildMessage { private Address[] addresses = new Address[0]; + public TransactionOutput(Transaction transaction, long value, Script script) { + this(transaction, value, script.getProgram()); + } + + public TransactionOutput(Transaction transaction, long value, byte[] scriptBytes) { + this.value = value; + this.scriptBytes = scriptBytes; + setParent(transaction); + length = 8 + VarInt.sizeOf(scriptBytes.length) + scriptBytes.length; + } + public TransactionOutput(Transaction parent, byte[] rawtx, int offset) { super(rawtx, offset); setParent(parent); } - public TransactionOutput(Transaction parent, long value, byte[] scriptBytes) { - super(new byte[0], 0); - this.value = value; - this.scriptBytes = scriptBytes; - setParent(parent); - length = 8 + VarInt.sizeOf(scriptBytes.length) + scriptBytes.length; - - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - bitcoinSerializeToStream(baos); - payload = baos.toByteArray(); - } catch(IOException e) { - //ignore - } - } - protected void parse() throws ProtocolException { value = readInt64(); int scriptLen = (int) readVarInt(); @@ -78,6 +72,11 @@ public class TransactionOutput extends ChildMessage { this.addresses = addresses; } + public Sha256Hash getHash() { + Transaction transaction = (Transaction)parent; + return transaction.getTxId(); + } + public int getIndex() { Transaction transaction = (Transaction)parent; return transaction.getOutputs().indexOf(this); diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java index 5aeb2b0..eb877ed 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java @@ -1,5 +1,6 @@ package com.sparrowwallet.drongo.protocol; +import com.sparrowwallet.drongo.crypto.ECKey; import org.bouncycastle.util.encoders.Hex; import java.io.ByteArrayOutputStream; @@ -13,6 +14,30 @@ import java.util.List; public class TransactionWitness extends ChildMessage { private List pushes; + public TransactionWitness(Transaction transaction, ECKey pubKey, TransactionSignature signature) { + setParent(transaction); + this.pushes = new ArrayList<>(); + pushes.add(signature.encodeToBitcoin()); + pushes.add(pubKey.getPubKey()); + } + + public TransactionWitness(Transaction transaction, List signatures, Script witnessScript) { + setParent(transaction); + this.pushes = new ArrayList<>(); + if(ScriptType.MULTISIG.isScriptType(witnessScript)) { + pushes.add(new byte[] { ScriptOpCodes.OP_0 }); + } + for(TransactionSignature signature : signatures) { + pushes.add(signature.encodeToBitcoin()); + } + pushes.add(witnessScript.getProgram()); + } + + public TransactionWitness(Transaction transaction, List witnesses) { + setParent(transaction); + this.pushes = witnesses; + } + public TransactionWitness(Transaction parent, byte[] rawtx, int offset) { super(rawtx, offset); setParent(parent); @@ -53,8 +78,12 @@ public class TransactionWitness extends ChildMessage { int length = new VarInt(pushes.size()).getSizeInBytes(); for (int i = 0; i < pushes.size(); i++) { byte[] push = pushes.get(i); - length += new VarInt(push.length).getSizeInBytes(); - length += push.length; + if(push.length == 1 && push[0] == 0) { + length++; + } else { + length += new VarInt(push.length).getSizeInBytes(); + length += push.length; + } } return length; @@ -64,8 +93,12 @@ public class TransactionWitness extends ChildMessage { stream.write(new VarInt(pushes.size()).encode()); for (int i = 0; i < pushes.size(); i++) { byte[] push = pushes.get(i); - stream.write(new VarInt(push.length).encode()); - stream.write(push); + if(push.length == 1 && push[0] == 0) { + stream.write(push); + } else { + stream.write(new VarInt(push.length).encode()); + stream.write(push); + } } } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/PriorityUtxoSelector.java b/src/main/java/com/sparrowwallet/drongo/wallet/PriorityUtxoSelector.java new file mode 100644 index 0000000..8aed731 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/wallet/PriorityUtxoSelector.java @@ -0,0 +1,59 @@ +package com.sparrowwallet.drongo.wallet; + +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Collectors; + +public class PriorityUtxoSelector implements UtxoSelector { + private final int currentBlockHeight; + + public PriorityUtxoSelector(int currentBlockHeight) { + this.currentBlockHeight = currentBlockHeight; + } + + @Override + public Collection select(long targetValue, Collection candidates) { + List selected = new ArrayList<>(); + + List sorted = candidates.stream().filter(ref -> ref.getHeight() != 0).collect(Collectors.toList()); + sort(sorted); + + long total = 0; + for(BlockTransactionHashIndex reference : sorted) { + if(total > targetValue) { + break; + } + + selected.add(reference); + total += reference.getValue(); + } + + return selected; + } + + private void sort(List outputs) { + outputs.sort((a, b) -> { + int depthA = currentBlockHeight - a.getHeight(); + int depthB = currentBlockHeight - b.getHeight(); + + Long valueA = a.getValue(); + Long valueB = b.getValue(); + + BigInteger coinDepthA = BigInteger.valueOf(depthA).multiply(BigInteger.valueOf(valueA)); + BigInteger coinDepthB = BigInteger.valueOf(depthB).multiply(BigInteger.valueOf(valueB)); + + int coinDepthCompare = coinDepthB.compareTo(coinDepthA); + if (coinDepthCompare != 0) { + return coinDepthCompare; + } + + // The "coin*days" destroyed are equal, sort by value alone to get the lowest transaction size. + int coinValueCompare = valueB.compareTo(valueA); + if (coinValueCompare != 0) { + return coinValueCompare; + } + + return a.compareTo(b); + }); + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 26c6f92..0270ddf 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -205,6 +205,23 @@ public class Wallet { } } + public Map getWalletUtxos() { + Map walletUtxos = new TreeMap<>(); + + getWalletUtxos(walletUtxos, getNode(KeyPurpose.RECEIVE)); + getWalletUtxos(walletUtxos, getNode(KeyPurpose.CHANGE)); + + return walletUtxos; + } + + private void getWalletUtxos(Map walletUtxos, WalletNode purposeNode) { + for(WalletNode addressNode : purposeNode.getChildren()) { + for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs()) { + walletUtxos.put(utxo, addressNode); + } + } + } + public void clearNodes() { purposeNodes.clear(); transactions.clear(); diff --git a/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java b/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java index 7a08ecf..5155827 100644 --- a/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java +++ b/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java @@ -2,11 +2,15 @@ package com.sparrowwallet.drongo.protocol; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; -import com.sparrowwallet.drongo.address.P2PKHAddress; +import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.crypto.ECKey; import org.junit.Assert; import org.junit.Test; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + public class TransactionTest { @Test public void verifyP2WPKH() { @@ -125,4 +129,338 @@ public class TransactionTest { TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("30440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783"), true, true); Assert.assertTrue(pubKey.verify(hash, signature)); } + + @Test + public void verifyConstructedTxLengthP2PKH() throws NonStandardScriptException, IOException { + String hex = "0100000003c07f2ee6dd4e55c6eefdc53659d1fb340beb5eb824d13bc15ba5269ade8de446000000006b483045022100d3f7526a8d1e22233c1f193b63f55406b32010aefeecdc802c07829b583d53a002205f1b666f156433baf6e976b8c43702cfe098e6d6c3c90e4bf2d24eeb1724740a012102faea485f773dbc2f57fe8cf664781a58d499c1f10ad55d370d5b08b92b8ee0c4ffffffffcac7a96d74d8a2b9177c7e0ce735f366d717e759d1f07bbd8a6db55e4b21304e000000006b483045022100d11822be0768c78cdb28ce613051facfa68c6689199505e7d0c75e95b7bd210c02202c5a610ceab38fc6816f6b792c43a1a25ae8507e80cd657dbfecfbff804a455101210287571cbb133887664c47917df7192017906916f7ce470532699c00ae4f10a178ffffffff3b16c58d5d76e119d337a56751b62b60c614ceca73d8e6403476c9e5a74497ab000000006b483045022100cb865e7b13f61f5968a734e0d8257fca72ad6f6b37c80e409e7f986a94f1269d022025e28e140e8087f1804a79b072ae18f69064f53223f2baa169685fe951f16b72012103f23d4fb4ab152b5f6b5e4a0bf79cfcac071c1f2cf07211c8cd176469b2a00628ffffffff02b3070000000000001976a914c3a1a5b559ff4db7f9c92c3d10274a3a18dcea3788ac4be28a00000000001976a914fe0c8a170be39d30f5447e57556e7836ed29e49088ac00000000"; + Transaction parsedTransaction = new Transaction(Utils.hexToBytes(hex)); + + Transaction transaction = new Transaction(); + for(TransactionInput txInput : parsedTransaction.getInputs()) { + transaction.addInput(txInput.getOutpoint().getHash(), txInput.getOutpoint().getIndex(), txInput.getScriptSig()); + } + + for(TransactionOutput txOutput : parsedTransaction.getOutputs()) { + Address address = txOutput.getScript().getToAddresses()[0]; + transaction.addOutput(txOutput.getValue(), address); + } + + Assert.assertEquals(parsedTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(hex, constructedHex); + } + + @Test + public void verifyConstructedTxLengthP2WPKH() throws NonStandardScriptException, IOException { + String hex = "020000000001014596cc6219630c13cbca099838c2fb0920cde29de1e5473087de8bbce06b9f510100000000ffffffff02502a4b000000000017a914b1cd708c9d49c7ad6ec851ad7f24076233fa7cfb8772915600000000001600145279dddc177883923bcf3bd5aab50e725dca01f302483045022100e9056474685b7d885956c7c7e5ac77e1249373e5d222b13620dcde6a63e337d602206ebb59c1834e991e9c9f6129a78c7669cfc4c41c4d19c6be4dabe6749715d5ee01210278f5f957591a07a51fc5033c3407de2ff722b0a5f98e91c9a9e1e038c9b1b59300000000"; + Transaction parsedTransaction = new Transaction(Utils.hexToBytes(hex)); + + Transaction transaction = new Transaction(); + transaction.setVersion(parsedTransaction.getVersion()); + transaction.setSegwitVersion(parsedTransaction.getSegwitVersion()); + for(TransactionInput txInput : parsedTransaction.getInputs()) { + transaction.addInput(txInput.getOutpoint().getHash(), txInput.getOutpoint().getIndex(), txInput.getScriptSig(), txInput.getWitness()); + } + + for(TransactionOutput txOutput : parsedTransaction.getOutputs()) { + Address address = txOutput.getScript().getToAddresses()[0]; + transaction.addOutput(txOutput.getValue(), address); + } + + Assert.assertEquals(parsedTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(hex, constructedHex); + } + + @Test + public void verifyConstructedTxLengthP2WPKHMulti() throws NonStandardScriptException, IOException { + String hex = "02000000000102ba4dc5a4a14bfaa941b7d115b379b5e15f960635cf694c178b9116763cbd63b11600000017160014fc164cbcac023f5eacfcead2d17d8768c41949affeffffff074d44d2856beb68ba52e8832da60a1682768c2421c2d9a8109ef4e66babd1fd1e000000171600148c3098be6b430859115f5ee99c84c368afecd0481500400002305310000000000017a914ffaf369c2212b178c7a2c21c9ccdd5d126e74c4187327f0300000000001976a914a7cda2e06b102a143ab606937a01d152e300cd3e88ac02473044022006da0ca227f765179219e08a33026b94e7cacff77f87b8cd8eb1b46d6dda11d6022064faa7912924fd23406b6ed3328f1bbbc3760dc51109a49c1b38bf57029d304f012103c6a2fcd030270427d4abe1041c8af929a9e2dbab07b243673453847ab842ee1f024730440220786316a16095105a0af28dccac5cf80f449dea2ea810a9559a89ecb989c2cb3d02205cbd9913d1217ffec144ae4f2bd895f16d778c2ec49ae9c929fdc8bcc2a2b1db0121024d4985241609d072a59be6418d700e87688f6c4d99a51ad68e66078211f076ee38820900"; + Transaction parsedTransaction = new Transaction(Utils.hexToBytes(hex)); + + Transaction transaction = new Transaction(); + transaction.setVersion(parsedTransaction.getVersion()); + transaction.setSegwitVersion(parsedTransaction.getSegwitVersion()); + transaction.setLocktime(parsedTransaction.getLocktime()); + for(TransactionInput txInput : parsedTransaction.getInputs()) { + TransactionInput newInput = transaction.addInput(txInput.getOutpoint().getHash(), txInput.getOutpoint().getIndex(), txInput.getScriptSig(), txInput.getWitness()); + newInput.setSequenceNumber(txInput.getSequenceNumber()); + } + + for(TransactionOutput txOutput : parsedTransaction.getOutputs()) { + Address address = txOutput.getScript().getToAddresses()[0]; + transaction.addOutput(txOutput.getValue(), address); + } + + Assert.assertEquals(parsedTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(hex, constructedHex); + } + + @Test + public void verifyReconstructedTxP2PK() throws NonStandardScriptException, IOException, InvalidAddressException { + String spent0Hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804e6ed5b1b02b000ffffffff0100f2052a01000000434104ab3779ba979cd2f7d76fd1b6a57f42bf4bdd9210409a693a46d6d426c0ba021aca2f364ce5141b7721b47fb5f34ce7301abbab24c067048b721c633ae65e1af0ac00000000"; + Transaction spent2Transaction = new Transaction(Utils.hexToBytes(spent0Hex)); + + TransactionOutput spent0Output = spent2Transaction.getOutputs().get(0); + ScriptType spent0ScriptType = ScriptType.getType(spent0Output.getScript()); + Assert.assertEquals(ScriptType.P2PK, spent0ScriptType); + ECKey key0 = ECKey.fromPublicOnly(spent0Output.getScript().getChunks().get(0).getData()); + + String spendingHex = "010000000183ec1de4385a9617e0ea098ab28936e22757b370c73132028f9d7eed08478db70000000049483045022100c9455b5b385292ca8783201d030ed3e091a56d8bc4f030b7ed1eec20cf9110d2022020b3455f661d466b55cfaae0dbd6e6f861e82f64f225861217b07315167e1b1501ffffffff02005ed0b2000000001976a914aa1cfb996782dfac1b860599d512ed6967e2d25a88ac00943577000000001976a9145fbc2d7b0018d31b5e6628150e5485af17b3fd1988ac00000000"; + Transaction spendingTransaction = new Transaction(Utils.hexToBytes(spendingHex)); + + TransactionInput input0 = spendingTransaction.getInputs().get(0); + TransactionSignature signature0 = input0.getScriptSig().getChunks().get(0).getSignature(); + + Transaction transaction = new Transaction(); + spent0ScriptType.addSpendingInput(transaction, spent0Output, key0, signature0); + + transaction.addOutput(3000000000L, Address.fromString("1GWUbNagGsvpwygRCjoczegGVDvpm5fLV8")); + transaction.addOutput(2000000000L, Address.fromString("19jCd38mHkNcXiGF4AjUCoJBSo7iqqjRHT")); + + Assert.assertEquals(spendingTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(spendingHex, constructedHex); + } + + @Test + public void verifyReconstructedTxP2PKH() throws NonStandardScriptException, IOException, InvalidAddressException { + String spent0Hex = "01000000000104923d5f8afc75ec6fafb836b694482ce91a8048c6512ecbbf52e3f9963db5f3000000000023220020d453a61c50faeaa771b9c00f4f6e61d0d38595ca72c614d7533c1650aff3bc24ffffffff5103933b1731e0a5f0b9fce310e27b75638c2951057142dbd984113bce3a5a8000000000232200204394c0daac00d1b6a974adbe592b298c871662931f6a297cde2fa74d3b641df0ffffffff36916f6416d61e320e585798a2f65a4b6ca8ebca640bd7d59a6ad0a83a6ed0bc5e0000002322002082715253072e9e37758b7c65e9b9dd68e21e3ad0559197be5dcfa3d18df8ec3dffffffff404e8656a6f98f00a184b32443ea5b6fd25d8204f8f5ad400e8deb9ae4363de709000000232200208efeaf9ab3d6d8d7c90304a982f67cc61d998ab0e2a4cb236d2dbb4fb2f0a0b5ffffffff60044000000000000017a91482c52f8c8a158a3fbcb0e43caf8103dc81f2e67a872a5a0000000000001976a9141c871707dab7dbb39aed712054b694652d43be7b88ac029200000000000017a9149fa63c45f31746370f4dc428718c5a1a3be49c7e8754d50000000000001976a914e9514a1955b793edf6b0f339211e8e4549ad60d588ac825501000000000017a91453ad08fcb81f1d80f4833c8e27aeb7aaa847cd3287c05701000000000017a9141396a674eab44afb86d46bcca1de2f1e1032593987b36c01000000000017a914c60c91e8a00781fcf55d9f86a31b5c9fd0c6bbd2879cae01000000000017a914d0d18d5e4fd5d59253ac57435e46df6f9039e57b87d5c201000000000017a914cf268163c27b69d15e9b1b64d4d2579e2909db378776eb0100000000001976a91427a45fb91b4f984dce5d10395af0b8a1f7fd42f388acdf610200000000001976a91455b6ee0b10a78292c240bb6376de98816a51049588aca46c0200000000001976a91410c13be439737abc292602d5eb81c3f254fd185288acfc7f02000000000017a9141652910080da8eb88ab327f501aa9d1322263b198767d302000000000017a9140c87f714f9c41b3107d3b7c7b813acb97a8092d3870df10200000000001976a914af12f81b449fb8e1adb0726a4b7bbb942b43dea688aca52a0300000000001976a914eb2beef07bd84514b390df99888ef1b763c9a65b88ac505503000000000017a9143a356b9a43258c0093c7ea2bf64ee3b0cccb4e8b87770004000000000017a914187eb7b8dff49c59d10334b9e6020dc4298668488722010400000000001976a914bc93dbf815ff9d925247b3159db832fb223af56288ac3a4a0400000000001976a91455e71a224f46fb422da845bfbfbedbedbebf6ceb88acc4b404000000000017a914c3aa990232b11bb6ca32b645c81dbe8511e8c7c5877cd904000000000017a9140e6be84ec77846a0f6cbde6b7ff8146793cad4738766ef04000000000017a91420e940dea42c49cb7c579a1266f7d3131c84e1da8766ef04000000000017a9149d56e1f7d4b7c293ab6d2abf03873e6c5fc7c31787061505000000000017a914acbe72dcc22303bac3b2eced921c1eefa290224987ff2f0500000000001976a914767d992989274817207c540d419c0bba95d6304388ac303c0500000000001976a91416963929eacb15be5ab4f1095ea49c1e7f06822e88ac663006000000000017a914d0e9d37cb183f77d404f442e5aa74559282ed76887887306000000000017a914db0439670345fe52363daead83d86fa528e05915870e7c06000000000017a914517a8ef91ed0a7c0c4e6837b17da68823a752e77879faa0600000000001976a914e887836b6491281543e3c9538ea6e5f367f3720b88ac60d80700000000001976a9148629cecb949fae32214c40c560cbf34625796fd288ac29220800000000001976a9141177bbb6bc10c4b121e4d93d44f2907635c5c5b188ac7e7d0800000000001976a91498800085638b60bd6e423e771930e7399926c71788acff7d08000000000017a9140b3a7f22301092e6ca74a9e357b6cc11903667be8719820800000000001976a91450ff7fbaa3115cb55b253ade8f7c08a0a0d8ae5a88acd04e0900000000001976a914924b9ddbf293b0cfeb9e33099ad81e777ecb2f7c88ac80020a000000000017a91455b5ebe096552dbc723d525490a31635a970cbb687e32f0a000000000017a9142f980452bc0d24f3419af297e010d6d66fa2819c87aad10a00000000001976a9147371e5f8991801f14b21ebd2d0361cf4274e506788ac2be90a00000000001976a91441d743c15f84d1f4639f46c963803c7857a43d1288acf8570b000000000017a914f74c8971988af3bc5aea2ab2690a06537c25a3be874c0d0c00000000001976a914576791e848aff210892f8d77f1e6a117519dae9588ac39270c00000000001976a9147761d002ee7737ef8e4e6f50536026602eac92dc88ace97f0d00000000001976a914fe0c8a170be39d30f5447e57556e7836ed29e49088ac04390e000000000017a914418f14799b1eb1234daa82f127b16c199e252a1b87d9a30f00000000001976a91423d7ef491a2bfa1ffe037bc0b837f0b6ee4e0b9f88aca8ea0f000000000017a91464a773d4d2369868256cdb81d1fa89adfc486e6e878eaa1000000000001976a9147ac6256ed9b1c229847dcaae6dd42695e6268ce988aca03213000000000017a914aaca29dda8565eeb203e01796b1ec447a275263187f86d1300000000001976a914f7aa88337662da193390d9b803de6ed8fcde34a188ac9ca21300000000001976a91441a3fddb3cf0580fdc4ea4b14c6a6c147eba56b788ac260114000000000017a914235fe1b23a2f4b7e0b5755adeb752c3244910dfc874f4614000000000017a9148beaa1d290b9365c35478412c983d7eab263950987c05c1500000000001976a9147dd24f287ba2e2a073dea4f53cc9050bcb9aaa0588acc7aa19000000000017a914d7c624671ab3c842648a96333e8e3e2afb28784c871cc51b00000000001976a914e2f3117510024cbe388d7f79fabcb9882c4484eb88ac911b1d00000000001976a9147d87b6e62e9d38a5acc6259115282b548e5bbb4188ac80841e00000000001976a9148f4c957c72beec3caf30d478e343bdf15ac9d6cf88acb5d72200000000001976a914a7bead6dcfd7a49c7a204166a6809c048471615488acf19f23000000000017a914ed61d110dbc8847a385a9cfd08969f994df3606b87f0c823000000000017a914553ec5546e240a01829a2a675680d2f27ef80c39870e172400000000001976a914a6570f223413341e04b47e25d37c6551dbd2115788ac73c42400000000001976a9140b6c529164aa80723c6161bd110c83df8f2ac4c288ace4de2400000000001976a914ea0de4671f149fe2a07e38efd58108b1bb5be22e88ac670226000000000017a914419def84439894f7765dec4874d04ee76a3d29a287ee902700000000001976a9143b020f649d3db57497a4cee4f15a9b804238452a88acb0fa2700000000001976a9149298d7b14ed3eaeee00a288d42607b5cd9beff9a88ac6d422a000000000017a914e6c2a201f5c7836d3055ee56ea40592fdb646d0b877ad02a00000000001976a9143857e125d4a29e08113cef102700634c5872462e88ac4fac31000000000017a9145ca6517ef8e582ef5354a77a29585d2e9702690e8726bd3500000000001976a914f8f8d1621b480a0d4e7d9f9cd314aa6108cc56f788ac7f873d00000000001976a91419288338a36b49192c7a0653d6c8b718df52a4c388acd95f3f00000000001976a914510a35dc572bc2dc55e558fbadc9e31953f7129b88ac80714500000000001976a914738d3394f7944ff1d76146ae856aad5e6841d3d988aca6294c00000000001976a914c1d441fe0168d67acc529a2e71bfdd19416ee62e88ac33034e00000000001976a914c9043264d845f9af61572adff7ad2b48b5cd50d088ac480d5000000000001976a91450a24587b22beb541b7e26b84add27671fd70cad88acc8545300000000001976a9148a77f414a06a9924a220216d8f18a8e25b1c032c88aca80e6900000000001976a9149a327f9ea4d51846ceb2ce78180a7a323061376188ac54c46900000000001976a914b67633d60d7cf4b658762d8bf348b7efe548cf1f88ac9db78800000000001976a914a7046f79031532214dac9e039e9b4823761738d288accde98900000000001976a914a1e65f57a630de0daac04e034f00452ec6385bfc88acc0d8a700000000001976a914a16a6ed6a85826625da49509e13f84316ab8aa5388ac23f4a800000000001976a914dad83b667ec2d442ab2d6296615ff480fbb39bfe88ac793ab600000000001976a9142a178bfe13a9509a69c7b89633adc7be84c2b3ec88acadfec7000000000017a9145022ad489df92d0a200ffadbcf3492f01efc9c0487db0b0601000000001976a91426248841f8d66c680a3a9b9816d53f3af283548e88ac802252010000000017a914640ba152487ba112aa487aea068cf1926475ee9887449ad602000000001976a9148cdad086ad0c8d1b1a265c51e49fe0d9e70e322588ac8d0c0603000000001976a914db8adda60de534cbd97cc2953ab922c521f54a7e88acf0565603000000001976a91438cb3d20424d77a0e1d21efd87579a3511fd537588ac008793030000000017a914b093a97da5eb35a965461bbb093840f72af2dc02877bd57f04000000001976a914edcefefa335223e2770d1f58b5b7e7cc0804366488ace0261706000000001976a91454e07bff6e6d36a6994db0ef54c608f8d66e327888ac8ca1f0200000000017a914fb687a86645e45cbaed8d8be9855c9d20e74c1358704004730440220688e04fbf3ce7e7b8dcefc4785f72a66d23026733a38775f223582555f6a22eb0220174d7fcedbbf61079a0089debf359cd2351bb1d4c2e17b79ea7cee6b0d22b1a70147304402205a863c92db3bb40a6346fb0f24281ad48436c5e52de630d4e349e10a70474c1f02202d9e1e175018d20df00a1ce9bc25f75d0e5865e27a4bcad07ca4e9e543bf3aa601695221027c157c4466a847cc0b01a2b51e23556d87a50ec6f89362a9a54e4e126a8614cb21034738313c53fbe6f63da94c0fdbfaef01084dd8795bc0baf8fb7709eb6241da7221032deae35acbb4eed54c70394f9d998910730f3c331d818106e105e013d264217453ae0400483045022100aaa49821cf736ea9dcbdd9f0ce22ae976381fffa3d49c246c49245ddeee1f95f022018790656b7729e6a29f712875bed98fb350e782bed9cf88ff20876231d5d5fd1014730440220726c9278fc250270a10bebbd482c42407b79270d2198a987410a966c2b451cc702207492185242b9e77004a4ce6227245752c6ef3d4766d48cc5f6c296f8595511660169522102480bfcfb664fbd8fbca04548010348d5334746b7ce87718440f9643430ee10802103a84ee88dbf345c082bb56d241bf7cb25afec6e4b953e3b4cfb2e27a4a58300de210391fa8e55da6f40a9b2a91dfe93b62d029073e0e091a672304345648dcc42f6db53ae0400473044022016426a4aebc58efdcb884ebe8a7d32fb3a1b1efdfcd2858f37b6ec88f7d4fbef02200cbb907cc6aaea6918d876d29c6e9abd6fe9c2dfb551f5095e2ad82a2660c0d1014730440220657801bc58b49b6bc039c5756f6d70ce47e62a004c8648b877ad3848a140d5fe0220581c5c48287454b89f92a37d4137ab67e2880dad51554ae4e887221a28999d5d0169522103dde1b42a6048f133fb417b592694081bbf6b51b994e36283f6f39fde09711aa321025981d800052069c31aeefac1ad8b0d0e451ce3248da5ffa41dbc0c21bcb26a1021026718ceaaadeb67721ebd532423a4f104e15c459a80d0bd363519cf3817d8be7953ae040047304402201b56739ea79145c320661a2f08811cb5d2f67b997c2078f4026b7ff0427819c9022045d69bd5a32592f1f1bf777a484bc20a1707a84b7036350128d9eee95638eb1d0147304402206c06e360814eb38391eaa14ed3dab6bb2d14987360d47a6685f4a2447518be2f02205d858969353c2c28233a4e15d3c010a7a9d0e884c17eec51ae50c8023a3beb5601695221036bb06e224c197c532e8cd67de05bcf331b00f692712625ba2de12d5afc5087c82103da6890cd195f4468dd976aa664edf90c151dc488fe4ee2ee041a98c5069c730b210265b884234c0b305f8c2af4b724adb8db1c160cd1d081d62a84afe6939e036d1453aeeeb80900"; + Transaction spent2Transaction = new Transaction(Utils.hexToBytes(spent0Hex)); + + TransactionOutput spent0Output = spent2Transaction.getOutputs().get(44); + ScriptType spent0ScriptType = ScriptType.getType(spent0Output.getScript()); + Assert.assertEquals(ScriptType.P2PKH, spent0ScriptType); + + String spent1Hex = "0100000003c07f2ee6dd4e55c6eefdc53659d1fb340beb5eb824d13bc15ba5269ade8de446000000006b483045022100d3f7526a8d1e22233c1f193b63f55406b32010aefeecdc802c07829b583d53a002205f1b666f156433baf6e976b8c43702cfe098e6d6c3c90e4bf2d24eeb1724740a012102faea485f773dbc2f57fe8cf664781a58d499c1f10ad55d370d5b08b92b8ee0c4ffffffffcac7a96d74d8a2b9177c7e0ce735f366d717e759d1f07bbd8a6db55e4b21304e000000006b483045022100d11822be0768c78cdb28ce613051facfa68c6689199505e7d0c75e95b7bd210c02202c5a610ceab38fc6816f6b792c43a1a25ae8507e80cd657dbfecfbff804a455101210287571cbb133887664c47917df7192017906916f7ce470532699c00ae4f10a178ffffffff3b16c58d5d76e119d337a56751b62b60c614ceca73d8e6403476c9e5a74497ab000000006b483045022100cb865e7b13f61f5968a734e0d8257fca72ad6f6b37c80e409e7f986a94f1269d022025e28e140e8087f1804a79b072ae18f69064f53223f2baa169685fe951f16b72012103f23d4fb4ab152b5f6b5e4a0bf79cfcac071c1f2cf07211c8cd176469b2a00628ffffffff02b3070000000000001976a914c3a1a5b559ff4db7f9c92c3d10274a3a18dcea3788ac4be28a00000000001976a914fe0c8a170be39d30f5447e57556e7836ed29e49088ac00000000"; + Transaction spentTransaction = new Transaction(Utils.hexToBytes(spent1Hex)); + + TransactionOutput spent1Output = spentTransaction.getOutputs().get(1); + ScriptType spent1ScriptType = ScriptType.getType(spent1Output.getScript()); + Assert.assertEquals(ScriptType.P2PKH, spent1ScriptType); + + String spendingHex = "010000000250d5218b2ff43b067dc11c06565d9bcf075aae26b392c4a29b673db6cffe94002c0000006b483045022100f0d5cea0874c38da6ae9e680486475cdf0267e8e316c23c390841b798f0e3cfe022043dea019c74ec6fed0126cbe18d6f7e72ad470222c5a6b3fc9369c99d9bdf8130121021fcbb2abcfd113f7e89a9d9ff4fce381dfd25e9d22d6d418839c00fa5316706fffffffff1985042874e037fbb0bb9e2ecc2dc2bab548b16d948c521e445c840a94c01f84010000006b483045022100918445e440fc81b2c42f658003b9b04a9be80200d22432e863b290b604ad1bea022032c21eaaea7a27b795dda03b2f3affb65347d929dfd32a8f12d1d459deda0cd10121021fcbb2abcfd113f7e89a9d9ff4fce381dfd25e9d22d6d418839c00fa5316706fffffffff029a030000000000001976a914bfefdae17b75487c1af143eed457a40a5bb2c44388ac385a9800000000001976a914fd76c43a0a3e4652ddb1832956959082d39aa72188ac00000000"; + Transaction spendingTransaction = new Transaction(Utils.hexToBytes(spendingHex)); + + TransactionInput input0 = spendingTransaction.getInputs().get(0); + TransactionSignature signature0 = input0.getScriptSig().getChunks().get(0).getSignature(); + ECKey pubKey0 = input0.getScriptSig().getChunks().get(1).getPubKey(); + + TransactionInput input1 = spendingTransaction.getInputs().get(1); + TransactionSignature signature1 = input1.getScriptSig().getChunks().get(0).getSignature(); + ECKey pubKey1 = input1.getScriptSig().getChunks().get(1).getPubKey(); + + Transaction transaction = new Transaction(); + spent0ScriptType.addSpendingInput(transaction, spent0Output, pubKey0, signature0); + spent1ScriptType.addSpendingInput(transaction, spent1Output, pubKey1, signature1); + + transaction.addOutput(922, Address.fromString("1JVsQ4L4HAcn58Gj5uF16dvgFNdVTarY6i")); + transaction.addOutput(9984568, Address.fromString("1Q7CEaM3CQ6ejGHgDZNbdTTAkoLcPk63nQ")); + + Assert.assertEquals(spendingTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(spendingHex, constructedHex); + } + + @Test + public void verifyReconstructedTxP2SH() throws NonStandardScriptException, IOException, InvalidAddressException { + String spent0Hex = "01000000013192d32076c7d60729f2eca05fd60d40887fe332421be0e828b188efb95b3e7c010000006a4730440220456662aff60c92d20e9e16ee01b6ea8748ef1b4720a858134d0ee038c83df8c70220464905dd25c1cd97833495a8101d7c0e5d0eb5edc9f7619fa14a2117ad92d116012103c144e864600c155326e0925844aace78fe424abbb8c00a0ce7d7e9ae13da7e95ffffffff02c8a67b490000000017a91432a81641354091c480fe29a64324ece273ed669487c2895ea8220000001976a914359fcea57940deaa62db92f65394e973ba25310288ac00000000"; + Transaction spent2Transaction = new Transaction(Utils.hexToBytes(spent0Hex)); + + TransactionOutput spent0Output = spent2Transaction.getOutputs().get(0); + ScriptType spent0ScriptType = ScriptType.getType(spent0Output.getScript()); + Assert.assertEquals(ScriptType.P2SH, spent0ScriptType); + + String spendingHex = "010000000166de02f10de096b3939f683bd1ff226ff216348df73cef884b6bbc853546600400000000fc0047304402204a66f038d6132bfebc692b5f23b8ce37165e8edd73e626f161bc69d0261aeec2022073afbf11c86538529221ea2eef95edc04159267d81a244bd15d3bd401e5ece05014730440220702644ef148ae4cd5f59679c0bd80ea44ed215dc42f75c2a8903bac126c7f36202201307e47ffd91dcc739ab670bbb1f6f22b5b468dbd058f937dd5b089d8dc4855d014c695221036973b3bedc40371520fa12bb165920fec7a4a842309f46c287d217794cde1f5b2103cc30b5a2c8b6e3ac18e6d8871c57b2e71030eee1167f1e0b0e11362d86e8f9632103da8b609d639d4dbb9490ab93e9f4de09bf969d2017dfe6925ac56abd541a0a5d53aeffffffff0414b70c000000000017a91400d0a158647216d83ad60659ac32b0a040990092878090dd48000000001976a9147c4898213f9741cb0cee70fd96844ee4eb67f19a88ace0100500000000001976a9146ec61f16216725bbc9d85509147a5fc5044d3da088ac343c89000000000017a914f41f1fcd0c35a20e23fb92c59b632bbcf7dc563c8700000000"; + Transaction spendingTransaction = new Transaction(Utils.hexToBytes(spendingHex)); + + TransactionInput input0 = spendingTransaction.getInputs().get(0); + TransactionSignature signature0 = input0.getScriptSig().getChunks().get(1).getSignature(); + TransactionSignature signature1 = input0.getScriptSig().getChunks().get(2).getSignature(); + Script redeemScript = new Script(input0.getScriptSig().getChunks().get(3).getData()); + ECKey key0 = redeemScript.getChunks().get(1).getPubKey(); + ECKey key1 = redeemScript.getChunks().get(2).getPubKey(); + ECKey key2 = redeemScript.getChunks().get(3).getPubKey(); + + Transaction transaction = new Transaction(); + spent0ScriptType.addMultisigSpendingInput(transaction, spent0Output, 2, List.of(key0, key1, key2), List.of(signature0, signature1)); + + transaction.addOutput(833300, Address.fromString("31mKrRn3xQoGppLY5dU92Dbm4kN4ddkknE")); + transaction.addOutput(1222480000, Address.fromString("1CL9kj1seXif6agPfeh6vpKkzc2Hxq1UpM")); + transaction.addOutput(332000, Address.fromString("1B6ifpYaSvBkjJTf4W1tjYgDYajFua3NU8")); + transaction.addOutput(8993844, Address.fromString("3Pwp5u7PwgrMw3gAAyLAkDKYKRrFuFkneG")); + + Assert.assertEquals(spendingTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(spendingHex, constructedHex); + } + + @Test + public void verifyReconstructedTxP2SHP2WPKH() throws NonStandardScriptException, IOException, InvalidAddressException { + String spent0Hex = "010000000001014e6120e50f876635a088f825793487fc0467722dbddef14ca31ae3a88aef1fc901000000171600148edd09d1117175a77cf9d58e1cb9c96430903979fdffffff03a00f0000000000001976a914d83f1dc836e701d17b8adf2c5e9726a4af8078b088ac4e6a00000000000017a91450dab4502ba8881856259a45611e0e9d859e1b4787026206000000000017a914c8bfc88770631bfcee814b3524abba965bf7be048702483045022100c8deb4e97dec5a0107ca124a05f6484a107c7bf9c6d9ddc50b0b2ff4a7a9f632022040390473e032b9d5f38fe777c209bac820d804800ae0494bf7fd938eb8fd8a3f012102ada4557d88d3d7130bd7b28cc06397ed91d2a1d49d4280f08c76b7b947b769b600000000"; + Transaction spent2Transaction = new Transaction(Utils.hexToBytes(spent0Hex)); + + TransactionOutput spent0Output = spent2Transaction.getOutputs().get(2); + ScriptType spent0ScriptType = ScriptType.getType(spent0Output.getScript()); + Assert.assertEquals(ScriptType.P2SH, spent0ScriptType); + + String spendingHex = "01000000000101e0a6a25fd728d9b755b4329acf66ae5f264e1bba763487b91410c36c85fb6e3802000000171600148edd09d1117175a77cf9d58e1cb9c96430903979fdffffff03a00f0000000000001976a914d83f1dc836e701d17b8adf2c5e9726a4af8078b088acf68205000000000017a914054e8b8fcd56228d18a4b3d4cc550cbad5f0c6a387dc6900000000000017a914c8bfc88770631bfcee814b3524abba965bf7be048702483045022100b6ecb3e7d4f607cc495bac79e2d5cf999cc347a51751b90e50505bf1a9153dcc02203bd844375f01183e7d08d90c6c18118dc8ac6b3686e5a22ec9a39205f632482b012102ada4557d88d3d7130bd7b28cc06397ed91d2a1d49d4280f08c76b7b947b769b600000000"; + Transaction spendingTransaction = new Transaction(Utils.hexToBytes(spendingHex)); + + TransactionInput input0 = spendingTransaction.getInputs().get(0); + Script spendingScript = input0.getScriptSig(); + TransactionWitness witness0 = input0.getWitness(); + TransactionSignature signature0 = TransactionSignature.decodeFromBitcoin(witness0.getPushes().get(0), false, false); + ECKey pubKey0 = ECKey.fromPublicOnly(witness0.getPushes().get(1)); + + Transaction transaction = new Transaction(); + transaction.setSegwitVersion(1); + TransactionInput input = ScriptType.P2SH_P2WPKH.addSpendingInput(transaction, spent0Output, pubKey0, signature0); + input.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED); + + transaction.addOutput(4000, Address.fromString("1LiQZqSwPqb615uyxDKTaN9Tg4CER98cgJ")); + transaction.addOutput(361206, Address.fromString("32B5Pv7Nvhh8iQ3Z2xK8cbKBW5f2bGMoqp")); + transaction.addOutput(27100, Address.fromString("3KzUpFMVKXiNETUy19VVW9Re5EimboDuyX")); + + Assert.assertEquals(spendingTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(spendingHex, constructedHex); + } + + @Test + public void verifyReconstructedTxP2SHP2WSH() throws NonStandardScriptException, IOException, InvalidAddressException { + String spent0Hex = "01000000000101e5e980b4cdcf1371a376d2f0a010f4e7de5d5691ec3b5ca998a2c2f6abab4a010600000023220020ef9feb56ba03b174c538bff1012cbb5e5c0d6100041a052adae09a9aa8d8b3b7ffffffff08601e8d030000000017a9141f1022d6732f39ee94ef3290e3f39a514d2e682187a07af1020000000017a914054fa6cad6486e64397190993903d3eb78e972eb87d0444c030000000017a91472c12c9b5dc4212a06c6e2be1768f6f08d2a69818720f734040000000017a914c8b38cd3262587cbf68690f53cf6f724d8f0c5e58740418f030000000017a9149e2de724b9b098ce75b92ee7331bea20b9a03416878026f8010000000017a9149d3ef266c162ddbb293d742001e0cc6390ae33a487802fa6040000000017a914603b0822693eb8934450653b2d77bb18a63958b987308ed5020000000017a914de706829f776b5b1139dc1b53360406c500538a5870400473044022070c435849751a32a816ec89d51126cc9798dbdfaafd571e6fa6aa8f8995bc30f0220337b86ea3f665b315717d920c4cf24c82b7877e843b504e15a8b00cb994a095e01473044022044b44dad3343e63abf7a23da234eeb6b828645248808d438cdb383fe5edc00e702205aa1f75369f3a87e5e0602f709076e6efa775d49c714e82bb26fa97e486cd3550169522103cdb6712a28c70ace204f817bcfa2296c81662bb7544c4e5595b80dfc58c01cad2102305242866430b4525a9ee53f2d010229347c201c3011d247ef847d9ed3a712a82102ae9b4d93708835ec02854732366ccedcf756b789082bd9802a5c66ba33fbda1453ae00000000"; + Transaction spent2Transaction = new Transaction(Utils.hexToBytes(spent0Hex)); + + TransactionOutput spent0Output = spent2Transaction.getOutputs().get(0); + ScriptType spent0ScriptType = ScriptType.getType(spent0Output.getScript()); + Assert.assertEquals(ScriptType.P2SH, spent0ScriptType); + + String spendingHex = "0100000000010168f3192941dda765f281d15f6402c16d9401ad5230eba8ed49f4caf63c00bf060000000023220020358b2d04e290b58d3481f9c4c019564c8c04046e4c4c333fc9672d838cb51856ffffffff0285a788030000000017a914ebc8d73588f7eae589d7c42b7a22af7215de9e24875b3e03000000000017a914c281f1d0c58168df144c7299f4e0c0e4281bf62c870400483045022100989e8a8dda2ba3a319bd6aa96a7cef12578000c70c67388e151da769b8be32c4022015c4de148ed129c80a538427a2c94e02743635badc3877a84f59c9142f7aa1a701473044022044cc31cff2af4d11f2fc9351dee3a06cd7407716cd5b72cf98176d336499ed5e02203e0e9c34ca94b68a64fd4f72d67d7f089f8b1fbe21fa05380bcd082736ade3c401695221025d7f3fc74bec54e0c62a50af262e465b9416047fda49f4257e0fda16250242272103b4021db115bae8fc9ce5c80de5d2c07f2ff76a41b1b8de828f9009f48547788d2103bff31daad67be914b8d65e3af52448ab897164d61c170849e28fc94e676cf1cb53ae00000000"; + Transaction spendingTransaction = new Transaction(Utils.hexToBytes(spendingHex)); + + TransactionInput input0 = spendingTransaction.getInputs().get(0); + TransactionWitness witness0 = input0.getWitness(); + TransactionSignature signature0 = TransactionSignature.decodeFromBitcoin(witness0.getPushes().get(1), false, false); + TransactionSignature signature1 = TransactionSignature.decodeFromBitcoin(witness0.getPushes().get(2), false, false); + Script witnessScript = new Script(witness0.getPushes().get(3)); + ECKey key0 = witnessScript.getChunks().get(1).getPubKey(); + ECKey key1 = witnessScript.getChunks().get(2).getPubKey(); + ECKey key2 = witnessScript.getChunks().get(3).getPubKey(); + + Transaction transaction = new Transaction(); + transaction.setSegwitVersion(1); + TransactionInput input = ScriptType.P2SH_P2WSH.addMultisigSpendingInput(transaction, spent0Output, 2, List.of(key0, key1, key2), List.of(signature0, signature1)); + + transaction.addOutput(59287429, Address.fromString("3PBjKH4FRuEKy4sD3NfL7tqfZTG5K42owu")); + transaction.addOutput(212571, Address.fromString("3KRUgU4XGuErXkjBtFhksPzTGJ4AMwF4jB")); + + Assert.assertEquals(spendingTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(spendingHex, constructedHex); + } + + @Test + public void verifyReconstructedTxP2WPKH() throws NonStandardScriptException, IOException, InvalidAddressException { + String spent0Hex = "02000000000101ba704c9e8a5981f2b2682312bf3632a3d4f99c5d6ee7dbf8f8135804459890730100000000ffffffff02be71b80c000000001600142db5decf53c76ecfc3ee3ba95909872fad4a0aefcfb81600000000001976a91429418cb34cec93a0a9f3bcd81ff1550cbdca002e88ac02473044022004c576c0f1cddc2d85a2e6ea620f051790415155d88e05d859b9f9347751963c02204a31f40a03f9da9b6a0f763b3ad852576383c74ceb4ed9549c198f37fc4b19b6012103fda72b0f00ba00675b1bc911c40e3fb708bb64548b79b6497e11b243c46f46ab00000000"; + Transaction spent2Transaction = new Transaction(Utils.hexToBytes(spent0Hex)); + + TransactionOutput spent0Output = spent2Transaction.getOutputs().get(0); + ScriptType spent0ScriptType = ScriptType.getType(spent0Output.getScript()); + Assert.assertEquals(ScriptType.P2WPKH, spent0ScriptType); + + String spendingHex = "020000000001017ea85b784c10a639517cc2d8ed4f5dcc261149aed9efc379ce66b9baed1259b70000000000ffffffff02de879c0c000000001600142db5decf53c76ecfc3ee3ba95909872fad4a0aef7b8f1b000000000017a914f85d965a9244e89bdb1925d62a9fd06b602b8962870247304402203a00ecd77a3051e924cc5a42b7b023a5abb17aea00365be49a75a5f5d9057c8702201e889ece99b24b8d44d63b4d727e40c88882d98adee937af9e4543f52de79ac8012103fda72b0f00ba00675b1bc911c40e3fb708bb64548b79b6497e11b243c46f46ab00000000"; + Transaction spendingTransaction = new Transaction(Utils.hexToBytes(spendingHex)); + + TransactionInput input0 = spendingTransaction.getInputs().get(0); + TransactionWitness witness0 = input0.getWitness(); + TransactionSignature signature0 = TransactionSignature.decodeFromBitcoin(witness0.getPushes().get(0), false, false); + ECKey key0 = ECKey.fromPublicOnly(witness0.getPushes().get(1)); + + Transaction transaction = new Transaction(); + transaction.setVersion(2); + transaction.setSegwitVersion(1); + spent0ScriptType.addSpendingInput(transaction, spent0Output, key0, signature0); + + transaction.addOutput(211584990, Address.fromString("bc1q9k6aan6ncahvlslw8w54jzv897k55zh077un6s")); + transaction.addOutput(1806203, Address.fromString("3QLFcgKFNzo262FYRFgGfrUNiUurpQbDZv")); + + Assert.assertEquals(spendingTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(spendingHex, constructedHex); + } + + @Test + public void verifyReconstructedTxP2WSH() throws NonStandardScriptException, IOException, InvalidAddressException { + String spent0Hex = "01000000000101275f4973ebc66a9d44b0196ec33820ad29ee32bef55899f2e322782740afd55d0500000000ffffffff06001bb7000000000017a914f065c662326faa3acd1226817f5f48f3d9748afd87b9ca1e000000000017a9149d19a31ef45bf46dee11974dcf6253c287a5fa7d8780f0fa020000000017a914a1475b36de36df9b8bb484fdc27ce051ef04115887ea42ee000000000017a91479e90530f4ecdae0ce59bf6dc9a7260240a656da8700c2eb0b000000001976a9145e1eb0472895a56375936e9bbc851ff0239acc9d88acab71cf1d00000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d040047304402204226aa74970340e5f2b63f8a70081624df42fac85f75217794fc047f6173fa6602200f534a6948767d79f0d9980dfe0bf8fccac17726fec7a1dfe8a4b42eda19ae080147304402201be0ad255f70d7944d0019e62bfa68da7b97b292c6e17419e3fc203a50eac81e0220604e6467257f623e3d3fcec718725c33b13d3445bc70aed719c04e429bd52efe016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000"; + Transaction spent2Transaction = new Transaction(Utils.hexToBytes(spent0Hex)); + + TransactionOutput spent0Output = spent2Transaction.getOutputs().get(5); + ScriptType spent0ScriptType = ScriptType.getType(spent0Output.getScript()); + Assert.assertEquals(ScriptType.P2WSH, spent0ScriptType); + + String spendingHex = "01000000000101b892f0a74954a730bc3e8a5a4341a144fc43dce4d9c2bc97dbdb13c501b067690500000000ffffffff032052a6000000000017a91485b5696f13edb4e9b2ac68f0de7a3e26e65c7c4e87208cd113000000001976a914c6872477e0d3f4bbd73cbaf4b9134f4204205e3888ac2bf7560900000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d040047304402205599051161390edd68d8ed01535a64f8f5d53f7418e73838c6fd6513670a095602200d7846aecf92765f4aa26da0f519f86d7b00cd29b9d43b8d73644a53975b94440147304402205cea311a37eb62219a75d4e05b513afd80a448b59caae99d4a9a3029d55dfd8d0220134656a5bcc2c5ec27c0f6e14f9e9212b0d5ca838fc7e5ac3699f8953fdafaf5016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000"; + Transaction spendingTransaction = new Transaction(Utils.hexToBytes(spendingHex)); + + TransactionInput input0 = spendingTransaction.getInputs().get(0); + TransactionWitness witness0 = input0.getWitness(); + TransactionSignature signature0 = TransactionSignature.decodeFromBitcoin(witness0.getPushes().get(1), false, false); + TransactionSignature signature1 = TransactionSignature.decodeFromBitcoin(witness0.getPushes().get(2), false, false); + Script witnessScript = new Script(witness0.getPushes().get(3)); + ECKey key0 = witnessScript.getChunks().get(1).getPubKey(); + ECKey key1 = witnessScript.getChunks().get(2).getPubKey(); + ECKey key2 = witnessScript.getChunks().get(3).getPubKey(); + + Transaction transaction = new Transaction(); + transaction.setSegwitVersion(1); + spent0ScriptType.addMultisigSpendingInput(transaction, spent0Output, 2, List.of(key0, key1, key2), List.of(signature0, signature1)); + + transaction.addOutput(10900000, Address.fromString("3Dt17mpd8FDXBjP56rCD7a4Sx7wpL91uhn")); + transaction.addOutput(332500000, Address.fromString("1K6igqzm36x8jxRTavPhgWXLVcVZVDTGc9")); + transaction.addOutput(156694315, Address.fromString("bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej")); + + Assert.assertEquals(spendingTransaction.getLength(), transaction.getLength()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + String constructedHex = Utils.bytesToHex(baos.toByteArray()); + + Assert.assertEquals(spendingHex, constructedHex); + } }