partial signature verification

This commit is contained in:
Craig Raw 2020-03-06 15:21:05 +02:00
parent 061440b422
commit 0340f06de4
16 changed files with 1025 additions and 46 deletions

View file

@ -11,6 +11,7 @@ import org.bouncycastle.crypto.params.KeyParameter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -175,6 +176,20 @@ public class Utils {
stream.write((int) (0xFF & (val >> 56))); 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. * Returns a copy of the given byte array in reverse order.
*/ */

View file

@ -1,18 +1,31 @@
package com.craigraw.drongo.crypto; package com.craigraw.drongo.crypto;
import com.craigraw.drongo.Utils; 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.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.ec.CustomNamedCurves; import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters; 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.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier; import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.math.ec.FixedPointUtil; import org.bouncycastle.math.ec.FixedPointUtil;
import org.bouncycastle.util.Properties;
import org.bouncycastle.util.encoders.Hex; 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.math.BigInteger;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Objects;
public class ECKey { public class ECKey {
private static final Logger log = LoggerFactory.getLogger(ECKey.class);
// The parameters of the secp256k1 curve that Bitcoin uses. // The parameters of the secp256k1 curve that Bitcoin uses.
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
@ -85,6 +98,10 @@ public class ECKey {
return pubKeyHash; 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, * Returns public key point from the given private key. To convert a byte array into a BigInteger,
* use {@code new BigInteger(1, bytes);} * use {@code new BigInteger(1, bytes);}
@ -111,4 +128,178 @@ public class ECKey {
else else
throw new IllegalArgumentException(Hex.toHexString(encoded)); 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());
}
/**
* <p>Verifies the given ECDSA signature against the message bytes using the public key bytes.</p>
*
* <p>When using native ECDSA verification, data must be 32 bytes, and no element may be
* larger than 520 bytes.</p>
*
* @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 <a
* href="https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures">BIP62</a>.
*/
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();
}
} }

View file

