psbt initial draft

This commit is contained in:
Craig Raw 2020-02-21 16:27:08 +02:00
parent 89e7892d7c
commit ce2b0648a6
17 changed files with 1378 additions and 41 deletions

View file

@ -4,10 +4,10 @@ import com.craigraw.drongo.crypto.*;
import com.craigraw.drongo.protocol.Base58;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
import static com.craigraw.drongo.KeyDerivation.parsePath;
import static com.craigraw.drongo.KeyDerivation.writePath;
public class ExtendedPublicKey {
private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
@ -15,17 +15,17 @@ public class ExtendedPublicKey {
private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub"
private static final int bip32HeaderP2WHSHPub = 0x2AA7ED3; // The 4 byte header that serializes in base58 to "Zpub"
private KeyDerivation keyDerivation;
private byte[] parentFingerprint;
private String keyDerivationPath;
private DeterministicKey pubKey;
private String childDerivationPath;
private ChildNumber pubKeyChildNumber;
private DeterministicHierarchy hierarchy;
public ExtendedPublicKey(byte[] parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
public ExtendedPublicKey(String masterFingerprint, byte[] parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
this.keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
this.parentFingerprint = parentFingerprint;
this.keyDerivationPath = keyDerivationPath;
this.pubKey = pubKey;
this.childDerivationPath = childDerivationPath;
this.pubKeyChildNumber = pubKeyChildNumber;
@ -33,6 +33,10 @@ public class ExtendedPublicKey {
this.hierarchy = new DeterministicHierarchy(pubKey);
}
public String getMasterFingerprint() {
return keyDerivation.getMasterFingerprint();
}
public byte[] getParentFingerprint() {
return parentFingerprint;
}
@ -41,8 +45,12 @@ public class ExtendedPublicKey {
return pubKey.getFingerprint();
}
public String getKeyDerivationPath() {
return keyDerivation.getDerivationPath();
}
public List<ChildNumber> getKeyDerivation() {
return parsePath(keyDerivationPath);
return keyDerivation.getParsedDerivationPath();
}
public DeterministicKey getPubKey() {
@ -101,27 +109,6 @@ public class ExtendedPublicKey {
return hierarchy.get(path);
}
public static List<ChildNumber> parsePath(String path) {
return parsePath(path, 0);
}
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
String[] parsedNodes = path.replace("M", "").split("/");
List<ChildNumber> nodes = new ArrayList<>();
for (String n : parsedNodes) {
n = n.replaceAll(" ", "");
if (n.length() == 0) continue;
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
if (isHard) n = n.substring(0, n.length() - 1);
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
int nodeNumber = Integer.parseInt(n);
nodes.add(new ChildNumber(nodeNumber, isHard));
}
return nodes;
}
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(getExtendedPublicKey());
@ -151,7 +138,7 @@ public class ExtendedPublicKey {
return buffer.array();
}
static ExtendedPublicKey fromDescriptor(String keyDerivationPath, String extPubKey, String childDerivationPath) {
public static ExtendedPublicKey fromDescriptor(String masterFingerprint, String keyDerivationPath, String extPubKey, String childDerivationPath) {
byte[] serializedKey = Base58.decodeChecked(extPubKey);
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
int header = buffer.getInt();
@ -184,7 +171,24 @@ public class ExtendedPublicKey {
throw new IllegalArgumentException("Found unexpected data in key");
}
if(childDerivationPath == null) {
childDerivationPath = writePath(Collections.singletonList(childNumber));
}
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
return new ExtendedPublicKey(parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
return new ExtendedPublicKey(masterFingerprint, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExtendedPublicKey that = (ExtendedPublicKey) o;
return that.toString().equals(this.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
}

View file

@ -0,0 +1,76 @@
package com.craigraw.drongo;
import com.craigraw.drongo.crypto.ChildNumber;
import java.util.ArrayList;
import java.util.List;
public class KeyDerivation {
private String masterFingerprint;
private String derivationPath;
public KeyDerivation(String masterFingerprint, String derivationPath) {
this.masterFingerprint = masterFingerprint;
this.derivationPath = derivationPath;
}
public String getMasterFingerprint() {
return masterFingerprint;
}
public String getDerivationPath() {
return derivationPath;
}
public List<ChildNumber> getParsedDerivationPath() {
return parsePath(derivationPath);
}
public static List<ChildNumber> parsePath(String path) {
return parsePath(path, 0);
}
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
String[] parsedNodes = path.replace("M", "").replace("m", "").split("/");
List<ChildNumber> nodes = new ArrayList<>();
for (String n : parsedNodes) {
n = n.replaceAll(" ", "");
if (n.length() == 0) continue;
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
if (isHard) n = n.substring(0, n.length() - 1);
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
int nodeNumber = Integer.parseInt(n);
nodes.add(new ChildNumber(nodeNumber, isHard));
}
return nodes;
}
public static String writePath(List<ChildNumber> pathList) {
String path = "m";
for (ChildNumber child: pathList) {
path += "/";
path += child.toString();
}
return path;
}
public String toString() {
return masterFingerprint + (derivationPath != null ? derivationPath.replace("m", "") : "");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KeyDerivation that = (KeyDerivation) o;
return that.toString().equals(this.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
}

View file

@ -17,6 +17,7 @@ import java.util.regex.Pattern;
public class OutputDescriptor {
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]+)(/[/\\d*']+)?");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([a-f0-9]+)([/\\d']+)\\]");
private String script;
private int multisigThreshold;
@ -196,12 +197,18 @@ public class OutputDescriptor {
List<ExtendedPublicKey> keys = new ArrayList<>();
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
while(matcher.find()) {
String keyDerivationPath ="";
String masterFingerprint = null;
String keyDerivationPath = null;
String extPubKey = null;
String childDerivationPath = "/0/*";
if(matcher.group(1) != null) {
keyDerivationPath = matcher.group(1);
String keyOrigin = matcher.group(1);
Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(keyOrigin);
if(keyOriginMatcher.matches()) {
masterFingerprint = keyOriginMatcher.group(1);
keyDerivationPath = "m" + keyOriginMatcher.group(2);
}
}
extPubKey = matcher.group(2);
@ -209,7 +216,7 @@ public class OutputDescriptor {
childDerivationPath = matcher.group(3);
}
ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(keyDerivationPath, extPubKey, childDerivationPath);
ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(masterFingerprint, keyDerivationPath, extPubKey, childDerivationPath);
keys.add(extendedPublicKey);
}

View file

@ -8,7 +8,6 @@ import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -21,6 +20,17 @@ public class Utils {
public static final int MAX_INITIAL_ARRAY_LENGTH = 20;
private final static char[] hexArray = "0123456789abcdef".toCharArray();
public static final String HEX_REGEX = "^[0-9A-Fa-f]+$";
public static final String BASE64_REGEX = "^[0-9A-Za-z\\\\+=/]+$";
public static boolean isHex(String s) {
return s.matches(HEX_REGEX);
}
public static boolean isBase64(String s) {
return s.matches(BASE64_REGEX);
}
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {

View file

@ -46,7 +46,7 @@ public class ChildNumber {
public int i() { return i; }
public String toString() {
return String.format(Locale.US, "%d%s", num(), isHardened() ? "H" : "");
return String.format(Locale.US, "%d%s", num(), isHardened() ? "'" : "");
}
public boolean equals(Object o) {

View file

@ -7,6 +7,7 @@ import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.math.ec.FixedPointUtil;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.security.SecureRandom;
@ -52,7 +53,7 @@ public class ECKey {
}
public static LazyECPoint compressPoint(LazyECPoint point) {
return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get()));
return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get()), true);
}
private static ECPoint getPointWithCompression(ECPoint point, boolean compressed) {
@ -98,4 +99,16 @@ public class ECKey {
}
return new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey);
}
/**
* Returns true if the given pubkey is in its compressed form.
*/
public static boolean isPubKeyCompressed(byte[] encoded) {
if (encoded.length == 33 && (encoded[0] == 0x02 || encoded[0] == 0x03))
return true;
else if (encoded.length == 65 && encoded[0] == 0x04)
return false;
else
throw new IllegalArgumentException(Hex.toHexString(encoded));
}
}

View file

@ -2,6 +2,7 @@ package com.craigraw.drongo.crypto;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import java.util.Arrays;
@ -11,6 +12,7 @@ public class LazyECPoint {
private final ECCurve curve;
private final byte[] bits;
private final boolean compressed;
// This field is effectively final - once set it won't change again. However it can be set after
// construction.
@ -19,10 +21,12 @@ public class LazyECPoint {
public LazyECPoint(ECCurve curve, byte[] bits) {
this.curve = curve;
this.bits = bits;
this.compressed = ECKey.isPubKeyCompressed(bits);
}
public LazyECPoint(ECPoint point) {
public LazyECPoint(ECPoint point, boolean compressed) {
this.point = point;
this.compressed = compressed;
this.curve = null;
this.bits = null;
}
@ -40,13 +44,40 @@ public class LazyECPoint {
}
public boolean isCompressed() {
return get().isCompressed();
return compressed;
}
public byte[] getEncoded() {
if (bits != null)
return Arrays.copyOf(bits, bits.length);
else
return get().getEncoded();
return get().getEncoded(compressed);
}
public byte[] getEncoded(boolean compressed) {
if (compressed == isCompressed() && bits != null)
return Arrays.copyOf(bits, bits.length);
else
return get().getEncoded(compressed);
}
public String toString() {
return Hex.toHexString(getEncoded());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return Arrays.equals(getCanonicalEncoding(), ((LazyECPoint)o).getCanonicalEncoding());
}
@Override
public int hashCode() {
return Arrays.hashCode(getCanonicalEncoding());
}
private byte[] getCanonicalEncoding() {
return getEncoded(true);
}
}

View file

@ -168,4 +168,33 @@ public class Script {
else
return value - 1 + OP_1;
}
public String toString() {
StringBuilder builder = new StringBuilder();
for(ScriptChunk chunk : chunks) {
builder.append(chunk.toString());
builder.append(" ");
}
return builder.toString().trim();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return Arrays.equals(getQuickProgram(), ((Script)o).getQuickProgram());
}
@Override
public int hashCode() {
return Arrays.hashCode(getQuickProgram());
}
// Utility that doesn't copy for internal use
private byte[] getQuickProgram() {
if (program != null)
return program;
return getProgram();
}
}

View file

@ -1,9 +1,13 @@
package com.craigraw.drongo.protocol;
import com.craigraw.drongo.Utils;
import org.bouncycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Objects;
import static com.craigraw.drongo.protocol.ScriptOpCodes.*;
@ -68,4 +72,53 @@ public class ScriptChunk {
stream.write(opcode); // smallNum
}
}
public byte[] toByteArray() {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
write(stream);
} catch (IOException e) {
// Should not happen as ByteArrayOutputStream does not throw IOException on write
throw new RuntimeException(e);
}
return stream.toByteArray();
}
/*
* The size, in bytes, that this chunk would occupy if serialized into a Script.
*/
public int size() {
final int opcodeLength = 1;
int pushDataSizeLength = 0;
if (opcode == OP_PUSHDATA1) pushDataSizeLength = 1;
else if (opcode == OP_PUSHDATA2) pushDataSizeLength = 2;
else if (opcode == OP_PUSHDATA4) pushDataSizeLength = 4;
final int dataLength = data == null ? 0 : data.length;
return opcodeLength + pushDataSizeLength + dataLength;
}
public String toString() {
if (data == null) {
return "OP_" + getOpCodeName(opcode);
}
if (data.length == 0) {
return "0";
}
return Hex.toHexString(data);
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ScriptChunk other = (ScriptChunk) o;
return opcode == other.opcode && Arrays.equals(data, other.data);
}
public int hashCode() {
return Objects.hash(opcode, Arrays.hashCode(data));
}
}

View file

@ -16,6 +16,7 @@
package com.craigraw.drongo.protocol;
import java.util.HashMap;
import java.util.Map;
/**
@ -161,4 +162,164 @@ public class ScriptOpCodes {
public static final int OP_NOP9 = 0xb8;
public static final int OP_NOP10 = 0xb9;
public static final int OP_INVALIDOPCODE = 0xff;
private static final Map<Integer, String> opCodeNameMap;
private static final Map<String, Integer> nameOpCodeMap;
static {
opCodeNameMap = new HashMap<Integer, String>();
opCodeNameMap.put(OP_0, "0");
opCodeNameMap.put(OP_PUSHDATA1, "PUSHDATA1");
opCodeNameMap.put(OP_PUSHDATA2, "PUSHDATA2");
opCodeNameMap.put(OP_PUSHDATA4, "PUSHDATA4");
opCodeNameMap.put(OP_1NEGATE, "1NEGATE");
opCodeNameMap.put(OP_RESERVED, "RESERVED");
opCodeNameMap.put(OP_1, "1");
opCodeNameMap.put(OP_2, "2");
opCodeNameMap.put(OP_3, "3");
opCodeNameMap.put(OP_4, "4");
opCodeNameMap.put(OP_5, "5");
opCodeNameMap.put(OP_6, "6");
opCodeNameMap.put(OP_7, "7");
opCodeNameMap.put(OP_8, "8");
opCodeNameMap.put(OP_9, "9");
opCodeNameMap.put(OP_10, "10");
opCodeNameMap.put(OP_11, "11");
opCodeNameMap.put(OP_12, "12");
opCodeNameMap.put(OP_13, "13");
opCodeNameMap.put(OP_14, "14");
opCodeNameMap.put(OP_15, "15");
opCodeNameMap.put(OP_16, "16");
opCodeNameMap.put(OP_NOP, "NOP");
opCodeNameMap.put(OP_VER, "VER");
opCodeNameMap.put(OP_IF, "IF");
opCodeNameMap.put(OP_NOTIF, "NOTIF");
opCodeNameMap.put(OP_VERIF, "VERIF");
opCodeNameMap.put(OP_VERNOTIF, "VERNOTIF");
opCodeNameMap.put(OP_ELSE, "ELSE");
opCodeNameMap.put(OP_ENDIF, "ENDIF");
opCodeNameMap.put(OP_VERIFY, "VERIFY");
opCodeNameMap.put(OP_RETURN, "RETURN");
opCodeNameMap.put(OP_TOALTSTACK, "TOALTSTACK");
opCodeNameMap.put(OP_FROMALTSTACK, "FROMALTSTACK");
opCodeNameMap.put(OP_2DROP, "2DROP");
opCodeNameMap.put(OP_2DUP, "2DUP");
opCodeNameMap.put(OP_3DUP, "3DUP");
opCodeNameMap.put(OP_2OVER, "2OVER");
opCodeNameMap.put(OP_2ROT, "2ROT");
opCodeNameMap.put(OP_2SWAP, "2SWAP");
opCodeNameMap.put(OP_IFDUP, "IFDUP");
opCodeNameMap.put(OP_DEPTH, "DEPTH");
opCodeNameMap.put(OP_DROP, "DROP");
opCodeNameMap.put(OP_DUP, "DUP");
opCodeNameMap.put(OP_NIP, "NIP");
opCodeNameMap.put(OP_OVER, "OVER");
opCodeNameMap.put(OP_PICK, "PICK");
opCodeNameMap.put(OP_ROLL, "ROLL");
opCodeNameMap.put(OP_ROT, "ROT");
opCodeNameMap.put(OP_SWAP, "SWAP");
opCodeNameMap.put(OP_TUCK, "TUCK");
opCodeNameMap.put(OP_CAT, "CAT");
opCodeNameMap.put(OP_SUBSTR, "SUBSTR");
opCodeNameMap.put(OP_LEFT, "LEFT");
opCodeNameMap.put(OP_RIGHT, "RIGHT");
opCodeNameMap.put(OP_SIZE, "SIZE");
opCodeNameMap.put(OP_INVERT, "INVERT");
opCodeNameMap.put(OP_AND, "AND");
opCodeNameMap.put(OP_OR, "OR");
opCodeNameMap.put(OP_XOR, "XOR");
opCodeNameMap.put(OP_EQUAL, "EQUAL");
opCodeNameMap.put(OP_EQUALVERIFY, "EQUALVERIFY");
opCodeNameMap.put(OP_RESERVED1, "RESERVED1");
opCodeNameMap.put(OP_RESERVED2, "RESERVED2");
opCodeNameMap.put(OP_1ADD, "1ADD");
opCodeNameMap.put(OP_1SUB, "1SUB");
opCodeNameMap.put(OP_2MUL, "2MUL");
opCodeNameMap.put(OP_2DIV, "2DIV");
opCodeNameMap.put(OP_NEGATE, "NEGATE");
opCodeNameMap.put(OP_ABS, "ABS");
opCodeNameMap.put(OP_NOT, "NOT");
opCodeNameMap.put(OP_0NOTEQUAL, "0NOTEQUAL");
opCodeNameMap.put(OP_ADD, "ADD");
opCodeNameMap.put(OP_SUB, "SUB");
opCodeNameMap.put(OP_MUL, "MUL");
opCodeNameMap.put(OP_DIV, "DIV");
opCodeNameMap.put(OP_MOD, "MOD");
opCodeNameMap.put(OP_LSHIFT, "LSHIFT");
opCodeNameMap.put(OP_RSHIFT, "RSHIFT");
opCodeNameMap.put(OP_BOOLAND, "BOOLAND");
opCodeNameMap.put(OP_BOOLOR, "BOOLOR");
opCodeNameMap.put(OP_NUMEQUAL, "NUMEQUAL");
opCodeNameMap.put(OP_NUMEQUALVERIFY, "NUMEQUALVERIFY");
opCodeNameMap.put(OP_NUMNOTEQUAL, "NUMNOTEQUAL");
opCodeNameMap.put(OP_LESSTHAN, "LESSTHAN");
opCodeNameMap.put(OP_GREATERTHAN, "GREATERTHAN");
opCodeNameMap.put(OP_LESSTHANOREQUAL, "LESSTHANOREQUAL");
opCodeNameMap.put(OP_GREATERTHANOREQUAL, "GREATERTHANOREQUAL");
opCodeNameMap.put(OP_MIN, "MIN");
opCodeNameMap.put(OP_MAX, "MAX");
opCodeNameMap.put(OP_WITHIN, "WITHIN");
opCodeNameMap.put(OP_RIPEMD160, "RIPEMD160");
opCodeNameMap.put(OP_SHA1, "SHA1");
opCodeNameMap.put(OP_SHA256, "SHA256");
opCodeNameMap.put(OP_HASH160, "HASH160");
opCodeNameMap.put(OP_HASH256, "HASH256");
opCodeNameMap.put(OP_CODESEPARATOR, "CODESEPARATOR");
opCodeNameMap.put(OP_CHECKSIG, "CHECKSIG");
opCodeNameMap.put(OP_CHECKSIGVERIFY, "CHECKSIGVERIFY");
opCodeNameMap.put(OP_CHECKMULTISIG, "CHECKMULTISIG");
opCodeNameMap.put(OP_CHECKMULTISIGVERIFY, "CHECKMULTISIGVERIFY");
opCodeNameMap.put(OP_NOP1, "NOP1");
opCodeNameMap.put(OP_CHECKLOCKTIMEVERIFY, "CHECKLOCKTIMEVERIFY");
opCodeNameMap.put(OP_CHECKSEQUENCEVERIFY, "CHECKSEQUENCEVERIFY");
opCodeNameMap.put(OP_NOP4, "NOP4");
opCodeNameMap.put(OP_NOP5, "NOP5");
opCodeNameMap.put(OP_NOP6, "NOP6");
opCodeNameMap.put(OP_NOP7, "NOP7");
opCodeNameMap.put(OP_NOP8, "NOP8");
opCodeNameMap.put(OP_NOP9, "NOP9");
opCodeNameMap.put(OP_NOP10, "NOP10");
nameOpCodeMap = new HashMap<String, Integer>();
for(Map.Entry<Integer, String> e : opCodeNameMap.entrySet()) {
nameOpCodeMap.put(e.getValue(), e.getKey());
}
nameOpCodeMap.put("OP_FALSE", OP_FALSE);
nameOpCodeMap.put("OP_TRUE", OP_TRUE);
nameOpCodeMap.put("NOP2", OP_NOP2);
nameOpCodeMap.put("NOP3", OP_NOP3);
}
/**
* Converts the given OpCode into a string (eg "0", "PUSHDATA", or "NON_OP(10)")
*/
public static String getOpCodeName(int opcode) {
if (opCodeNameMap.containsKey((Integer)opcode)) {
return opCodeNameMap.get(opcode);
}
return "NON_OP(" + opcode + ")";
}
/**
* Converts the given pushdata OpCode into a string (eg "PUSHDATA2", or "PUSHDATA(23)")
*/
public static String getPushDataName(int opcode) {
if (opCodeNameMap.containsKey(opcode)) {
return opCodeNameMap.get(opcode);
}
return "PUSHDATA(" + opcode + ")";
}
/**
* Converts the given OpCodeName into an int
*/
public static int getOpCode(String opCodeName) {
if (opCodeNameMap.containsKey(opCodeName)) {
return nameOpCodeMap.get(opCodeName);
}
return OP_INVALIDOPCODE;
}
}

View file

@ -26,6 +26,14 @@ public class Transaction extends TransactionPart {
super(rawtx, 0);
}
public long getVersion() {
return version;
}
public long getLockTime() {
return lockTime;
}
public Sha256Hash getTxId() {
if (cachedTxId == null) {
if (!hasWitnesses() && cachedWTxId != null) {
@ -177,6 +185,54 @@ public class Transaction extends TransactionPart {
return Collections.unmodifiableList(outputs);
}
/**
* 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.
*/
public enum SigHash {
ALL(1),
NONE(2),
SINGLE(3),
ANYONECANPAY(0x80), // Caution: Using this type in isolation is non-standard. Treated similar to ANYONECANPAY_ALL.
ANYONECANPAY_ALL(0x81),
ANYONECANPAY_NONE(0x82),
ANYONECANPAY_SINGLE(0x83),
UNSET(0); // Caution: Using this type in isolation is non-standard. Treated similar to ALL.
public final int value;
/**
* @param value
*/
private SigHash(final int value) {
this.value = value;
}
/**
* @return the value as a int
*/
public int intValue() {
return this.value;
}
/**
* @return the value as a byte
*/
public byte byteValue() {
return (byte) this.value;
}
public static SigHash fromInt(int sigHashInt) {
for(SigHash value : SigHash.values()) {
if(sigHashInt == value.intValue()) {
return value;
}
}
throw new IllegalArgumentException("No defined sighash value for int " + sigHashInt);
}
}
public static final void main(String[] args) {
String hex = "020000000001017811567adbc80d903030ae30fc28d5cd7c395a6a74ccab96734cf5da5bd67f1a0100000000feffffff0227030000000000002200206a4c4d9be3de0e40f601d11cebd86b6d8763caa9d91f8e5e8de5f5fc8657d46da00f000000000000220020e9eaae21539323a2627701dd2c234e3499e0faf563d73fd5fcd4d263192924a604004730440220385a8b9b998abfc9319b710c44b78727b189d7029fc6e4b6c4013a3ff2976a7b02207ab7ca6aedd8d86de6d08835d8b3e4481c778043675f59f72241e7d608aa80820147304402201f62ed94f41b77ee5eb490e127ead10bd4c2144a2eacc8d61865d86fec437ed2022037488b5b96390911ded8ba086b419c335c037dc4cb004202313635741d3691b001475221022a0d4dd0d1a7182cd45de3f460737988c17653428dcb32d9c2ab35a584c716882103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a252ae8d0b0900";
byte[] transactionBytes = Utils.hexToBytes(hex);

View file

@ -0,0 +1,664 @@
package com.craigraw.drongo.psbt;
import com.craigraw.drongo.ExtendedPublicKey;
import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.Utils;
import com.craigraw.drongo.crypto.ChildNumber;
import com.craigraw.drongo.crypto.ECKey;
import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.*;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
public class PSBT {
public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00;
public static final byte PSBT_GLOBAL_BIP32_PUBKEY = 0x01;
public static final byte PSBT_GLOBAL_VERSION = (byte)0xfb;
public static final byte PSBT_GLOBAL_PROPRIETARY = (byte)0xfc;
public static final byte PSBT_IN_NON_WITNESS_UTXO = 0x00;
public static final byte PSBT_IN_WITNESS_UTXO = 0x01;
public static final byte PSBT_IN_PARTIAL_SIG = 0x02;
public static final byte PSBT_IN_SIGHASH_TYPE = 0x03;
public static final byte PSBT_IN_REDEEM_SCRIPT = 0x04;
public static final byte PSBT_IN_WITNESS_SCRIPT = 0x05;
public static final byte PSBT_IN_BIP32_DERIVATION = 0x06;
public static final byte PSBT_IN_FINAL_SCRIPTSIG = 0x07;
public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08;
public static final byte PSBT_IN_POR_COMMITMENT = 0x09;
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00;
public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01;
public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02;
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
public static final String PSBT_MAGIC = "70736274";
private static final int STATE_GLOBALS = 1;
private static final int STATE_INPUTS = 2;
private static final int STATE_OUTPUTS = 3;
private static final int STATE_END = 4;
private static final int HARDENED = 0x80000000;
private int inputs = 0;
private int outputs = 0;
private boolean parseOK = false;
private String strPSBT = null;
private byte[] psbtBytes = null;
private ByteBuffer psbtByteBuffer = null;
private Transaction transaction = null;
private Integer version = null;
private Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
private Map<String, String> globalProprietary = new LinkedHashMap<>();
private List<PSBTInput> psbtInputs = new ArrayList<>();
private List<PSBTOutput> psbtOutputs = new ArrayList<>();
private static final Logger log = LoggerFactory.getLogger(PSBT.class);
public PSBT(String strPSBT) throws Exception {
if (!isPSBT(strPSBT)) {
log.debug("Provided string is not a PSBT");
return;
}
if (Utils.isBase64(strPSBT) && !Utils.isHex(strPSBT)) {
this.strPSBT = Hex.toHexString(Base64.decode(strPSBT));
} else {
this.strPSBT = strPSBT;
}
psbtBytes = Hex.decode(this.strPSBT);
psbtByteBuffer = ByteBuffer.wrap(psbtBytes);
read();
}
public PSBT(byte[] psbt) throws Exception {
this(Hex.toHexString(psbt));
}
public void read() throws Exception {
int seenInputs = 0;
int seenOutputs = 0;
psbtBytes = Hex.decode(strPSBT);
psbtByteBuffer = ByteBuffer.wrap(psbtBytes);
log.debug("--- ***** START ***** ---");
log.debug("--- PSBT length:" + psbtBytes.length + "---");
log.debug("--- parsing header ---");
byte[] magicBuf = new byte[4];
psbtByteBuffer.get(magicBuf);
if (!PSBT.PSBT_MAGIC.equalsIgnoreCase(Hex.toHexString(magicBuf))) {
throw new Exception("Invalid magic value");
}
byte sep = psbtByteBuffer.get();
if (sep != (byte) 0xff) {
throw new Exception("Bad 0xff separator:" + Hex.toHexString(new byte[]{sep}));
}
int currentState = STATE_GLOBALS;
PSBTInput currentInput = new PSBTInput();
PSBTOutput currentOutput = new PSBTOutput();
while (psbtByteBuffer.hasRemaining()) {
if (currentState == STATE_GLOBALS) {
log.debug("--- parsing globals ---");
} else if (currentState == STATE_INPUTS) {
log.debug("--- parsing inputs ---");
} else if (currentState == STATE_OUTPUTS) {
log.debug("--- parsing outputs ---");
}
PSBTEntry entry = parse();
if (entry == null) {
log.debug("PSBT parse returned null entry");
}
if (entry.getKey() == null) { // length == 0
switch (currentState) {
case STATE_GLOBALS:
currentState = STATE_INPUTS;
break;
case STATE_INPUTS:
psbtInputs.add(currentInput);
currentInput = new PSBTInput();
seenInputs++;
if (seenInputs == inputs) {
currentState = STATE_OUTPUTS;
}
break;
case STATE_OUTPUTS:
psbtOutputs.add(currentOutput);
currentOutput = new PSBTOutput();
seenOutputs++;
if (seenOutputs == outputs) {
currentState = STATE_END;
}
break;
case STATE_END:
parseOK = true;
break;
default:
log.debug("PSBT read is in unknown state");
break;
}
} else if (currentState == STATE_GLOBALS) {
switch (entry.getKeyType()[0]) {
case PSBT.PSBT_GLOBAL_UNSIGNED_TX:
Transaction transaction = new Transaction(entry.getData());
inputs = transaction.getInputs().size();
outputs = transaction.getOutputs().size();
log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime());
for(TransactionInput input: transaction.getInputs()) {
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript());
}
for(TransactionOutput output: transaction.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());
}
setTransaction(transaction);
break;
case PSBT.PSBT_GLOBAL_BIP32_PUBKEY:
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivationPath(), Base58.encodeChecked(entry.getKeyData()), null);
addExtendedPublicKey(pubKey, keyDerivation);
log.debug("Pubkey with master fingerprint " + pubKey.getMasterFingerprint() + " at path " + pubKey.getKeyDerivationPath() + ": " + pubKey.getExtendedPublicKey());
break;
case PSBT.PSBT_GLOBAL_VERSION:
int version = (int)Utils.readUint32(entry.getData(), 0);
setVersion(version);
log.debug("PSBT version: " + version);
break;
case PSBT.PSBT_GLOBAL_PROPRIETARY:
addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("PSBT global proprietary data: " + Hex.toHexString(entry.getData()));
break;
default:
log.debug("PSBT global not recognized key type: " + entry.getKeyType()[0]);
break;
}
} else if (currentState == STATE_INPUTS) {
switch (entry.getKeyType()[0]) {
case PSBT.PSBT_IN_NON_WITNESS_UTXO:
Transaction nonWitnessTx = new Transaction(entry.getData());
currentInput.setNonWitnessUtxo(nonWitnessTx);
log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLockTime());
for(TransactionInput input: nonWitnessTx.getInputs()) {
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript());
}
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());
}
break;
case PSBT.PSBT_IN_WITNESS_UTXO:
TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0);
currentInput.setWitnessUtxo(witnessTxOutput);
log.debug("Found input witness utxo amount " + witnessTxOutput.getValue() + " script hex " + Hex.toHexString(witnessTxOutput.getScript().getProgram()) + " script " + witnessTxOutput.getScript() + " addresses " + Arrays.asList(witnessTxOutput.getScript().getToAddresses()));
break;
case PSBT.PSBT_IN_PARTIAL_SIG:
LazyECPoint sigPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
currentInput.addPartialSignature(sigPublicKey, entry.getData());
log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData()));
break;
case PSBT.PSBT_IN_SIGHASH_TYPE:
long sighashType = Utils.readUint32(entry.getData(), 0);
Transaction.SigHash sigHash = Transaction.SigHash.fromInt((int)sighashType);
currentInput.setSigHash(sigHash);
log.debug("Found input sighash_type " + sigHash.toString());
break;
case PSBT.PSBT_IN_REDEEM_SCRIPT:
Script redeemScript = new Script(entry.getData());
currentInput.setRedeemScript(redeemScript);
log.debug("Found input redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
break;
case PSBT.PSBT_IN_WITNESS_SCRIPT:
Script witnessScript = new Script(entry.getData());
currentInput.setWitnessScript(witnessScript);
log.debug("Found input witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
break;
case PSBT.PSBT_IN_BIP32_DERIVATION:
LazyECPoint derivedPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
currentInput.addDerivedPublicKey(derivedPublicKey, keyDerivation);
log.debug("Found input bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey);
break;
case PSBT.PSBT_IN_FINAL_SCRIPTSIG:
Script finalScriptSig = new Script(entry.getData());
currentInput.setFinalScriptSig(finalScriptSig);
log.debug("Found input final scriptSig script hex " + Hex.toHexString(finalScriptSig.getProgram()) + " script " + finalScriptSig.toString());
break;
case PSBT.PSBT_IN_FINAL_SCRIPTWITNESS:
Script finalScriptWitness = new Script(entry.getData());
currentInput.setFinalScriptWitness(finalScriptWitness);
log.debug("Found input final scriptWitness script hex " + Hex.toHexString(finalScriptWitness.getProgram()) + " script " + finalScriptWitness.toString());
break;
case PSBT.PSBT_IN_POR_COMMITMENT:
String porMessage = new String(entry.getData(), "UTF-8");
currentInput.setPorCommitment(porMessage);
log.debug("Found input POR commitment message " + porMessage);
break;
case PSBT.PSBT_IN_PROPRIETARY:
currentInput.addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("Found proprietary input " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
break;
default:
log.debug("PSBT input not recognized key type:" + entry.getKeyType()[0]);
break;
}
} else if (currentState == STATE_OUTPUTS) {
switch (entry.getKeyType()[0]) {
case PSBT.PSBT_OUT_REDEEM_SCRIPT:
Script redeemScript = new Script(entry.getData());
currentOutput.setRedeemScript(redeemScript);
log.debug("Found output redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
break;
case PSBT.PSBT_OUT_WITNESS_SCRIPT:
Script witnessScript = new Script(entry.getData());
currentOutput.setWitnessScript(witnessScript);
log.debug("Found output witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
break;
case PSBT.PSBT_OUT_BIP32_DERIVATION:
LazyECPoint publicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData());
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
currentOutput.addDerivedPublicKey(publicKey, keyDerivation);
log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + publicKey);
break;
case PSBT.PSBT_OUT_PROPRIETARY:
currentOutput.addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
log.debug("Found proprietary output " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
break;
default:
log.debug("PSBT output not recognized key type:" + entry.getKeyType()[0]);
break;
}
} else {
log.debug("PSBT structure invalid");
}
}
if (currentState == STATE_END) {
log.debug("--- ***** END ***** ---");
}
}
private PSBTEntry parse() {
PSBTEntry entry = new PSBTEntry();
try {
int keyLen = PSBT.readCompactInt(psbtByteBuffer);
log.debug("PSBT entry key length: " + keyLen);
if (keyLen == 0x00) {
log.debug("PSBT entry separator 0x00");
return entry;
}
byte[] key = new byte[keyLen];
psbtByteBuffer.get(key);
log.debug("PSBT entry key: " + Hex.toHexString(key));
byte[] keyType = new byte[1];
keyType[0] = key[0];
log.debug("PSBT entry key type: " + Hex.toHexString(keyType));
byte[] keyData = null;
if (key.length > 1) {
keyData = new byte[key.length - 1];
System.arraycopy(key, 1, keyData, 0, keyData.length);
log.debug("PSBT entry key data: " + Hex.toHexString(keyData));
}
int dataLen = PSBT.readCompactInt(psbtByteBuffer);
log.debug("PSBT entry data length: " + dataLen);
byte[] data = new byte[dataLen];
psbtByteBuffer.get(data);
log.debug("PSBT entry data: " + Hex.toHexString(data));
entry.setKey(key);
entry.setKeyType(keyType);
entry.setKeyData(keyData);
entry.setData(data);
return entry;
} catch (Exception e) {
log.debug("Error parsing PSBT entry", e);
return null;
}
}
private PSBTEntry populateEntry(byte type, byte[] keydata, byte[] data) throws Exception {
PSBTEntry entry = new PSBTEntry();
entry.setKeyType(new byte[]{type});
entry.setKey(new byte[]{type});
if (keydata != null) {
entry.setKeyData(keydata);
}
entry.setData(data);
return entry;
}
public byte[] serialize() throws IOException {
ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream();
transaction.bitcoinSerialize(transactionbaos);
byte[] serialized = transactionbaos.toByteArray();
byte[] txLen = PSBT.writeCompactInt(serialized.length);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// magic
baos.write(Hex.decode(PSBT.PSBT_MAGIC), 0, Hex.decode(PSBT.PSBT_MAGIC).length);
// separator
baos.write((byte) 0xff);
// globals
baos.write(writeCompactInt(1L)); // key length
baos.write((byte) 0x00); // key
baos.write(txLen, 0, txLen.length); // value length
baos.write(serialized, 0, serialized.length); // value
baos.write((byte) 0x00);
// inputs
// for (PSBTEntry entry : psbtInputs) {
// int keyLen = 1;
// if (entry.getKeyData() != null) {
// keyLen += entry.getKeyData().length;
// }
// baos.write(writeCompactInt(keyLen));
// baos.write(entry.getKey());
// if (entry.getKeyData() != null) {
// baos.write(entry.getKeyData());
// }
// baos.write(writeCompactInt(entry.getData().length));
// baos.write(entry.getData());
// }
// baos.write((byte) 0x00);
//
// // outputs
// for (PSBTEntry entry : psbtOutputs) {
// int keyLen = 1;
// if (entry.getKeyData() != null) {
// keyLen += entry.getKeyData().length;
// }
// baos.write(writeCompactInt(keyLen));
// baos.write(entry.getKey());
// if (entry.getKeyData() != null) {
// baos.write(entry.getKeyData());
// }
// baos.write(writeCompactInt(entry.getData().length));
// baos.write(entry.getData());
// }
baos.write((byte) 0x00);
// eof
baos.write((byte) 0x00);
psbtBytes = baos.toByteArray();
strPSBT = Hex.toHexString(psbtBytes);
log.debug("Wrote PSBT: " + strPSBT);
return psbtBytes;
}
public List<PSBTInput> getPsbtInputs() {
return psbtInputs;
}
public List<PSBTOutput> getPsbtOutputs() {
return psbtOutputs;
}
public Transaction getTransaction() {
return transaction;
}
public void setTransaction(Transaction transaction) {
testIfNull(this.transaction);
this.transaction = transaction;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
testIfNull(this.version);
this.version = version;
}
public KeyDerivation getKeyDerivation(ExtendedPublicKey publicKey) {
return extendedPublicKeys.get(publicKey);
}
public List<ExtendedPublicKey> getExtendedPublicKeys() {
return new ArrayList<ExtendedPublicKey>(extendedPublicKeys.keySet());
}
public void addExtendedPublicKey(ExtendedPublicKey publicKey, KeyDerivation derivation) {
if(extendedPublicKeys.containsKey(publicKey)) {
throw new IllegalStateException("Duplicate public key in scope");
}
this.extendedPublicKeys.put(publicKey, derivation);
}
public void addProprietary(String key, String data) {
globalProprietary.put(key, data);
}
private void testIfNull(Object obj) {
if(obj != null) {
throw new IllegalStateException("Duplicate keys in scope");
}
}
public String toString() {
try {
return Hex.toHexString(serialize());
} catch (IOException ioe) {
return null;
}
}
public String toBase64String() throws IOException {
return Base64.toBase64String(serialize());
}
public static int readCompactInt(ByteBuffer psbtByteBuffer) throws Exception {
byte b = psbtByteBuffer.get();
switch (b) {
case (byte) 0xfd: {
byte[] buf = new byte[2];
psbtByteBuffer.get(buf);
ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
return byteBuffer.getShort();
}
case (byte) 0xfe: {
byte[] buf = new byte[4];
psbtByteBuffer.get(buf);
ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
return byteBuffer.getInt();
}
case (byte) 0xff: {
byte[] buf = new byte[8];
psbtByteBuffer.get(buf);
ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
throw new Exception("Data too long:" + byteBuffer.getLong());
}
default:
return (int) (b & 0xff);
}
}
public static byte[] writeCompactInt(long val) {
ByteBuffer bb = null;
if (val < 0xfdL) {
bb = ByteBuffer.allocate(1);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put((byte) val);
} else if (val < 0xffffL) {
bb = ByteBuffer.allocate(3);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put((byte) 0xfd);
bb.put((byte) (val & 0xff));
bb.put((byte) ((val >> 8) & 0xff));
} else if (val < 0xffffffffL) {
bb = ByteBuffer.allocate(5);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put((byte) 0xfe);
bb.putInt((int) val);
} else {
bb = ByteBuffer.allocate(9);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put((byte) 0xff);
bb.putLong(val);
}
return bb.array();
}
public static byte[] writeSegwitInputUTXO(long value, byte[] scriptPubKey) {
byte[] ret = new byte[scriptPubKey.length + Long.BYTES];
// long to byte array
ByteBuffer xlat = ByteBuffer.allocate(Long.BYTES);
xlat.order(ByteOrder.LITTLE_ENDIAN);
xlat.putLong(0, value);
byte[] val = new byte[Long.BYTES];
xlat.get(val);
System.arraycopy(val, 0, ret, 0, Long.BYTES);
System.arraycopy(scriptPubKey, 0, ret, Long.BYTES, scriptPubKey.length);
return ret;
}
public KeyDerivation parseKeyDerivation(byte[] data) {
String masterFingerprint = getMasterFingerprint(Arrays.copyOfRange(data, 0, 4));
List<ChildNumber> bip32pathList = readBIP32Derivation(Arrays.copyOfRange(data, 4, data.length));
String bip32path = KeyDerivation.writePath(bip32pathList);
return new KeyDerivation(masterFingerprint, bip32path);
}
public static String getMasterFingerprint(byte[] data) {
return Hex.toHexString(data);
}
public static List<ChildNumber> readBIP32Derivation(byte[] data) {
List<ChildNumber> path = new ArrayList<>();
ByteBuffer bb = ByteBuffer.wrap(data);
byte[] buf = new byte[4];
do {
bb.get(buf);
reverse(buf);
ByteBuffer pbuf = ByteBuffer.wrap(buf);
path.add(new ChildNumber(pbuf.getInt()));
} while(bb.hasRemaining());
return path;
}
private static void reverse(byte[] array) {
for (int i = 0; i < array.length / 2; i++) {
byte temp = array[i];
array[i] = array[array.length - i - 1];
array[array.length - i - 1] = temp;
}
}
public static byte[] writeBIP32Derivation(byte[] fingerprint, int purpose, int type, int account, int chain, int index) {
// fingerprint and integer values to BIP32 derivation buffer
byte[] bip32buf = new byte[24];
System.arraycopy(fingerprint, 0, bip32buf, 0, fingerprint.length);
ByteBuffer xlat = ByteBuffer.allocate(Integer.BYTES);
xlat.order(ByteOrder.LITTLE_ENDIAN);
xlat.putInt(0, purpose + HARDENED);
byte[] out = new byte[Integer.BYTES];
xlat.get(out);
System.arraycopy(out, 0, bip32buf, fingerprint.length, out.length);
xlat.clear();
xlat.order(ByteOrder.LITTLE_ENDIAN);
xlat.putInt(0, type + HARDENED);
xlat.get(out);
System.arraycopy(out, 0, bip32buf, fingerprint.length + out.length, out.length);
xlat.clear();
xlat.order(ByteOrder.LITTLE_ENDIAN);
xlat.putInt(0, account + HARDENED);
xlat.get(out);
System.arraycopy(out, 0, bip32buf, fingerprint.length + (out.length * 2), out.length);
xlat.clear();
xlat.order(ByteOrder.LITTLE_ENDIAN);
xlat.putInt(0, chain);
xlat.get(out);
System.arraycopy(out, 0, bip32buf, fingerprint.length + (out.length * 3), out.length);
xlat.clear();
xlat.order(ByteOrder.LITTLE_ENDIAN);
xlat.putInt(0, index);
xlat.get(out);
System.arraycopy(out, 0, bip32buf, fingerprint.length + (out.length * 4), out.length);
return bip32buf;
}
public static boolean isPSBT(String s) {
if (Utils.isHex(s) && s.startsWith(PSBT.PSBT_MAGIC)) {
return true;
} else if (Utils.isBase64(s) && Hex.toHexString(Base64.decode(s)).startsWith(PSBT.PSBT_MAGIC)) {
return true;
} else {
return false;
}
}
public static void main(String[] args) throws Exception {
String psbtBase64 = "cHNidP8BAMkCAAAAA3lxWr8zSZt5tiGZegyFWmd8b62cew6qi/4rTZGGif8OAAAAAAD/////td4T4zmwdQ8R2SbwRjRj+alAy1VX8mYZD2o9ZmefNIsAAAAAAP////+k9Xvvp9Lpap1TWd51NWu+MIfojG+MCqmguPyjII+5YgAAAAAA/////wKMz/AIAAAAABl2qRSE7GtWKUoaFcVQ8n9qfMYi41Yh0YisjM/wCAAAAAAZdqkUmka3O8TiIRG8h+a1mDLFQVTfJEiIrAAAAAAAAQBVAgAAAAGt3gAAAAAAAO++AAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAAAAA/////wEA4fUFAAAAABl2qRSvQiRNb8B3El3G+KdspA3+DRvH1IisAAAAACIGA383lPO+TErMCGrITWkCwCVxPqv4iQ8g9ErPCzTjwPD3DHSXSzsAAAAAAAAAAAABAFUCAAAAAa3eAAAAAAAA774AAAAAAAAAAAAAAAAAAAAAAAAAAAAASQAAAAD/////AQDh9QUAAAAAGXapFAn8nw1IXPh34v8wuhJrcu34Xg8qiKwAAAAAIgYDTr6iJ7sP/u+0gz4wi+Muuc4IxEoJaGYedN/uqwmSfbgMdJdLOwAAAAABAAAAAAEAVQIAAAABrd4AAAAAAADvvgAAAAAAAAAAAAAAAAAAAAAAAAAAAABJAAAAAP////8BAOH1BQAAAAAZdqkUGMIzFJsgyFIYzDbThZ5S2zTnvRiIrAAAAAAiBgK7oYu+Z/kEK6XK3urdEDW2ngkwnXD1gZBjEgRW0wD7Igx0l0s7AAAAAAIAAAAAACICAyw+nsM8JYHohVqRsQ2qilEwjZPh+OkGPqkO2kYZczCZEHSXSzsMAAAAIgAAADcBAAAA";
PSBT psbt = null;
String filename = "default.psbt";
File psbtFile = new File(filename);
if(psbtFile.exists()) {
byte[] psbtBytes = new byte[(int)psbtFile.length()];
FileInputStream stream = new FileInputStream(psbtFile);
stream.read(psbtBytes);
stream.close();
psbt = new PSBT(psbtBytes);
} else {
psbt = new PSBT(psbtBase64);
}
System.out.println(psbt);
}
}

View file

@ -0,0 +1,40 @@
package com.craigraw.drongo.psbt;
public class PSBTEntry {
private byte[] key = null;
private byte[] keyType = null;
private byte[] keyData = null;
private byte[] data = null;
public byte[] getKey() {
return key;
}
public void setKey(byte[] key) {
this.key = key;
}
public byte[] getKeyType() {
return keyType;
}
public void setKeyType(byte[] keyType) {
this.keyType = keyType;
}
public byte[] getKeyData() {
return keyData;
}
public void setKeyData(byte[] keyData) {
this.keyData = keyData;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}

View file

@ -0,0 +1,130 @@
package com.craigraw.drongo.psbt;
import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.Script;
import com.craigraw.drongo.protocol.Transaction;
import com.craigraw.drongo.protocol.TransactionOutput;
import java.util.LinkedHashMap;
import java.util.Map;
public class PSBTInput {
private Transaction nonWitnessUtxo;
private TransactionOutput witnessUtxo;
private Map<LazyECPoint, byte[]> partialSignatures = new LinkedHashMap<>();
private Transaction.SigHash sigHash;
private Script redeemScript;
private Script witnessScript;
private Map<LazyECPoint, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
private Script finalScriptSig;
private Script finalScriptWitness;
private String porCommitment;
private Map<String, String> proprietary = new LinkedHashMap<>();
public Transaction getNonWitnessUtxo() {
return nonWitnessUtxo;
}
public void setNonWitnessUtxo(Transaction nonWitnessUtxo) {
testIfNull(this.nonWitnessUtxo);
this.nonWitnessUtxo = nonWitnessUtxo;
}
public TransactionOutput getWitnessUtxo() {
return witnessUtxo;
}
public void setWitnessUtxo(TransactionOutput witnessUtxo) {
testIfNull(this.witnessUtxo);
this.witnessUtxo = witnessUtxo;
}
public byte[] getPartialSignature(LazyECPoint publicKey) {
return partialSignatures.get(publicKey);
}
public void addPartialSignature(LazyECPoint publicKey, byte[] partialSignature) {
if(partialSignatures.containsKey(publicKey)) {
throw new IllegalStateException("Duplicate public key signature in scope");
}
this.partialSignatures.put(publicKey, partialSignature);
}
public Transaction.SigHash getSigHash() {
return sigHash;
}
public void setSigHash(Transaction.SigHash sigHash) {
testIfNull(this.sigHash);
this.sigHash = sigHash;
}
public Script getRedeemScript() {
return redeemScript;
}
public void setRedeemScript(Script redeemScript) {
testIfNull(this.redeemScript);
this.redeemScript = redeemScript;
}
public Script getWitnessScript() {
return witnessScript;
}
public void setWitnessScript(Script witnessScript) {
testIfNull(this.witnessScript);
this.witnessScript = witnessScript;
}
public KeyDerivation getKeyDerivation(LazyECPoint publicKey) {
return derivedPublicKeys.get(publicKey);
}
public void addDerivedPublicKey(LazyECPoint publicKey, KeyDerivation derivation) {
if(derivedPublicKeys.containsKey(publicKey)) {
throw new IllegalStateException("Duplicate public key in scope");
}
this.derivedPublicKeys.put(publicKey, derivation);
}
public Script getFinalScriptSig() {
return finalScriptSig;
}
public void setFinalScriptSig(Script finalScriptSig) {
testIfNull(this.finalScriptSig);
this.finalScriptSig = finalScriptSig;
}
public Script getFinalScriptWitness() {
return finalScriptWitness;
}
public void setFinalScriptWitness(Script finalScriptWitness) {
testIfNull(this.finalScriptWitness);
this.finalScriptWitness = finalScriptWitness;
}
public String getPorCommitment() {
return porCommitment;
}
public void setPorCommitment(String porCommitment) {
testIfNull(this.porCommitment);
this.porCommitment = porCommitment;
}
public void addProprietary(String key, String data) {
proprietary.put(key, data);
}
private void testIfNull(Object obj) {
if(obj != null) {
throw new IllegalStateException("Duplicate keys in scope");
}
}
}

View file

@ -0,0 +1,55 @@
package com.craigraw.drongo.psbt;
import com.craigraw.drongo.KeyDerivation;
import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.Script;
import java.util.LinkedHashMap;
import java.util.Map;
public class PSBTOutput {
private Script redeemScript;
private Script witnessScript;
private Map<LazyECPoint, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
private Map<String, String> proprietary = new LinkedHashMap<>();
public Script getRedeemScript() {
return redeemScript;
}
public void setRedeemScript(Script redeemScript) {
testIfNull(this.redeemScript);
this.redeemScript = redeemScript;
}
public Script getWitnessScript() {
return witnessScript;
}
public void setWitnessScript(Script witnessScript) {
testIfNull(this.witnessScript);
this.witnessScript = witnessScript;
}
public KeyDerivation getKeyDerivation(LazyECPoint publicKey) {
return derivedPublicKeys.get(publicKey);
}
public void addDerivedPublicKey(LazyECPoint publicKey, KeyDerivation derivation) {
if(derivedPublicKeys.containsKey(publicKey)) {
throw new IllegalStateException("Duplicate public key in scope");
}
this.derivedPublicKeys.put(publicKey, derivation);
}
public void addProprietary(String key, String data) {
proprietary.put(key, data);
}
private void testIfNull(Object obj) {
if(obj != null) {
throw new IllegalStateException("Duplicate keys in scope");
}
}
}

View file

@ -1,4 +1,4 @@
log4j.rootLogger=INFO, stdout, file
log4j.rootLogger=DEBUG, stdout, file
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

View file

@ -40,4 +40,12 @@ public class OutputDescriptorTest {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6XiW9nhToS1gjVsFKzgmtWZuqo6V1YY7xaCns37aR3oYhFyAsTehAqV1iW2UCNtgWFQFkz3aNSZZbkfe5d1tD8MzjZuFJQn2XnczsxtjoXr");
Assert.assertEquals("sh(wpkh(xpub6CtEr82YekUCtCg8Vdu9gRUQfpx34vYd3Tga5eDh33RfeA9wcoV8YmpshJ4tCUEm6cHT1WT1unD1iU45MvbsQtgPsECpiVxYG4ZMVKEKqGP/0/*))", descriptor.toString());
}
@Test
public void masterP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)");
Assert.assertEquals("pkh(xpub6CY2xt3vG5BhUS7krcphJprmHCh3jHYB1A8bxtJocU8NyQttKUCLp5izorV1wdXbp7XSSEcaFiKzUroEAL5tD1de8iAUeHP76byTWZu79SD/1/*)", descriptor.toString());
Assert.assertEquals("d34db33f", descriptor.getSingletonExtendedPublicKey().getMasterFingerprint());
Assert.assertEquals("m/44'/0'/0'", descriptor.getSingletonExtendedPublicKey().getKeyDerivationPath());
}
}