From 0340f06de42ebc529945c1f2396545cf5b5dfcea Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 6 Mar 2020 15:21:05 +0200 Subject: [PATCH] partial signature verification --- src/main/java/com/craigraw/drongo/Utils.java | 15 ++ .../com/craigraw/drongo/crypto/ECKey.java | 191 ++++++++++++++ .../com/craigraw/drongo/protocol/Script.java | 45 ++++ .../drongo/protocol/ScriptPattern.java | 1 - .../protocol/SignatureDecodeException.java | 19 ++ .../craigraw/drongo/protocol/Transaction.java | 237 +++++++++++++++++- .../drongo/protocol/TransactionInput.java | 26 +- .../drongo/protocol/TransactionOutPoint.java | 24 ++ .../drongo/protocol/TransactionOutput.java | 25 +- .../drongo/protocol/TransactionPart.java | 34 ++- .../drongo/protocol/TransactionSignature.java | 166 ++++++++++++ .../protocol/VerificationException.java | 63 +++++ .../java/com/craigraw/drongo/psbt/PSBT.java | 67 ++++- .../com/craigraw/drongo/psbt/PSBTInput.java | 20 +- .../drongo/protocol/TransactionTest.java | 130 ++++++++++ .../com/craigraw/drongo/psbt/PSBTTest.java | 8 +- 16 files changed, 1025 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/craigraw/drongo/protocol/SignatureDecodeException.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/TransactionSignature.java create mode 100644 src/main/java/com/craigraw/drongo/protocol/VerificationException.java create mode 100644 src/test/java/com/craigraw/drongo/protocol/TransactionTest.java diff --git a/src/main/java/com/craigraw/drongo/Utils.java b/src/main/java/com/craigraw/drongo/Utils.java index be0d951..fba44b1 100644 --- a/src/main/java/com/craigraw/drongo/Utils.java +++ b/src/main/java/com/craigraw/drongo/Utils.java @@ -11,6 +11,7 @@ import org.bouncycastle.crypto.params.KeyParameter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -175,6 +176,20 @@ public class Utils { stream.write((int) (0xFF & (val >> 56))); } + /** Write 8 bytes to the output stream as unsigned 64-bit integer in little endian format. */ + public static void uint64ToByteStreamLE(BigInteger val, OutputStream stream) throws IOException { + byte[] bytes = val.toByteArray(); + if (bytes.length > 8) { + throw new RuntimeException("Input too large to encode into a uint64"); + } + bytes = reverseBytes(bytes); + stream.write(bytes); + if (bytes.length < 8) { + for (int i = 0; i < 8 - bytes.length; i++) + stream.write(0); + } + } + /** * Returns a copy of the given byte array in reverse order. */ diff --git a/src/main/java/com/craigraw/drongo/crypto/ECKey.java b/src/main/java/com/craigraw/drongo/crypto/ECKey.java index 1e94411..d2cac1a 100644 --- a/src/main/java/com/craigraw/drongo/crypto/ECKey.java +++ b/src/main/java/com/craigraw/drongo/crypto/ECKey.java @@ -1,18 +1,31 @@ package com.craigraw.drongo.crypto; import com.craigraw.drongo.Utils; +import com.craigraw.drongo.protocol.Sha256Hash; +import com.craigraw.drongo.protocol.SignatureDecodeException; +import org.bouncycastle.asn1.*; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.ec.CustomNamedCurves; import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.math.ec.FixedPointCombMultiplier; import org.bouncycastle.math.ec.FixedPointUtil; +import org.bouncycastle.util.Properties; import org.bouncycastle.util.encoders.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigInteger; import java.security.SecureRandom; +import java.util.Objects; public class ECKey { + private static final Logger log = LoggerFactory.getLogger(ECKey.class); + // The parameters of the secp256k1 curve that Bitcoin uses. private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); @@ -85,6 +98,10 @@ public class ECKey { return pubKeyHash; } + public boolean isCompressed() { + return pub.isCompressed(); + } + /** * Returns public key point from the given private key. To convert a byte array into a BigInteger, * use {@code new BigInteger(1, bytes);} @@ -111,4 +128,178 @@ public class ECKey { else throw new IllegalArgumentException(Hex.toHexString(encoded)); } + + /** + * Creates an ECKey that cannot be used for signing, only verifying signatures, from the given encoded point. + * The compression state of pub will be preserved. + */ + public static ECKey fromPublicOnly(byte[] pub) { + return new ECKey(new LazyECPoint(CURVE.getCurve(), pub)); + } + + /** + * Verifies the given R/S pair (signature) against a hash using the public key. + */ + public boolean verify(Sha256Hash sigHash, ECDSASignature signature) { + return ECKey.verify(sigHash.getBytes(), signature, getPubKey()); + } + + /** + *

Verifies the given ECDSA signature against the message bytes using the public key bytes.

+ * + *

When using native ECDSA verification, data must be 32 bytes, and no element may be + * larger than 520 bytes.

+ * + * @param data Hash of the data to verify. + * @param signature ASN.1 encoded signature. + * @param pub The public key bytes to use. + */ + public static boolean verify(byte[] data, ECDSASignature signature, byte[] pub) { + ECDSASigner signer = new ECDSASigner(); + ECPublicKeyParameters params = new ECPublicKeyParameters(CURVE.getCurve().decodePoint(pub), CURVE); + signer.init(false, params); + try { + return signer.verifySignature(data, signature.r, signature.s); + } catch (NullPointerException e) { + // Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those signatures + // are inherently invalid/attack sigs so we just fail them here rather than crash the thread. + log.error("Caught NPE inside bouncy castle", e); + return false; + } + } + + /** + * Groups the two components that make up a signature, and provides a way to encode to DER form, which is + * how ECDSA signatures are represented when embedded in other data structures in the Bitcoin protocol. The raw + * components can be useful for doing further EC maths on them. + */ + public static class ECDSASignature { + /** The two components of the signature. */ + public final BigInteger r, s; + + /** + * Constructs a signature with the given components. Does NOT automatically canonicalise the signature. + */ + public ECDSASignature(BigInteger r, BigInteger s) { + this.r = r; + this.s = s; + } + + /** + * Returns true if the S component is "low", that means it is below {@link ECKey#HALF_CURVE_ORDER}. See BIP62. + */ + public boolean isCanonical() { + return s.compareTo(HALF_CURVE_ORDER) <= 0; + } + + /** + * Will automatically adjust the S component to be less than or equal to half the curve order, if necessary. + * This is required because for every signature (r,s) the signature (r, -s (mod N)) is a valid signature of + * the same message. However, we dislike the ability to modify the bits of a Bitcoin transaction after it's + * been signed, as that violates various assumed invariants. Thus in future only one of those forms will be + * considered legal and the other will be banned. + */ + public ECDSASignature toCanonicalised() { + if (!isCanonical()) { + // The order of the curve is the number of valid points that exist on that curve. If S is in the upper + // half of the number of valid points, then bring it back to the lower half. Otherwise, imagine that + // N = 10 + // s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions. + // 10 - 8 == 2, giving us always the latter solution, which is canonical. + return new ECDSASignature(r, CURVE.getN().subtract(s)); + } else { + return this; + } + } + + /** + * DER is an international standard for serializing data structures which is widely used in cryptography. + * It's somewhat like protocol buffers but less convenient. This method returns a standard DER encoding + * of the signature, as recognized by OpenSSL and other libraries. + */ + public byte[] encodeToDER() { + try { + return derByteStream().toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + + /** + * @throws SignatureDecodeException if the signature is unparseable in some way. + */ + public static ECDSASignature decodeFromDER(byte[] bytes) throws SignatureDecodeException { + ASN1InputStream decoder = null; + try { + // BouncyCastle by default is strict about parsing ASN.1 integers. We relax this check, because some + // Bitcoin signatures would not parse. + Properties.setThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer", true); + decoder = new ASN1InputStream(bytes); + final ASN1Primitive seqObj = decoder.readObject(); + if (seqObj == null) + throw new SignatureDecodeException("Reached past end of ASN.1 stream."); + if (!(seqObj instanceof DLSequence)) + throw new SignatureDecodeException("Read unexpected class: " + seqObj.getClass().getName()); + final DLSequence seq = (DLSequence) seqObj; + ASN1Integer r, s; + try { + r = (ASN1Integer) seq.getObjectAt(0); + s = (ASN1Integer) seq.getObjectAt(1); + } catch (ClassCastException e) { + throw new SignatureDecodeException(e); + } + // OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be + // Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html + return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue()); + } catch (IOException e) { + throw new SignatureDecodeException(e); + } finally { + if (decoder != null) + try { decoder.close(); } catch (IOException x) {} + Properties.removeThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer"); + } + } + + protected ByteArrayOutputStream derByteStream() throws IOException { + // Usually 70-72 bytes. + ByteArrayOutputStream bos = new ByteArrayOutputStream(72); + DERSequenceGenerator seq = new DERSequenceGenerator(bos); + seq.addObject(new ASN1Integer(r)); + seq.addObject(new ASN1Integer(s)); + seq.close(); + return bos; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ECDSASignature other = (ECDSASignature) o; + return r.equals(other.r) && s.equals(other.s); + } + + @Override + public int hashCode() { + return Objects.hash(r, s); + } + } + + @Override + public String toString() { + return pub.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ECKey)) return false; + ECKey other = (ECKey) o; + return Objects.equals(this.pub, other.pub); + } + + @Override + public int hashCode() { + return pub.hashCode(); + } } diff --git a/src/main/java/com/craigraw/drongo/protocol/Script.java b/src/main/java/com/craigraw/drongo/protocol/Script.java index d79ab9b..696e4d0 100644 --- a/src/main/java/com/craigraw/drongo/protocol/Script.java +++ b/src/main/java/com/craigraw/drongo/protocol/Script.java @@ -174,6 +174,51 @@ public class Script { return value - 1 + OP_1; } + public static byte[] removeAllInstancesOfOp(byte[] inputScript, int opCode) { + return removeAllInstancesOf(inputScript, new byte[] {(byte)opCode}); + } + + public static byte[] removeAllInstancesOf(byte[] inputScript, byte[] chunkToRemove) { + // We usually don't end up removing anything + UnsafeByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(inputScript.length); + + int cursor = 0; + while (cursor < inputScript.length) { + boolean skip = equalsRange(inputScript, cursor, chunkToRemove); + + int opcode = inputScript[cursor++] & 0xFF; + int additionalBytes = 0; + if (opcode >= 0 && opcode < OP_PUSHDATA1) { + additionalBytes = opcode; + } else if (opcode == OP_PUSHDATA1) { + additionalBytes = (0xFF & inputScript[cursor]) + 1; + } else if (opcode == OP_PUSHDATA2) { + additionalBytes = Utils.readUint16(inputScript, cursor) + 2; + } else if (opcode == OP_PUSHDATA4) { + additionalBytes = (int) Utils.readUint32(inputScript, cursor) + 4; + } + if (!skip) { + try { + bos.write(opcode); + bos.write(Arrays.copyOfRange(inputScript, cursor, cursor + additionalBytes)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + cursor += additionalBytes; + } + return bos.toByteArray(); + } + + private static boolean equalsRange(byte[] a, int start, byte[] b) { + if (start + b.length > a.length) + return false; + for (int i = 0; i < b.length; i++) + if (a[i + start] != b[i]) + return false; + return true; + } + public String toString() { StringBuilder builder = new StringBuilder(); for(ScriptChunk chunk : chunks) { diff --git a/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java b/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java index 7d8db09..4d43868 100644 --- a/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java +++ b/src/main/java/com/craigraw/drongo/protocol/ScriptPattern.java @@ -1,6 +1,5 @@ package com.craigraw.drongo.protocol; -import com.craigraw.drongo.Utils; import com.craigraw.drongo.address.Address; import com.craigraw.drongo.address.P2PKAddress; diff --git a/src/main/java/com/craigraw/drongo/protocol/SignatureDecodeException.java b/src/main/java/com/craigraw/drongo/protocol/SignatureDecodeException.java new file mode 100644 index 0000000..bf8d51b --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/SignatureDecodeException.java @@ -0,0 +1,19 @@ +package com.craigraw.drongo.protocol; + +public class SignatureDecodeException extends RuntimeException { + public SignatureDecodeException() { + super(); + } + + public SignatureDecodeException(String message) { + super(message); + } + + public SignatureDecodeException(Throwable cause) { + super(cause); + } + + public SignatureDecodeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/craigraw/drongo/protocol/Transaction.java b/src/main/java/com/craigraw/drongo/protocol/Transaction.java index f936a4d..790f065 100644 --- a/src/main/java/com/craigraw/drongo/protocol/Transaction.java +++ b/src/main/java/com/craigraw/drongo/protocol/Transaction.java @@ -2,17 +2,26 @@ package com.craigraw.drongo.protocol; import com.craigraw.drongo.Utils; import com.craigraw.drongo.address.Address; +import com.craigraw.drongo.address.P2PKHAddress; +import com.craigraw.drongo.crypto.ECKey; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import static com.craigraw.drongo.Utils.uint32ToByteStreamLE; +import static com.craigraw.drongo.Utils.uint64ToByteStreamLE; public class Transaction extends TransactionPart { + public static final int MAX_BLOCK_SIZE = 1000 * 1000; + public static final long MAX_BITCOIN = 21 * 1000 * 1000L; + public static final long SATOSHIS_PER_BITCOIN = 100 * 1000 * 1000L; + private long version; private long lockTime; @@ -73,7 +82,7 @@ public class Transaction extends TransactionPart { return false; } - protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + public void bitcoinSerializeToStream(OutputStream stream) throws IOException { boolean useSegwit = hasWitnesses(); bitcoinSerializeToStream(stream, useSegwit); } @@ -94,11 +103,11 @@ public class Transaction extends TransactionPart { // txin_count, txins stream.write(new VarInt(inputs.size()).encode()); for (TransactionInput in : inputs) - in.bitcoinSerialize(stream); + in.bitcoinSerializeToStream(stream); // txout_count, txouts stream.write(new VarInt(outputs.size()).encode()); for (TransactionOutput out : outputs) - out.bitcoinSerialize(stream); + out.bitcoinSerializeToStream(stream); // script_witnisses if (useSegwit) { for (TransactionInput in : inputs) { @@ -173,16 +182,221 @@ public class Transaction extends TransactionPart { } } - /** Returns an unmodifiable view of all inputs. */ public List getInputs() { return Collections.unmodifiableList(inputs); } - /** Returns an unmodifiable view of all outputs. */ public List getOutputs() { return Collections.unmodifiableList(outputs); } + public void verify() throws VerificationException { + if (inputs.size() == 0 || outputs.size() == 0) + throw new VerificationException.EmptyInputsOrOutputs(); + if (this.getMessageSize() > MAX_BLOCK_SIZE) + throw new VerificationException.LargerThanMaxBlockSize(); + + HashSet outpoints = new HashSet<>(); + for (TransactionInput input : inputs) { + if (outpoints.contains(input.getOutpoint())) + throw new VerificationException.DuplicatedOutPoint(); + outpoints.add(input.getOutpoint()); + } + + long valueOut = 0L; + for (TransactionOutput output : outputs) { + long value = output.getValue(); + if (value < 0) + throw new VerificationException.NegativeValueOutput(); + try { + valueOut = Math.addExact(valueOut, value); + } catch (ArithmeticException e) { + throw new VerificationException.ExcessiveValue(); + } + double bitcoin = (double)value/SATOSHIS_PER_BITCOIN; + if (bitcoin > MAX_BITCOIN) { + throw new VerificationException.ExcessiveValue(); + } + } + + if (isCoinBase()) { + if (inputs.get(0).getScriptBytes().length < 2 || inputs.get(0).getScriptBytes().length > 100) + throw new VerificationException.CoinbaseScriptSizeOutOfRange(); + } else { + for (TransactionInput input : inputs) + if (input.isCoinBase()) + throw new VerificationException.UnexpectedCoinbaseInput(); + } + } + + public boolean isCoinBase() { + return inputs.size() == 1 && inputs.get(0).isCoinBase(); + } + + public Sha256Hash hashForSignature(int inputIndex, Script redeemScript, SigHash type, boolean anyoneCanPay) { + int sigHash = TransactionSignature.calcSigHashValue(type, anyoneCanPay); + return hashForSignature(inputIndex, redeemScript.getProgram(), (byte) sigHash); + } + + public Sha256Hash hashForSignature(int inputIndex, byte[] connectedScript, byte sigHashType) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + this.bitcoinSerializeToStream(baos); + Transaction tx = new Transaction(baos.toByteArray()); + + // Clear input scripts in preparation for signing. If we're signing a fresh + // transaction that step isn't very helpful, but it doesn't add much cost relative to the actual + // EC math so we'll do it anyway. + for (int i = 0; i < tx.inputs.size(); i++) { + TransactionInput input = tx.inputs.get(i); + input.clearScriptBytes(); + input.setWitness(null); + } + + // This step has no purpose beyond being synchronized with Bitcoin Core's bugs. OP_CODESEPARATOR + // is a legacy holdover from a previous, broken design of executing scripts that shipped in Bitcoin 0.1. + // It was seriously flawed and would have let anyone take anyone elses money. Later versions switched to + // the design we use today where scripts are executed independently but share a stack. This left the + // OP_CODESEPARATOR instruction having no purpose as it was only meant to be used internally, not actually + // ever put into scripts. Deleting OP_CODESEPARATOR is a step that should never be required but if we don't + // do it, we could split off the best chain. + connectedScript = Script.removeAllInstancesOfOp(connectedScript, ScriptOpCodes.OP_CODESEPARATOR); + + TransactionInput input = tx.inputs.get(inputIndex); + input.setScriptBytes(connectedScript); + + if ((sigHashType & 0x1f) == SigHash.NONE.value) { + // SIGHASH_NONE means no outputs are signed at all - the signature is effectively for a "blank cheque". + tx.outputs = new ArrayList<>(0); + // The signature isn't broken by new versions of the transaction issued by other parties. + for (int i = 0; i < tx.inputs.size(); i++) + if (i != inputIndex) + tx.inputs.get(i).setSequenceNumber(0); + } else if ((sigHashType & 0x1f) == SigHash.SINGLE.value) { + // SIGHASH_SINGLE means only sign the output at the same index as the input (ie, my output). + if (inputIndex >= tx.outputs.size()) { + // The input index is beyond the number of outputs, it's a buggy signature made by a broken + // Bitcoin implementation. Bitcoin Core also contains a bug in handling this case: + // any transaction output that is signed in this case will result in both the signed output + // and any future outputs to this public key being steal-able by anyone who has + // the resulting signature and the public key (both of which are part of the signed tx input). + + // Bitcoin Core's bug is that SignatureHash was supposed to return a hash and on this codepath it + // actually returns the constant "1" to indicate an error, which is never checked for. Oops. + return Sha256Hash.wrap("0100000000000000000000000000000000000000000000000000000000000000"); + } + // In SIGHASH_SINGLE the outputs after the matching input index are deleted, and the outputs before + // that position are "nulled out". Unintuitively, the value in a "null" transaction is set to -1. + tx.outputs = new ArrayList<>(tx.outputs.subList(0, inputIndex + 1)); + for (int i = 0; i < inputIndex; i++) + tx.outputs.set(i, new TransactionOutput(tx, -1L, new byte[] {})); + // The signature isn't broken by new versions of the transaction issued by other parties. + for (int i = 0; i < tx.inputs.size(); i++) + if (i != inputIndex) + tx.inputs.get(i).setSequenceNumber(0); + } + + if ((sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value) { + // SIGHASH_ANYONECANPAY means the signature in the input is not broken by changes/additions/removals + // of other inputs. For example, this is useful for building assurance contracts. + tx.inputs = new ArrayList<>(); + tx.inputs.add(input); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(tx.length); + tx.bitcoinSerializeToStream(bos, false); + // We also have to write a hash type (sigHashType is actually an unsigned char) + uint32ToByteStreamLE(0x000000ff & sigHashType, bos); + // Note that this is NOT reversed to ensure it will be signed correctly. If it were to be printed out + // however then we would expect that it is IS reversed. + Sha256Hash hash = Sha256Hash.twiceOf(bos.toByteArray()); + bos.close(); + + return hash; + } catch (IOException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + + /** + *

Calculates a signature hash, that is, a hash of a simplified form of the transaction. How exactly the transaction + * is simplified is specified by the type and anyoneCanPay parameters.

+ * + * (See BIP143: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki)

+ * + * @param inputIndex input the signature is being calculated for. Tx signatures are always relative to an input. + * @param scriptCode the script that should be in the given input during signing. + * @param prevValue the value of the coin being spent + * @param type Should be SigHash.ALL + * @param anyoneCanPay should be false. + */ + public synchronized Sha256Hash hashForWitnessSignature(int inputIndex, Script scriptCode, long prevValue, SigHash type, boolean anyoneCanPay) { + int sigHash = TransactionSignature.calcSigHashValue(type, anyoneCanPay); + return hashForWitnessSignature(inputIndex, scriptCode.getProgram(), prevValue, (byte)sigHash); + } + + public synchronized Sha256Hash hashForWitnessSignature(int inputIndex, byte[] scriptCode, long prevValue, byte sigHashType) { + ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(length == UNKNOWN_LENGTH ? 256 : length + 4); + try { + byte[] hashPrevouts = new byte[32]; + byte[] hashSequence = new byte[32]; + byte[] hashOutputs = new byte[32]; + int basicSigHashType = sigHashType & 0x1f; + boolean anyoneCanPay = (sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value; + boolean signAll = (basicSigHashType != SigHash.SINGLE.value) && (basicSigHashType != SigHash.NONE.value); + + if (!anyoneCanPay) { + ByteArrayOutputStream bosHashPrevouts = new UnsafeByteArrayOutputStream(256); + for (int i = 0; i < this.inputs.size(); ++i) { + bosHashPrevouts.write(this.inputs.get(i).getOutpoint().getHash().getReversedBytes()); + uint32ToByteStreamLE(this.inputs.get(i).getOutpoint().getIndex(), bosHashPrevouts); + } + hashPrevouts = Sha256Hash.hashTwice(bosHashPrevouts.toByteArray()); + } + + if (!anyoneCanPay && signAll) { + ByteArrayOutputStream bosSequence = new UnsafeByteArrayOutputStream(256); + for (int i = 0; i < this.inputs.size(); ++i) { + uint32ToByteStreamLE(this.inputs.get(i).getSequenceNumber(), bosSequence); + } + hashSequence = Sha256Hash.hashTwice(bosSequence.toByteArray()); + } + + if (signAll) { + ByteArrayOutputStream bosHashOutputs = new UnsafeByteArrayOutputStream(256); + for (int i = 0; i < this.outputs.size(); ++i) { + uint64ToByteStreamLE(BigInteger.valueOf(this.outputs.get(i).getValue()), bosHashOutputs); + bosHashOutputs.write(new VarInt(this.outputs.get(i).getScriptBytes().length).encode()); + bosHashOutputs.write(this.outputs.get(i).getScriptBytes()); + } + hashOutputs = Sha256Hash.hashTwice(bosHashOutputs.toByteArray()); + } else if (basicSigHashType == SigHash.SINGLE.value && inputIndex < outputs.size()) { + ByteArrayOutputStream bosHashOutputs = new UnsafeByteArrayOutputStream(256); + uint64ToByteStreamLE(BigInteger.valueOf(this.outputs.get(inputIndex).getValue()), bosHashOutputs); + bosHashOutputs.write(new VarInt(this.outputs.get(inputIndex).getScriptBytes().length).encode()); + bosHashOutputs.write(this.outputs.get(inputIndex).getScriptBytes()); + hashOutputs = Sha256Hash.hashTwice(bosHashOutputs.toByteArray()); + } + uint32ToByteStreamLE(version, bos); + bos.write(hashPrevouts); + bos.write(hashSequence); + bos.write(inputs.get(inputIndex).getOutpoint().getHash().getReversedBytes()); + uint32ToByteStreamLE(inputs.get(inputIndex).getOutpoint().getIndex(), bos); + VarInt scriptLength = new VarInt(scriptCode.length); + bos.write(scriptLength.encode()); + bos.write(scriptCode); + uint64ToByteStreamLE(BigInteger.valueOf(prevValue), bos); + uint32ToByteStreamLE(inputs.get(inputIndex).getSequenceNumber(), bos); + bos.write(hashOutputs); + uint32ToByteStreamLE(this.lockTime, bos); + uint32ToByteStreamLE(0x000000ff & sigHashType, bos); + } catch (IOException e) { + throw new RuntimeException(e); // Cannot happen. + } + + return Sha256Hash.twiceOf(bos.toByteArray()); + } + /** * These constants are a part of a scriptSig signature on the inputs. They define the details of how a * transaction can be redeemed, specifically, they control how the hash of the transaction is calculated. @@ -232,10 +446,21 @@ public class Transaction extends TransactionPart { } public static final void main(String[] args) { - String hex = "020000000001017811567adbc80d903030ae30fc28d5cd7c395a6a74ccab96734cf5da5bd67f1a0100000000feffffff0227030000000000002200206a4c4d9be3de0e40f601d11cebd86b6d8763caa9d91f8e5e8de5f5fc8657d46da00f000000000000220020e9eaae21539323a2627701dd2c234e3499e0faf563d73fd5fcd4d263192924a604004730440220385a8b9b998abfc9319b710c44b78727b189d7029fc6e4b6c4013a3ff2976a7b02207ab7ca6aedd8d86de6d08835d8b3e4481c778043675f59f72241e7d608aa80820147304402201f62ed94f41b77ee5eb490e127ead10bd4c2144a2eacc8d61865d86fec437ed2022037488b5b96390911ded8ba086b419c335c037dc4cb004202313635741d3691b001475221022a0d4dd0d1a7182cd45de3f460737988c17653428dcb32d9c2ab35a584c716882103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a252ae8d0b0900"; + String hex = "0100000002fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e0000000000ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac00000000"; byte[] transactionBytes = Utils.hexToBytes(hex); Transaction transaction = new Transaction(transactionBytes); + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880ae")); + P2PKHAddress address = new P2PKHAddress(pubKey.getPubKeyHash()); + System.out.println(address.getOutputScript().getProgram().length); + Script script = new Script(Utils.hexToBytes("21026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac")); + Sha256Hash hash = transaction.hashForWitnessSignature(1, script,4900000000L, SigHash.SINGLE, false); + System.out.println("Sighash: " + hash.toString()); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e2703"), true, true); + if(pubKey.verify(hash, signature)) { + System.out.println("Verified!"); + } + Address[] addresses = transaction.getOutputs().get(0).getScript().getToAddresses(); System.out.println(addresses[0]); } diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java b/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java index fcb41f5..26515f5 100644 --- a/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionInput.java @@ -14,7 +14,7 @@ public class TransactionInput extends TransactionPart { private byte[] scriptBytes; - private Script script; + private Script scriptSig; private TransactionWitness witness; @@ -36,12 +36,26 @@ public class TransactionInput extends TransactionPart { return scriptBytes; } - public Script getScript() { - if(script == null) { - script = new Script(scriptBytes); + public Script getScriptSig() { + if(scriptSig == null) { + scriptSig = new Script(scriptBytes); } - return script; + return scriptSig; + } + + void setScriptBytes(byte[] scriptBytes) { + super.rawtx = null; + this.scriptSig = null; + int oldLength = length; + this.scriptBytes = scriptBytes; + // 40 = previous_outpoint (36) + sequence (4) + int newLength = 40 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length); + adjustLength(newLength - oldLength); + } + + public void clearScriptBytes() { + setScriptBytes(new byte[0]); } public TransactionWitness getWitness() { @@ -77,7 +91,7 @@ public class TransactionInput extends TransactionPart { } protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { - outpoint.bitcoinSerialize(stream); + outpoint.bitcoinSerializeToStream(stream); stream.write(new VarInt(scriptBytes.length).encode()); stream.write(scriptBytes); Utils.uint32ToByteStreamLE(sequence, stream); diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java b/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java index ec3e31e..a0b301e 100644 --- a/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionOutPoint.java @@ -1,7 +1,12 @@ package com.craigraw.drongo.protocol; +import com.craigraw.drongo.Utils; import com.craigraw.drongo.address.Address; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Objects; + public class TransactionOutPoint extends TransactionPart { static final int MESSAGE_LENGTH = 36; @@ -39,4 +44,23 @@ public class TransactionOutPoint extends TransactionPart { public void setAddresses(Address[] addresses) { this.addresses = addresses; } + + @Override + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + stream.write(hash.getReversedBytes()); + Utils.uint32ToByteStreamLE(index, stream); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransactionOutPoint other = (TransactionOutPoint) o; + return getIndex() == other.getIndex() && getHash().equals(other.getHash()); + } + + @Override + public int hashCode() { + return Objects.hash(getIndex(), getHash()); + } } diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java b/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java index bd950e2..7bfd0b9 100644 --- a/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionOutput.java @@ -3,6 +3,7 @@ package com.craigraw.drongo.protocol; import com.craigraw.drongo.Utils; import com.craigraw.drongo.address.Address; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -16,18 +17,32 @@ public class TransactionOutput extends TransactionPart { private Script script; - private int scriptLen; - private Address[] addresses = new Address[0]; - public TransactionOutput(Transaction transaction, byte[] rawtx, int offset) { + public TransactionOutput(Transaction parent, byte[] rawtx, int offset) { super(rawtx, offset); - setParent(transaction); + 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); + rawtx = baos.toByteArray(); + } catch(IOException e) { + //ignore + } } protected void parse() throws ProtocolException { value = readInt64(); - scriptLen = (int) readVarInt(); + int scriptLen = (int) readVarInt(); length = cursor - offset + scriptLen; scriptBytes = readBytes(scriptLen); } diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java b/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java index 151d10e..a80e7ae 100644 --- a/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionPart.java @@ -103,17 +103,31 @@ public abstract class TransactionPart { return Sha256Hash.wrapReversed(readBytes(32)); } - public final void bitcoinSerialize(OutputStream stream) throws IOException { - // 1st check for cached bytes. - if (rawtx != null && length != UNKNOWN_LENGTH) { - stream.write(rawtx, offset, length); - return; - } - - bitcoinSerializeToStream(stream); - } - protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { log.error("Error: {} class has not implemented bitcoinSerializeToStream method. Generating message with no payload", getClass()); } + + protected void adjustLength(int adjustment) { + adjustLength(0, adjustment); + } + + protected void adjustLength(int newArraySize, int adjustment) { + if (length == UNKNOWN_LENGTH) + return; + // Our own length is now unknown if we have an unknown length adjustment. + if (adjustment == UNKNOWN_LENGTH) { + length = UNKNOWN_LENGTH; + return; + } + length += adjustment; + // Check if we will need more bytes to encode the length prefix. + if (newArraySize == 1) + length++; // The assumption here is we never call adjustLength with the same arraySize as before. + else if (newArraySize != 0) + length += VarInt.sizeOf(newArraySize) - VarInt.sizeOf(newArraySize - 1); + + if (parent != null) { + parent.adjustLength(newArraySize, adjustment); + } + } } diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionSignature.java b/src/main/java/com/craigraw/drongo/protocol/TransactionSignature.java new file mode 100644 index 0000000..e42cf61 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionSignature.java @@ -0,0 +1,166 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.crypto.ECKey; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; + +public class TransactionSignature extends ECKey.ECDSASignature { + /** + * A byte that controls which parts of a transaction are signed. This is exposed because signatures + * parsed off the wire may have sighash flags that aren't "normal" serializations of the enum values. + * Because Bitcoin Core works via bit testing, we must not lose the exact value when round-tripping + * otherwise we'll fail to verify signature hashes. + */ + public final int sighashFlags; + + /** Constructs a signature with the given components and SIGHASH_ALL. */ + public TransactionSignature(BigInteger r, BigInteger s) { + this(r, s, Transaction.SigHash.ALL.value); + } + + /** Constructs a signature with the given components and raw sighash flag bytes (needed for rule compatibility). */ + public TransactionSignature(BigInteger r, BigInteger s, int sighashFlags) { + super(r, s); + this.sighashFlags = sighashFlags; + } + + /** Constructs a transaction signature based on the ECDSA signature. */ + public TransactionSignature(ECKey.ECDSASignature signature, Transaction.SigHash mode, boolean anyoneCanPay) { + super(signature.r, signature.s); + sighashFlags = calcSigHashValue(mode, anyoneCanPay); + } + + /** + * Returns a dummy invalid signature whose R/S values are set such that they will take up the same number of + * encoded bytes as a real signature. This can be useful when you want to fill out a transaction to be of the + * right size (e.g. for fee calculations) but don't have the requisite signing key yet and will fill out the + * real signature later. + */ + public static TransactionSignature dummy() { + BigInteger val = ECKey.HALF_CURVE_ORDER; + return new TransactionSignature(val, val); + } + + /** Calculates the byte used in the protocol to represent the combination of mode and anyoneCanPay. */ + public static int calcSigHashValue(Transaction.SigHash mode, boolean anyoneCanPay) { + if(Transaction.SigHash.ALL != mode && Transaction.SigHash.NONE != mode && Transaction.SigHash.SINGLE != mode) { // enforce compatibility since this code was made before the SigHash enum was updated + throw new IllegalArgumentException("Sighash mode must be one of ALL, NONE or SINGLE"); + } + int sighashFlags = mode.value; + if (anyoneCanPay) + sighashFlags |= Transaction.SigHash.ANYONECANPAY.value; + return sighashFlags; + } + + /** + * Returns true if the given signature is has canonical encoding, and will thus be accepted as standard by + * Bitcoin Core. DER and the SIGHASH encoding allow for quite some flexibility in how the same structures + * are encoded, and this can open up novel attacks in which a man in the middle takes a transaction and then + * changes its signature such that the transaction hash is different but it's still valid. This can confuse wallets + * and generally violates people's mental model of how Bitcoin should work, thus, non-canonical signatures are now + * not relayed by default. + */ + public static boolean isEncodingCanonical(byte[] signature) { + // See Bitcoin Core's IsCanonicalSignature, https://bitcointalk.org/index.php?topic=8392.msg127623#msg127623 + // A canonical signature exists of: <30> <02> <02> + // Where R and S are not negative (their first byte has its highest bit not set), and not + // excessively padded (do not start with a 0 byte, unless an otherwise negative number follows, + // in which case a single 0 byte is necessary and even required). + + // Empty signatures, while not strictly DER encoded, are allowed. + if (signature.length == 0) + return true; + + if (signature.length < 9 || signature.length > 73) + return false; + + int hashType = (signature[signature.length-1] & 0xff) & ~Transaction.SigHash.ANYONECANPAY.value; // mask the byte to prevent sign-extension hurting us + if (hashType < Transaction.SigHash.ALL.value || hashType > Transaction.SigHash.SINGLE.value) + return false; + + // "wrong type" "wrong length marker" + if ((signature[0] & 0xff) != 0x30 || (signature[1] & 0xff) != signature.length-3) + return false; + + int lenR = signature[3] & 0xff; + if (5 + lenR >= signature.length || lenR == 0) + return false; + int lenS = signature[5+lenR] & 0xff; + if (lenR + lenS + 7 != signature.length || lenS == 0) + return false; + + // R value type mismatch R value negative + if (signature[4-2] != 0x02 || (signature[4] & 0x80) == 0x80) + return false; + if (lenR > 1 && signature[4] == 0x00 && (signature[4+1] & 0x80) != 0x80) + return false; // R value excessively padded + + // S value type mismatch S value negative + if (signature[6 + lenR - 2] != 0x02 || (signature[6 + lenR] & 0x80) == 0x80) + return false; + if (lenS > 1 && signature[6 + lenR] == 0x00 && (signature[6 + lenR + 1] & 0x80) != 0x80) + return false; // S value excessively padded + + return true; + } + + public boolean anyoneCanPay() { + return (sighashFlags & Transaction.SigHash.ANYONECANPAY.value) != 0; + } + + public Transaction.SigHash sigHashMode() { + final int mode = sighashFlags & 0x1f; + if (mode == Transaction.SigHash.NONE.value) + return Transaction.SigHash.NONE; + else if (mode == Transaction.SigHash.SINGLE.value) + return Transaction.SigHash.SINGLE; + else + return Transaction.SigHash.ALL; + } + + /** + * What we get back from the signer are the two components of a signature, r and s. To get a flat byte stream + * of the type used by Bitcoin we have to encode them using DER encoding, which is just a way to pack the two + * components into a structure, and then we append a byte to the end for the sighash flags. + */ + public byte[] encodeToBitcoin() { + try { + ByteArrayOutputStream bos = derByteStream(); + bos.write(sighashFlags); + return bos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); // Cannot happen. + } + } + + @Override + public ECKey.ECDSASignature toCanonicalised() { + return new TransactionSignature(super.toCanonicalised(), sigHashMode(), anyoneCanPay()); + } + + /** + * Returns a decoded signature. + * + * @param requireCanonicalEncoding if the encoding of the signature must + * be canonical. + * @param requireCanonicalSValue if the S-value must be canonical (below half + * the order of the curve). + * @throws SignatureDecodeException if the signature is unparseable in some way. + * @throws VerificationException if the signature is invalid. + */ + public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding, + boolean requireCanonicalSValue) throws SignatureDecodeException, VerificationException { + // Bitcoin encoding is DER signature + sighash byte. + if (requireCanonicalEncoding && !isEncodingCanonical(bytes)) + throw new VerificationException.NoncanonicalSignature(); + ECKey.ECDSASignature sig = ECKey.ECDSASignature.decodeFromDER(bytes); + if (requireCanonicalSValue && !sig.isCanonical()) + throw new VerificationException("S-value is not canonical."); + + // In Bitcoin, any value of the final byte is valid, but not necessarily canonical. See javadocs for + // isEncodingCanonical to learn more about this. So we must store the exact byte found. + return new TransactionSignature(sig.r, sig.s, bytes[bytes.length - 1]); + } +} \ No newline at end of file diff --git a/src/main/java/com/craigraw/drongo/protocol/VerificationException.java b/src/main/java/com/craigraw/drongo/protocol/VerificationException.java new file mode 100644 index 0000000..1874a16 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/protocol/VerificationException.java @@ -0,0 +1,63 @@ +package com.craigraw.drongo.protocol; + +public class VerificationException extends RuntimeException { + public VerificationException(String msg) { + super(msg); + } + + public VerificationException(Exception e) { + super(e); + } + + public VerificationException(String msg, Throwable t) { + super(msg, t); + } + + public static class EmptyInputsOrOutputs extends VerificationException { + public EmptyInputsOrOutputs() { + super("Transaction had no inputs or no outputs."); + } + } + + public static class LargerThanMaxBlockSize extends VerificationException { + public LargerThanMaxBlockSize() { + super("Transaction larger than MAX_BLOCK_SIZE"); + } + } + + public static class DuplicatedOutPoint extends VerificationException { + public DuplicatedOutPoint() { + super("Duplicated outpoint"); + } + } + + public static class NegativeValueOutput extends VerificationException { + public NegativeValueOutput() { + super("Transaction output negative"); + } + } + + public static class ExcessiveValue extends VerificationException { + public ExcessiveValue() { + super("Total transaction output value greater than possible"); + } + } + + public static class CoinbaseScriptSizeOutOfRange extends VerificationException { + public CoinbaseScriptSizeOutOfRange() { + super("Coinbase script size out of range"); + } + } + + public static class UnexpectedCoinbaseInput extends VerificationException { + public UnexpectedCoinbaseInput() { + super("Coinbase input as input in non-coinbase transaction"); + } + } + + public static class NoncanonicalSignature extends VerificationException { + public NoncanonicalSignature() { + super("Signature encoding is not canonical"); + } + } +} diff --git a/src/main/java/com/craigraw/drongo/psbt/PSBT.java b/src/main/java/com/craigraw/drongo/psbt/PSBT.java index a6e7b39..8ac4ba4 100644 --- a/src/main/java/com/craigraw/drongo/psbt/PSBT.java +++ b/src/main/java/com/craigraw/drongo/psbt/PSBT.java @@ -3,6 +3,9 @@ package com.craigraw.drongo.psbt; import com.craigraw.drongo.ExtendedPublicKey; import com.craigraw.drongo.KeyDerivation; import com.craigraw.drongo.Utils; +import com.craigraw.drongo.address.Address; +import com.craigraw.drongo.address.P2PKHAddress; +import com.craigraw.drongo.crypto.ECKey; import com.craigraw.drongo.protocol.*; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; @@ -198,14 +201,15 @@ public class PSBT { case PSBT_GLOBAL_UNSIGNED_TX: entry.checkOneByteKey(); Transaction transaction = new Transaction(entry.getData()); + transaction.verify(); inputs = transaction.getInputs().size(); outputs = transaction.getOutputs().size(); log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime()); for(TransactionInput input: transaction.getInputs()) { - if(input.getScript().getProgram().length != 0) { + if(input.getScriptSig().getProgram().length != 0) { throw new IllegalStateException("Unsigned tx input does not have empty scriptSig"); } - log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript()); + log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScriptSig()); } for(TransactionOutput output: transaction.getOutputs()) { try { @@ -246,11 +250,64 @@ public class PSBT { throw new IllegalStateException("Found duplicate key for PSBT input: " + Hex.toHexString(duplicate.getKey())); } - PSBTInput input = new PSBTInput(inputEntries, transaction, this.psbtInputs.size()); + int inputIndex = this.psbtInputs.size(); + PSBTInput input = new PSBTInput(inputEntries, transaction, inputIndex); + + boolean verified = verifySignatures(input, inputIndex); + if(verified) { + log.debug("Verified signatures on input #" + inputIndex); + } + this.psbtInputs.add(input); } } + private boolean verifySignatures(PSBTInput input, int index) { + if(input.getSigHash() != null && (input.getNonWitnessUtxo() != null || input.getWitnessUtxo() != null)) { + int vout = (int)transaction.getInputs().get(index).getOutpoint().getIndex(); + Script inputScript = input.getNonWitnessUtxo() != null ? input.getNonWitnessUtxo().getOutputs().get(vout).getScript() : input.getWitnessUtxo().getScript(); + + Script connectedScript = inputScript; + if(ScriptPattern.isP2SH(connectedScript)) { + if(input.getRedeemScript() == null) { + return false; + } else { + connectedScript = input.getRedeemScript(); + } + } + + if(ScriptPattern.isP2WPKH(connectedScript)) { + Address address = new P2PKHAddress(connectedScript.getPubKeyHash()); + connectedScript = address.getOutputScript(); + } else if(ScriptPattern.isP2WSH(connectedScript)) { + if(input.getWitnessScript() == null) { + return false; + } else { + connectedScript = input.getWitnessScript(); + } + } + + Sha256Hash hash = null; + if(input.getNonWitnessUtxo() != null) { + hash = transaction.hashForSignature(index, connectedScript, input.getSigHash(), false); + } else { + long prevValue = input.getWitnessUtxo().getValue(); + hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, input.getSigHash(), false); + } + + for(ECKey sigPublicKey : input.getPartialSignatures().keySet()) { + TransactionSignature signature = input.getPartialSignature(sigPublicKey); + if(!sigPublicKey.verify(hash, signature)) { + throw new IllegalStateException("Partial signature does not verify against provided public key"); + } + } + + return true; + } + + return false; + } + private void parseOutputEntries(List> outputEntryLists) { for(List outputEntries : outputEntryLists) { PSBTEntry duplicate = findDuplicateKey(outputEntries); @@ -300,7 +357,7 @@ public class PSBT { public byte[] serialize() throws IOException { ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream(); - transaction.bitcoinSerialize(transactionbaos); + transaction.bitcoinSerializeToStream(transactionbaos); byte[] serialized = transactionbaos.toByteArray(); byte[] txLen = PSBT.writeCompactInt(serialized.length); @@ -533,7 +590,7 @@ public class PSBT { } public static void main(String[] args) throws Exception { - String psbtBase64 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA=="; + String psbtBase64 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA="; PSBT psbt = null; String filename = "default.psbt"; diff --git a/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java b/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java index 1cd43d5..c790919 100644 --- a/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java @@ -32,11 +32,11 @@ public class PSBTInput { private Transaction nonWitnessUtxo; private TransactionOutput witnessUtxo; - private Map partialSignatures = new LinkedHashMap<>(); + private Map partialSignatures = new LinkedHashMap<>(); private Transaction.SigHash sigHash; private Script redeemScript; private Script witnessScript; - private Map derivedPublicKeys = new LinkedHashMap<>(); + private Map derivedPublicKeys = new LinkedHashMap<>(); private Script finalScriptSig; private Script finalScriptWitness; private String porCommitment; @@ -53,6 +53,7 @@ public class PSBTInput { throw new IllegalStateException("Cannot have both witness and non-witness utxos in PSBT input"); } Transaction nonWitnessTx = new Transaction(entry.getData()); + nonWitnessTx.verify(); Sha256Hash inputHash = nonWitnessTx.calculateTxId(false); Sha256Hash outpointHash = transaction.getInputs().get(index).getOutpoint().getHash(); if(!outpointHash.equals(inputHash)) { @@ -62,7 +63,7 @@ public class PSBTInput { this.nonWitnessUtxo = nonWitnessTx; log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLockTime()); for(TransactionInput input: nonWitnessTx.getInputs()) { - log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript()); + log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScriptSig()); } for(TransactionOutput output: nonWitnessTx.getOutputs()) { log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript()); @@ -82,9 +83,10 @@ public class PSBTInput { break; case PSBT_IN_PARTIAL_SIG: entry.checkOneBytePlusPubKey(); - LazyECPoint sigPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); + ECKey sigPublicKey = ECKey.fromPublicOnly(entry.getKeyData()); //TODO: Verify signature - this.partialSignatures.put(sigPublicKey, entry.getData()); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(entry.getData(), true, false); + this.partialSignatures.put(sigPublicKey, signature); log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData())); break; case PSBT_IN_SIGHASH_TYPE: @@ -135,7 +137,7 @@ public class PSBTInput { break; case PSBT_IN_BIP32_DERIVATION: entry.checkOneBytePlusPubKey(); - LazyECPoint derivedPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); + ECKey derivedPublicKey = ECKey.fromPublicOnly(entry.getKeyData()); KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); this.derivedPublicKeys.put(derivedPublicKey, keyDerivation); log.debug("Found input bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey); @@ -176,7 +178,7 @@ public class PSBTInput { return witnessUtxo; } - public byte[] getPartialSignature(LazyECPoint publicKey) { + public TransactionSignature getPartialSignature(ECKey publicKey) { return partialSignatures.get(publicKey); } @@ -208,11 +210,11 @@ public class PSBTInput { return porCommitment; } - public Map getPartialSignatures() { + public Map getPartialSignatures() { return partialSignatures; } - public Map getDerivedPublicKeys() { + public Map getDerivedPublicKeys() { return derivedPublicKeys; } diff --git a/src/test/java/com/craigraw/drongo/protocol/TransactionTest.java b/src/test/java/com/craigraw/drongo/protocol/TransactionTest.java new file mode 100644 index 0000000..4a87a97 --- /dev/null +++ b/src/test/java/com/craigraw/drongo/protocol/TransactionTest.java @@ -0,0 +1,130 @@ +package com.craigraw.drongo.protocol; + +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.address.Address; +import com.craigraw.drongo.address.P2PKHAddress; +import com.craigraw.drongo.crypto.ECKey; +import org.junit.Assert; +import org.junit.Test; + +public class TransactionTest { + @Test + public void verifyP2WPKH() { + String hex = "0100000002fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f0000000000eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac11000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357")); + P2PKHAddress address = new P2PKHAddress(pubKey.getPubKeyHash()); + Sha256Hash hash = transaction.hashForWitnessSignature(1, address.getOutputScript(),600000000L, Transaction.SigHash.ALL, false); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee"), false, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2SHP2WPKH() { + String hex = "0100000001db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a54770100000000feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac92040000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("03ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a26873")); + Address address = new P2PKHAddress(pubKey.getPubKeyHash()); + Sha256Hash hash = transaction.hashForWitnessSignature(0, address.getOutputScript(),1000000000L, Transaction.SigHash.ALL, false); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb01"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2WSHSigHashSingle() { + String hex = "0100000002fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e0000000000ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880ae")); + Script script = new Script(Utils.hexToBytes("21026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac")); + Sha256Hash hash = transaction.hashForWitnessSignature(1, script,4900000000L, Transaction.SigHash.SINGLE, false); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e2703"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2WSHSigHashSingleAnyoneCanPay() { + String hex = "0100000002e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("0392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98")); + Script script = new Script(Utils.hexToBytes("68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac")); + Sha256Hash hash = transaction.hashForWitnessSignature(1, script,16777215L, Transaction.SigHash.SINGLE, true); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("30440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2SHP2WSHSigHashAll() { + String hex = "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("0307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba3")); + Script script = new Script(Utils.hexToBytes("56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae")); + Sha256Hash hash = transaction.hashForWitnessSignature(0, script,987654321L, Transaction.SigHash.ALL, false); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2SHP2WSHSigHashNone() { + String hex = "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("03b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b")); + Script script = new Script(Utils.hexToBytes("56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae")); + Sha256Hash hash = transaction.hashForWitnessSignature(0, script,987654321L, Transaction.SigHash.NONE, false); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2SHP2WSHSigHashSingle() { + String hex = "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a")); + Script script = new Script(Utils.hexToBytes("56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae")); + Sha256Hash hash = transaction.hashForWitnessSignature(0, script,987654321L, Transaction.SigHash.SINGLE, false); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2SHP2WSHSigHashAllAnyoneCanPay() { + String hex = "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f4")); + Script script = new Script(Utils.hexToBytes("56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae")); + Sha256Hash hash = transaction.hashForWitnessSignature(0, script,987654321L, Transaction.SigHash.ALL, true); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2SHP2WSHSigHashNoneAnyoneCanPay() { + String hex = "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("03a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac16")); + Script script = new Script(Utils.hexToBytes("56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae")); + Sha256Hash hash = transaction.hashForWitnessSignature(0, script,987654321L, Transaction.SigHash.NONE, true); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a0882"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } + + @Test + public void verifyP2SHP2WSHSigHashSingleAnyoneCanPay() { + String hex = "010000000136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000000ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac00000000"; + Transaction transaction = new Transaction(Utils.hexToBytes(hex)); + + ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("02d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b")); + Script script = new Script(Utils.hexToBytes("56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae")); + Sha256Hash hash = transaction.hashForWitnessSignature(0, script,987654321L, Transaction.SigHash.SINGLE, true); + TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("30440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783"), true, true); + Assert.assertTrue(pubKey.verify(hash, signature)); + } +} diff --git a/src/test/java/com/craigraw/drongo/psbt/PSBTTest.java b/src/test/java/com/craigraw/drongo/psbt/PSBTTest.java index ba75574..20aa14d 100644 --- a/src/test/java/com/craigraw/drongo/psbt/PSBTTest.java +++ b/src/test/java/com/craigraw/drongo/psbt/PSBTTest.java @@ -140,7 +140,7 @@ public class PSBTTest { Assert.assertEquals("f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getTxId().toString()); Assert.assertEquals(421, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getMessageSize()); Assert.assertEquals("e567952fb6cc33857f392efa3a46c995a28f69cca4bb1b37e0204dab1ec7a389", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getOutpoint().getHash().toString()); - Assert.assertEquals("160014be18d152a9b012039daf3da7de4f53349eecb985", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getScript().getProgramAsHex()); + Assert.assertEquals("160014be18d152a9b012039daf3da7de4f53349eecb985", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getScriptSig().getProgramAsHex()); Assert.assertEquals("304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c01 03d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f2105", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getWitness().toString()); Assert.assertEquals("b490486aec3ae671012dddb2bb08466bef37720a533a894814ff1da743aaf886", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(1).getOutpoint().getHash().toString()); Assert.assertEquals(200000000L, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(0).getValue()); @@ -188,7 +188,7 @@ public class PSBTTest { Assert.assertEquals("0020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681", psbt1.getPsbtInputs().get(0).getRedeemScript().getProgramAsHex()); Assert.assertEquals("522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae", psbt1.getPsbtInputs().get(0).getWitnessScript().getProgramAsHex()); - Assert.assertEquals("304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01", Hex.toHexString(psbt1.getPsbtInputs().get(0).getPartialSignatures().values().iterator().next())); + Assert.assertEquals("304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01", Hex.toHexString(psbt1.getPsbtInputs().get(0).getPartialSignatures().values().iterator().next().encodeToBitcoin())); } @Test @@ -265,7 +265,7 @@ public class PSBTTest { String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA="; PSBT psbt1 = PSBT.fromString(psbt); - Assert.assertEquals("3044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d201", Hex.toHexString(psbt1.getPsbtInputs().get(1).getPartialSignatures().values().iterator().next())); + Assert.assertEquals("3044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d201", Hex.toHexString(psbt1.getPsbtInputs().get(1).getPartialSignatures().values().iterator().next().encodeToBitcoin())); } @Test @@ -273,7 +273,7 @@ public class PSBTTest { String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA"; PSBT psbt1 = PSBT.fromString(psbt); - Assert.assertEquals("3044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01", Hex.toHexString(psbt1.getPsbtInputs().get(1).getPartialSignatures().values().iterator().next())); + Assert.assertEquals("3044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01", Hex.toHexString(psbt1.getPsbtInputs().get(1).getPartialSignatures().values().iterator().next().encodeToBitcoin())); } @Test