@ -174,6 +174,51 @@ public class Script {
return value - 1 + OP_1; 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() { public String toString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
for(ScriptChunk chunk : chunks) { for(ScriptChunk chunk : chunks) {

View file

@ -1,6 +1,5 @@
package com.craigraw.drongo.protocol; package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.Address; import com.craigraw.drongo.address.Address;
import com.craigraw.drongo.address.P2PKAddress; import com.craigraw.drongo.address.P2PKAddress;

View file

@ -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);
}
}

View file

@ -2,17 +2,26 @@ package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils; import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.Address; 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.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import static com.craigraw.drongo.Utils.uint32ToByteStreamLE; import static com.craigraw.drongo.Utils.uint32ToByteStreamLE;
import static com.craigraw.drongo.Utils.uint64ToByteStreamLE;
public class Transaction extends TransactionPart { 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 version;
private long lockTime; private long lockTime;
@ -73,7 +82,7 @@ public class Transaction extends TransactionPart {
return false; return false;
} }
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
boolean useSegwit = hasWitnesses(); boolean useSegwit = hasWitnesses();
bitcoinSerializeToStream(stream, useSegwit); bitcoinSerializeToStream(stream, useSegwit);
} }
@ -94,11 +103,11 @@ public class Transaction extends TransactionPart {
// txin_count, txins // txin_count, txins
stream.write(new VarInt(inputs.size()).encode()); stream.write(new VarInt(inputs.size()).encode());
for (TransactionInput in : inputs) for (TransactionInput in : inputs)
in.bitcoinSerialize(stream); in.bitcoinSerializeToStream(stream);
// txout_count, txouts // txout_count, txouts
stream.write(new VarInt(outputs.size()).encode()); stream.write(new VarInt(outputs.size()).encode());
for (TransactionOutput out : outputs) for (TransactionOutput out : outputs)
out.bitcoinSerialize(stream); out.bitcoinSerializeToStream(stream);
// script_witnisses // script_witnisses
if (useSegwit) { if (useSegwit) {
for (TransactionInput in : inputs) { for (TransactionInput in : inputs) {
@ -173,16 +182,221 @@ public class Transaction extends TransactionPart {
} }
} }
/** Returns an unmodifiable view of all inputs. */
public List<TransactionInput> getInputs() { public List<TransactionInput> getInputs() {
return Collections.unmodifiableList(inputs); return Collections.unmodifiableList(inputs);
} }
/** Returns an unmodifiable view of all outputs. */
public List<TransactionOutput> getOutputs() { public List<TransactionOutput> getOutputs() {
return Collections.unmodifiableList(outputs); 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<TransactionOutPoint> 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.
}
}
/**
* <p>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.</p>
*
* (See BIP143: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki)</p>
*
* @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 * 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. * 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) { public static final void main(String[] args) {
String hex = "020000000001017811567adbc80d903030ae30fc28d5cd7c395a6a74ccab96734cf5da5bd67f1a0100000000feffffff0227030000000000002200206a4c4d9be3de0e40f601d11cebd86b6d8763caa9d91f8e5e8de5f5fc8657d46da00f000000000000220020e9eaae21539323a2627701dd2c234e3499e0faf563d73fd5fcd4d263192924a604004730440220385a8b9b998abfc9319b710c44b78727b189d7029fc6e4b6c4013a3ff2976a7b02207ab7ca6aedd8d86de6d08835d8b3e4481c778043675f59f72241e7d608aa80820147304402201f62ed94f41b77ee5eb490e127ead10bd4c2144a2eacc8d61865d86fec437ed2022037488b5b96390911ded8ba086b419c335c037dc4cb004202313635741d3691b001475221022a0d4dd0d1a7182cd45de3f460737988c17653428dcb32d9c2ab35a584c716882103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a252ae8d0b0900"; String hex = "0100000002fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e0000000000ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac00000000";
byte[] transactionBytes = Utils.hexToBytes(hex); byte[] transactionBytes = Utils.hexToBytes(hex);
Transaction transaction = new Transaction(transactionBytes); 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(); Address[] addresses = transaction.getOutputs().get(0).getScript().getToAddresses();
System.out.println(addresses[0]); System.out.println(addresses[0]);
} }

View file

@ -14,7 +14,7 @@ public class TransactionInput extends TransactionPart {
private byte[] scriptBytes; private byte[] scriptBytes;
private Script script; private Script scriptSig;
private TransactionWitness witness; private TransactionWitness witness;
@ -36,12 +36,26 @@ public class TransactionInput extends TransactionPart {
return scriptBytes; return scriptBytes;
} }
public Script getScript() { public Script getScriptSig() {
if(script == null) { if(scriptSig == null) {
script = new Script(scriptBytes); 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() { public TransactionWitness getWitness() {
@ -77,7 +91,7 @@ public class TransactionInput extends TransactionPart {
} }
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
outpoint.bitcoinSerialize(stream); outpoint.bitcoinSerializeToStream(stream);
stream.write(new VarInt(scriptBytes.length).encode()); stream.write(new VarInt(scriptBytes.length).encode());
stream.write(scriptBytes); stream.write(scriptBytes);
Utils.uint32ToByteStreamLE(sequence, stream); Utils.uint32ToByteStreamLE(sequence, stream);

View file

@ -1,7 +1,12 @@
package com.craigraw.drongo.protocol; package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.Address; import com.craigraw.drongo.address.Address;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;
public class TransactionOutPoint extends TransactionPart { public class TransactionOutPoint extends TransactionPart {
static final int MESSAGE_LENGTH = 36; static final int MESSAGE_LENGTH = 36;
@ -39,4 +44,23 @@ public class TransactionOutPoint extends TransactionPart {
public void setAddresses(Address[] addresses) { public void setAddresses(Address[] addresses) {
this.addresses = 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());
}
} }

View file

@ -3,6 +3,7 @@ package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils; import com.craigraw.drongo.Utils;
import com.craigraw.drongo.address.Address; import com.craigraw.drongo.address.Address;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -16,18 +17,32 @@ public class TransactionOutput extends TransactionPart {
private Script script; private Script script;
private int scriptLen;
private Address[] addresses = new Address[0]; 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); 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 { protected void parse() throws ProtocolException {
value = readInt64(); value = readInt64();
scriptLen = (int) readVarInt(); int scriptLen = (int) readVarInt();
length = cursor - offset + scriptLen; length = cursor - offset + scriptLen;
scriptBytes = readBytes(scriptLen); scriptBytes = readBytes(scriptLen);
} }

View file

@ -103,17 +103,31 @@ public abstract class TransactionPart {
return Sha256Hash.wrapReversed(readBytes(32)); 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 { protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
log.error("Error: {} class has not implemented bitcoinSerializeToStream method. Generating message with no payload", getClass()); 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);
}
}
} }

View file

@ -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> <total len> <02> <len R> <R> <02> <len S> <S> <hashtype>
// 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]);
}
}

View file

@ -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");
}
}
}

View file

@ -3,6 +3,9 @@ package com.craigraw.drongo.psbt;
import com.craigraw.drongo.ExtendedPublicKey; import com.craigraw.drongo.ExtendedPublicKey;
import com.craigraw.drongo.KeyDerivation; import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.Utils; 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 com.craigraw.drongo.protocol.*;
import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
@ -198,14 +201,15 @@ public class PSBT {
case PSBT_GLOBAL_UNSIGNED_TX: case PSBT_GLOBAL_UNSIGNED_TX:
entry.checkOneByteKey(); entry.checkOneByteKey();
Transaction transaction = new Transaction(entry.getData()); Transaction transaction = new Transaction(entry.getData());
transaction.verify();
inputs = transaction.getInputs().size(); inputs = transaction.getInputs().size();
outputs = transaction.getOutputs().size(); outputs = transaction.getOutputs().size();
log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime()); log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime());
for(TransactionInput input: transaction.getInputs()) { 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"); 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()) { for(TransactionOutput output: transaction.getOutputs()) {
try { try {
@ -246,11 +250,64 @@ public class PSBT {
throw new IllegalStateException("Found duplicate key for PSBT input: " + Hex.toHexString(duplicate.getKey())); 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); 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<List<PSBTEntry>> outputEntryLists) { private void parseOutputEntries(List<List<PSBTEntry>> outputEntryLists) {
for(List<PSBTEntry> outputEntries : outputEntryLists) { for(List<PSBTEntry> outputEntries : outputEntryLists) {
PSBTEntry duplicate = findDuplicateKey(outputEntries); PSBTEntry duplicate = findDuplicateKey(outputEntries);
@ -300,7 +357,7 @@ public class PSBT {
public byte[] serialize() throws IOException { public byte[] serialize() throws IOException {
ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream(); ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream();
transaction.bitcoinSerialize(transactionbaos); transaction.bitcoinSerializeToStream(transactionbaos);
byte[] serialized = transactionbaos.toByteArray(); byte[] serialized = transactionbaos.toByteArray();
byte[] txLen = PSBT.writeCompactInt(serialized.length); byte[] txLen = PSBT.writeCompactInt(serialized.length);
@ -533,7 +590,7 @@ public class PSBT {
} }
public static void main(String[] args) throws Exception { 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; PSBT psbt = null;
String filename = "default.psbt"; String filename = "default.psbt";

View file

@ -32,11 +32,11 @@ public class PSBTInput {
private Transaction nonWitnessUtxo; private Transaction nonWitnessUtxo;
private TransactionOutput witnessUtxo; private TransactionOutput witnessUtxo;
private Map<LazyECPoint, byte[]> partialSignatures = new LinkedHashMap<>(); private Map<ECKey, TransactionSignature> partialSignatures = new LinkedHashMap<>();
private Transaction.SigHash sigHash; private Transaction.SigHash sigHash;
private Script redeemScript; private Script redeemScript;
private Script witnessScript; private Script witnessScript;
private Map<LazyECPoint, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); private Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
private Script finalScriptSig; private Script finalScriptSig;
private Script finalScriptWitness; private Script finalScriptWitness;
private String porCommitment; private String porCommitment;
@ -53,6 +53,7 @@ public class PSBTInput {
throw new IllegalStateException("Cannot have both witness and non-witness utxos in PSBT input"); throw new IllegalStateException("Cannot have both witness and non-witness utxos in PSBT input");
} }
Transaction nonWitnessTx = new Transaction(entry.getData()); Transaction nonWitnessTx = new Transaction(entry.getData());
nonWitnessTx.verify();
Sha256Hash inputHash = nonWitnessTx.calculateTxId(false); Sha256Hash inputHash = nonWitnessTx.calculateTxId(false);
Sha256Hash outpointHash = transaction.getInputs().get(index).getOutpoint().getHash(); Sha256Hash outpointHash = transaction.getInputs().get(index).getOutpoint().getHash();
if(!outpointHash.equals(inputHash)) { if(!outpointHash.equals(inputHash)) {
@ -62,7 +63,7 @@ public class PSBTInput {
this.nonWitnessUtxo = nonWitnessTx; this.nonWitnessUtxo = nonWitnessTx;
log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLockTime()); 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()) { 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()) { 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()); 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; break;
case PSBT_IN_PARTIAL_SIG: case PSBT_IN_PARTIAL_SIG:
entry.checkOneBytePlusPubKey(); entry.checkOneBytePlusPubKey();
LazyECPoint sigPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); ECKey sigPublicKey = ECKey.fromPublicOnly(entry.getKeyData());
//TODO: Verify signature //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())); log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData()));
break; break;
case PSBT_IN_SIGHASH_TYPE: case PSBT_IN_SIGHASH_TYPE:
@ -135,7 +137,7 @@ public class PSBTInput {
break; break;
case PSBT_IN_BIP32_DERIVATION: case PSBT_IN_BIP32_DERIVATION:
entry.checkOneBytePlusPubKey(); entry.checkOneBytePlusPubKey();
LazyECPoint derivedPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); ECKey derivedPublicKey = ECKey.fromPublicOnly(entry.getKeyData());
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
this.derivedPublicKeys.put(derivedPublicKey, keyDerivation); this.derivedPublicKeys.put(derivedPublicKey, keyDerivation);
log.debug("Found input bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey); 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; return witnessUtxo;
} }
public byte[] getPartialSignature(LazyECPoint publicKey) { public TransactionSignature getPartialSignature(ECKey publicKey) {
return partialSignatures.get(publicKey); return partialSignatures.get(publicKey);
} }
@ -208,11 +210,11 @@ public class PSBTInput {
return porCommitment; return porCommitment;
} }
public Map<LazyECPoint, byte[]> getPartialSignatures() { public Map<ECKey, TransactionSignature> getPartialSignatures() {
return partialSignatures; return partialSignatures;
} }
public Map<LazyECPoint, KeyDerivation> getDerivedPublicKeys() { public Map<ECKey, KeyDerivation> getDerivedPublicKeys() {
return derivedPublicKeys; return derivedPublicKeys;
} }

View file

@ -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));
}
}

View file

@ -140,7 +140,7 @@ public class PSBTTest {
Assert.assertEquals("f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getTxId().toString()); Assert.assertEquals("f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getTxId().toString());
Assert.assertEquals(421, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getMessageSize()); 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("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("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("b490486aec3ae671012dddb2bb08466bef37720a533a894814ff1da743aaf886", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(1).getOutpoint().getHash().toString());
Assert.assertEquals(200000000L, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(0).getValue()); 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("0020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681", psbt1.getPsbtInputs().get(0).getRedeemScript().getProgramAsHex());
Assert.assertEquals("522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae", psbt1.getPsbtInputs().get(0).getWitnessScript().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 @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="; 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); 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 @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"; 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); 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 @Test