mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56:44 +00:00
psbt initial draft
This commit is contained in:
parent
89e7892d7c
commit
ce2b0648a6
17 changed files with 1378 additions and 41 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
76
src/main/java/com/craigraw/drongo/KeyDerivation.java
Normal file
76
src/main/java/com/craigraw/drongo/KeyDerivation.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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++ ) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
664
src/main/java/com/craigraw/drongo/psbt/PSBT.java
Normal file
664
src/main/java/com/craigraw/drongo/psbt/PSBT.java
Normal 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);
|
||||
}
|
||||
}
|
40
src/main/java/com/craigraw/drongo/psbt/PSBTEntry.java
Normal file
40
src/main/java/com/craigraw/drongo/psbt/PSBTEntry.java
Normal 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;
|
||||
}
|
||||
}
|
130
src/main/java/com/craigraw/drongo/psbt/PSBTInput.java
Normal file
130
src/main/java/com/craigraw/drongo/psbt/PSBTInput.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
55
src/main/java/com/craigraw/drongo/psbt/PSBTOutput.java
Normal file
55
src/main/java/com/craigraw/drongo/psbt/PSBTOutput.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue