mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 10:06:45 +00:00
partial signature verification
This commit is contained in:
parent
061440b422
commit
0340f06de4
16 changed files with 1025 additions and 46 deletions
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
130
src/test/java/com/craigraw/drongo/protocol/TransactionTest.java
Normal file
130
src/test/java/com/craigraw/drongo/protocol/TransactionTest.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue