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