mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 18:16:45 +00:00
refactor psbt classes and add serialization
This commit is contained in:
parent
f2ee57cc4d
commit
0cfe954463
7 changed files with 342 additions and 309 deletions
|
@ -33,6 +33,10 @@ public enum SigHash {
|
|||
return this.value;
|
||||
}
|
||||
|
||||
public int intValue() {
|
||||
return Byte.toUnsignedInt(value);
|
||||
}
|
||||
|
||||
public boolean anyoneCanPay() {
|
||||
return (value & SigHash.ANYONECANPAY.value) != 0;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.sparrowwallet.drongo.protocol;
|
|||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
|
@ -41,6 +42,18 @@ public class TransactionOutput extends ChildMessage {
|
|||
scriptBytes = readBytes(scriptLen);
|
||||
}
|
||||
|
||||
public byte[] bitcoinSerialize() {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
bitcoinSerializeToStream(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
//can't happen
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||
Utils.int64ToByteStreamLE(value, stream);
|
||||
// TODO: Move script serialization into the Script class, where it belongs.
|
||||
|
|
|
@ -7,16 +7,14 @@ import com.sparrowwallet.drongo.crypto.ECKey;
|
|||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
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.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||
|
||||
public class PSBT {
|
||||
public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00;
|
||||
|
@ -32,8 +30,6 @@ public class PSBT {
|
|||
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;
|
||||
|
||||
|
@ -141,13 +137,13 @@ public class PSBT {
|
|||
|
||||
byte[] magicBuf = new byte[4];
|
||||
psbtByteBuffer.get(magicBuf);
|
||||
if (!PSBT_MAGIC_HEX.equalsIgnoreCase(Hex.toHexString(magicBuf))) {
|
||||
if (!PSBT_MAGIC_HEX.equalsIgnoreCase(Utils.bytesToHex(magicBuf))) {
|
||||
throw new PSBTParseException("PSBT has invalid magic value");
|
||||
}
|
||||
|
||||
byte sep = psbtByteBuffer.get();
|
||||
if (sep != (byte) 0xff) {
|
||||
throw new PSBTParseException("PSBT has bad initial separator: " + Hex.toHexString(new byte[]{sep}));
|
||||
throw new PSBTParseException("PSBT has bad initial separator: " + Utils.bytesToHex(new byte[]{sep}));
|
||||
}
|
||||
|
||||
int currentState = STATE_GLOBALS;
|
||||
|
@ -159,7 +155,7 @@ public class PSBT {
|
|||
List<PSBTEntry> outputEntries = new ArrayList<>();
|
||||
|
||||
while (psbtByteBuffer.hasRemaining()) {
|
||||
PSBTEntry entry = parseEntry(psbtByteBuffer);
|
||||
PSBTEntry entry = new PSBTEntry(psbtByteBuffer);
|
||||
|
||||
if(entry.getKey() == null) { // length == 0
|
||||
switch (currentState) {
|
||||
|
@ -220,59 +216,10 @@ public class PSBT {
|
|||
log.debug("Calculated fee at " + getFee());
|
||||
}
|
||||
|
||||
private PSBTEntry parseEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException {
|
||||
PSBTEntry entry = new PSBTEntry();
|
||||
|
||||
try {
|
||||
int keyLen = PSBT.readCompactInt(psbtByteBuffer);
|
||||
|
||||
if (keyLen == 0x00) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
byte[] key = new byte[keyLen];
|
||||
psbtByteBuffer.get(key);
|
||||
|
||||
byte keyType = key[0];
|
||||
|
||||
byte[] keyData = null;
|
||||
if (key.length > 1) {
|
||||
keyData = new byte[key.length - 1];
|
||||
System.arraycopy(key, 1, keyData, 0, keyData.length);
|
||||
}
|
||||
|
||||
int dataLen = PSBT.readCompactInt(psbtByteBuffer);
|
||||
byte[] data = new byte[dataLen];
|
||||
psbtByteBuffer.get(data);
|
||||
|
||||
entry.setKey(key);
|
||||
entry.setKeyType(keyType);
|
||||
entry.setKeyData(keyData);
|
||||
entry.setData(data);
|
||||
|
||||
return entry;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new PSBTParseException("Error parsing PSBT entry", e);
|
||||
}
|
||||
}
|
||||
|
||||
private PSBTEntry populateEntry(byte type, byte[] keydata, byte[] data) throws Exception {
|
||||
PSBTEntry entry = new PSBTEntry();
|
||||
entry.setKeyType(type);
|
||||
entry.setKey(new byte[]{type});
|
||||
if (keydata != null) {
|
||||
entry.setKeyData(keydata);
|
||||
}
|
||||
entry.setData(data);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private void parseGlobalEntries(List<PSBTEntry> globalEntries) throws PSBTParseException {
|
||||
PSBTEntry duplicate = findDuplicateKey(globalEntries);
|
||||
if(duplicate != null) {
|
||||
throw new PSBTParseException("Found duplicate key for PSBT global: " + Hex.toHexString(duplicate.getKey()));
|
||||
throw new PSBTParseException("Found duplicate key for PSBT global: " + Utils.bytesToHex(duplicate.getKey()));
|
||||
}
|
||||
|
||||
for(PSBTEntry entry : globalEntries) {
|
||||
|
@ -292,9 +239,9 @@ public class PSBT {
|
|||
}
|
||||
for(TransactionOutput output: transaction.getOutputs()) {
|
||||
try {
|
||||
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript());
|
||||
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Utils.bytesToHex(output.getScript().getProgram()) + " to script " + output.getScript());
|
||||
} catch(NonStandardScriptException e) {
|
||||
log.debug(" Transaction output value: " + output.getValue() + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript());
|
||||
log.debug(" Transaction output value: " + output.getValue() + " with script hex " + Utils.bytesToHex(output.getScript().getProgram()) + " to script " + output.getScript());
|
||||
}
|
||||
}
|
||||
this.transaction = transaction;
|
||||
|
@ -313,8 +260,8 @@ public class PSBT {
|
|||
log.debug("PSBT version: " + version);
|
||||
break;
|
||||
case PSBT_GLOBAL_PROPRIETARY:
|
||||
globalProprietary.put(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
|
||||
log.debug("PSBT global proprietary data: " + Hex.toHexString(entry.getData()));
|
||||
globalProprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("PSBT global proprietary data: " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
default:
|
||||
log.warn("PSBT global not recognized key type: " + entry.getKeyType());
|
||||
|
@ -326,7 +273,7 @@ public class PSBT {
|
|||
for(List<PSBTEntry> inputEntries : inputEntryLists) {
|
||||
PSBTEntry duplicate = findDuplicateKey(inputEntries);
|
||||
if(duplicate != null) {
|
||||
throw new PSBTParseException("Found duplicate key for PSBT input: " + Hex.toHexString(duplicate.getKey()));
|
||||
throw new PSBTParseException("Found duplicate key for PSBT input: " + Utils.bytesToHex(duplicate.getKey()));
|
||||
}
|
||||
|
||||
int inputIndex = this.psbtInputs.size();
|
||||
|
@ -345,7 +292,7 @@ public class PSBT {
|
|||
for(List<PSBTEntry> outputEntries : outputEntryLists) {
|
||||
PSBTEntry duplicate = findDuplicateKey(outputEntries);
|
||||
if(duplicate != null) {
|
||||
throw new PSBTParseException("Found duplicate key for PSBT output: " + Hex.toHexString(duplicate.getKey()));
|
||||
throw new PSBTParseException("Found duplicate key for PSBT output: " + Utils.bytesToHex(duplicate.getKey()));
|
||||
}
|
||||
|
||||
PSBTOutput output = new PSBTOutput(outputEntries);
|
||||
|
@ -356,7 +303,7 @@ public class PSBT {
|
|||
private PSBTEntry findDuplicateKey(List<PSBTEntry> entries) {
|
||||
Set<String> checkSet = new HashSet<>();
|
||||
for(PSBTEntry entry: entries) {
|
||||
if(!checkSet.add(Hex.toHexString(entry.getKey())) ) {
|
||||
if(!checkSet.add(Utils.bytesToHex(entry.getKey())) ) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
@ -408,63 +355,59 @@ public class PSBT {
|
|||
return true;
|
||||
}
|
||||
|
||||
public byte[] serialize() throws IOException {
|
||||
ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream();
|
||||
transaction.bitcoinSerializeToStream(transactionbaos);
|
||||
byte[] serialized = transactionbaos.toByteArray();
|
||||
byte[] txLen = PSBT.writeCompactInt(serialized.length);
|
||||
private List<PSBTEntry> getGlobalEntries() {
|
||||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
if(transaction != null) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_UNSIGNED_TX, null, transaction.bitcoinSerialize()));
|
||||
}
|
||||
|
||||
for(Map.Entry<ExtendedKey, KeyDerivation> entry : extendedPublicKeys.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_BIP32_PUBKEY, entry.getKey().getExtendedKeyBytes(), serializeKeyDerivation(entry.getValue())));
|
||||
}
|
||||
|
||||
if(version != null) {
|
||||
byte[] versionBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(version, versionBytes, 0);
|
||||
entries.add(populateEntry(PSBT_GLOBAL_VERSION, null, versionBytes));
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : globalProprietary.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_GLOBAL_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
// magic
|
||||
baos.write(Hex.decode(PSBT_MAGIC_HEX), 0, Hex.decode(PSBT_MAGIC_HEX).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);
|
||||
baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX));
|
||||
baos.writeBytes(new byte[] {(byte)0xff});
|
||||
|
||||
// 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);
|
||||
List<PSBTEntry> globalEntries = getGlobalEntries();
|
||||
for(PSBTEntry entry : globalEntries) {
|
||||
entry.serializeToStream(baos);
|
||||
}
|
||||
baos.writeBytes(new byte[] {(byte)0x00});
|
||||
|
||||
// eof
|
||||
baos.write((byte) 0x00);
|
||||
for(PSBTInput psbtInput : getPsbtInputs()) {
|
||||
List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
|
||||
for(PSBTEntry entry : inputEntries) {
|
||||
entry.serializeToStream(baos);
|
||||
}
|
||||
baos.writeBytes(new byte[] {(byte)0x00});
|
||||
}
|
||||
|
||||
psbtBytes = baos.toByteArray();
|
||||
for(PSBTOutput psbtOutput : getPsbtOutputs()) {
|
||||
List<PSBTEntry> outputEntries = psbtOutput.getOutputEntries();
|
||||
for(PSBTEntry entry : outputEntries) {
|
||||
entry.serializeToStream(baos);
|
||||
}
|
||||
baos.writeBytes(new byte[] {(byte)0x00});
|
||||
}
|
||||
|
||||
return psbtBytes;
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public List<PSBTInput> getPsbtInputs() {
|
||||
|
@ -496,133 +439,13 @@ public class PSBT {
|
|||
}
|
||||
|
||||
public String toString() {
|
||||
try {
|
||||
return Hex.toHexString(serialize());
|
||||
} catch (IOException ioe) {
|
||||
return null;
|
||||
}
|
||||
return Utils.bytesToHex(serialize());
|
||||
}
|
||||
|
||||
public String toBase64String() throws IOException {
|
||||
public String toBase64String() {
|
||||
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 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(byte[] b) {
|
||||
try {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(b);
|
||||
|
@ -639,7 +462,7 @@ public class PSBT {
|
|||
if (Utils.isHex(s) && s.startsWith(PSBT_MAGIC_HEX)) {
|
||||
return true;
|
||||
} else {
|
||||
return Utils.isBase64(s) && Hex.toHexString(Base64.decode(s)).startsWith(PSBT_MAGIC_HEX);
|
||||
return Utils.isBase64(s) && Utils.bytesToHex(Base64.decode(s)).startsWith(PSBT_MAGIC_HEX);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -649,29 +472,10 @@ public class PSBT {
|
|||
}
|
||||
|
||||
if (Utils.isBase64(strPSBT) && !Utils.isHex(strPSBT)) {
|
||||
strPSBT = Hex.toHexString(Base64.decode(strPSBT));
|
||||
strPSBT = Utils.bytesToHex(Base64.decode(strPSBT));
|
||||
}
|
||||
|
||||
byte[] psbtBytes = Hex.decode(strPSBT);
|
||||
byte[] psbtBytes = Utils.hexToBytes(strPSBT);
|
||||
return new PSBT(psbtBytes);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String psbtBase64 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=";
|
||||
|
||||
PSBT psbt = null;
|
||||
String filename = "default.psbt";
|
||||
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 = PSBT.fromString(psbtBase64);
|
||||
}
|
||||
|
||||
System.out.println(psbt);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +1,60 @@
|
|||
package com.sparrowwallet.drongo.psbt;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class PSBTEntry {
|
||||
private byte[] key = null;
|
||||
private byte keyType;
|
||||
private byte[] keyData = null;
|
||||
private byte[] data = null;
|
||||
private final byte[] key;
|
||||
private final byte keyType;
|
||||
private final byte[] keyData;
|
||||
private final byte[] data;
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(byte[] key) {
|
||||
public PSBTEntry(byte[] key, byte keyType, byte[] keyData, byte[] data) {
|
||||
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;
|
||||
}
|
||||
|
||||
PSBTEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException {
|
||||
int keyLen = readCompactInt(psbtByteBuffer);
|
||||
|
||||
if (keyLen == 0x00) {
|
||||
key = null;
|
||||
keyType = 0x00;
|
||||
keyData = null;
|
||||
data = null;
|
||||
} else {
|
||||
byte[] key = new byte[keyLen];
|
||||
psbtByteBuffer.get(key);
|
||||
|
||||
byte keyType = key[0];
|
||||
|
||||
byte[] keyData = null;
|
||||
if (key.length > 1) {
|
||||
keyData = new byte[key.length - 1];
|
||||
System.arraycopy(key, 1, keyData, 0, keyData.length);
|
||||
}
|
||||
|
||||
int dataLen = readCompactInt(psbtByteBuffer);
|
||||
byte[] data = new byte[dataLen];
|
||||
psbtByteBuffer.get(data);
|
||||
|
||||
this.key = key;
|
||||
this.keyType = keyType;
|
||||
this.keyData = keyData;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyDerivation parseKeyDerivation(byte[] data) throws PSBTParseException {
|
||||
if(data.length < 4) {
|
||||
throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes");
|
||||
|
@ -64,7 +72,7 @@ public class PSBTEntry {
|
|||
}
|
||||
|
||||
public static String getMasterFingerprint(byte[] data) {
|
||||
return Hex.toHexString(data);
|
||||
return Utils.bytesToHex(data);
|
||||
}
|
||||
|
||||
public static List<ChildNumber> readBIP32Derivation(byte[] data) {
|
||||
|
@ -83,6 +91,117 @@ public class PSBTEntry {
|
|||
return path;
|
||||
}
|
||||
|
||||
public static byte[] serializeKeyDerivation(KeyDerivation keyDerivation) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] fingerprintBytes = Utils.hexToBytes(keyDerivation.getMasterFingerprint());
|
||||
if(fingerprintBytes.length != 4) {
|
||||
throw new IllegalArgumentException("Invalid number of fingerprint bytes: " + fingerprintBytes.length);
|
||||
}
|
||||
baos.writeBytes(fingerprintBytes);
|
||||
|
||||
for(ChildNumber childNumber : keyDerivation.getDerivation()) {
|
||||
byte[] indexBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(childNumber.i(), indexBytes, 0);
|
||||
baos.writeBytes(indexBytes);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
static PSBTEntry populateEntry(byte type, byte[] keydata, byte[] data) {
|
||||
return new PSBTEntry(new byte[] {type}, type, keydata, data);
|
||||
}
|
||||
|
||||
void serializeToStream(ByteArrayOutputStream baos) {
|
||||
int keyLen = 1;
|
||||
if(keyData != null) {
|
||||
keyLen += keyData.length;
|
||||
}
|
||||
|
||||
baos.writeBytes(writeCompactInt(keyLen));
|
||||
baos.writeBytes(key);
|
||||
if(keyData != null) {
|
||||
baos.writeBytes(keyData);
|
||||
}
|
||||
|
||||
baos.writeBytes(writeCompactInt(data.length));
|
||||
baos.writeBytes(data);
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public byte getKeyType() {
|
||||
return keyType;
|
||||
}
|
||||
|
||||
public byte[] getKeyData() {
|
||||
return keyData;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public static int readCompactInt(ByteBuffer psbtByteBuffer) throws PSBTParseException {
|
||||
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 PSBTParseException("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();
|
||||
}
|
||||
|
||||
private static void reverse(byte[] array) {
|
||||
for (int i = 0; i < array.length / 2; i++) {
|
||||
byte temp = array[i];
|
||||
|
|
|
@ -4,7 +4,6 @@ import com.sparrowwallet.drongo.KeyDerivation;
|
|||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -12,7 +11,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||
|
||||
public class PSBTInput {
|
||||
public static final byte PSBT_IN_NON_WITNESS_UTXO = 0x00;
|
||||
|
@ -85,7 +84,7 @@ public class PSBTInput {
|
|||
}
|
||||
for(TransactionOutput output: nonWitnessTx.getOutputs()) {
|
||||
try {
|
||||
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript());
|
||||
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Utils.bytesToHex(output.getScript().getProgram()) + " to script " + output.getScript());
|
||||
} catch(NonStandardScriptException e) {
|
||||
log.error("Unknown script type", e);
|
||||
}
|
||||
|
@ -102,7 +101,7 @@ public class PSBTInput {
|
|||
}
|
||||
this.witnessUtxo = witnessTxOutput;
|
||||
try {
|
||||
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()));
|
||||
log.debug("Found input witness utxo amount " + witnessTxOutput.getValue() + " script hex " + Utils.bytesToHex(witnessTxOutput.getScript().getProgram()) + " script " + witnessTxOutput.getScript() + " addresses " + Arrays.asList(witnessTxOutput.getScript().getToAddresses()));
|
||||
} catch(NonStandardScriptException e) {
|
||||
log.error("Unknown script type", e);
|
||||
}
|
||||
|
@ -113,7 +112,7 @@ public class PSBTInput {
|
|||
//TODO: Verify signature
|
||||
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(entry.getData(), true, false);
|
||||
this.partialSignatures.put(sigPublicKey, signature);
|
||||
log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData()));
|
||||
log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
case PSBT_IN_SIGHASH_TYPE:
|
||||
entry.checkOneByteKey();
|
||||
|
@ -138,11 +137,11 @@ public class PSBTInput {
|
|||
throw new PSBTParseException("PSBT provided a redeem script for a transaction output that does not need one");
|
||||
}
|
||||
if(!Arrays.equals(Utils.sha256hash160(redeemScript.getProgram()), scriptPubKey.getPubKeyHash())) {
|
||||
throw new PSBTParseException("Redeem script hash does not match transaction output script pubkey hash " + Hex.toHexString(scriptPubKey.getPubKeyHash()));
|
||||
throw new PSBTParseException("Redeem script hash does not match transaction output script pubkey hash " + Utils.bytesToHex(scriptPubKey.getPubKeyHash()));
|
||||
}
|
||||
|
||||
this.redeemScript = redeemScript;
|
||||
log.debug("Found input redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
|
||||
log.debug("Found input redeem script hex " + Utils.bytesToHex(redeemScript.getProgram()) + " script " + redeemScript);
|
||||
break;
|
||||
case PSBT_IN_WITNESS_SCRIPT:
|
||||
entry.checkOneByteKey();
|
||||
|
@ -156,10 +155,10 @@ public class PSBTInput {
|
|||
if(pubKeyHash == null) {
|
||||
throw new PSBTParseException("Witness script provided without P2WSH witness utxo or P2SH redeem script");
|
||||
} else if(!Arrays.equals(Sha256Hash.hash(witnessScript.getProgram()), pubKeyHash)) {
|
||||
throw new PSBTParseException("Witness script hash does not match provided pay to script hash " + Hex.toHexString(pubKeyHash));
|
||||
throw new PSBTParseException("Witness script hash does not match provided pay to script hash " + Utils.bytesToHex(pubKeyHash));
|
||||
}
|
||||
this.witnessScript = witnessScript;
|
||||
log.debug("Found input witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
|
||||
log.debug("Found input witness script hex " + Utils.bytesToHex(witnessScript.getProgram()) + " script " + witnessScript);
|
||||
break;
|
||||
case PSBT_IN_BIP32_DERIVATION:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
|
@ -172,7 +171,7 @@ public class PSBTInput {
|
|||
entry.checkOneByteKey();
|
||||
Script finalScriptSig = new Script(entry.getData());
|
||||
this.finalScriptSig = finalScriptSig;
|
||||
log.debug("Found input final scriptSig script hex " + Hex.toHexString(finalScriptSig.getProgram()) + " script " + finalScriptSig.toString());
|
||||
log.debug("Found input final scriptSig script hex " + Utils.bytesToHex(finalScriptSig.getProgram()) + " script " + finalScriptSig.toString());
|
||||
break;
|
||||
case PSBT_IN_FINAL_SCRIPTWITNESS:
|
||||
entry.checkOneByteKey();
|
||||
|
@ -187,8 +186,8 @@ public class PSBTInput {
|
|||
log.debug("Found input POR commitment message " + porMessage);
|
||||
break;
|
||||
case PSBT_IN_PROPRIETARY:
|
||||
this.proprietary.put(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
|
||||
log.debug("Found proprietary input " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
|
||||
this.proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
default:
|
||||
log.warn("PSBT input not recognized key type: " + entry.getKeyType());
|
||||
|
@ -199,6 +198,58 @@ public class PSBTInput {
|
|||
this.index = index;
|
||||
}
|
||||
|
||||
public List<PSBTEntry> getInputEntries() {
|
||||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
if(nonWitnessUtxo != null) {
|
||||
entries.add(populateEntry(PSBT_IN_NON_WITNESS_UTXO, null, nonWitnessUtxo.bitcoinSerialize()));
|
||||
}
|
||||
|
||||
if(witnessUtxo != null) {
|
||||
entries.add(populateEntry(PSBT_IN_WITNESS_UTXO, null, witnessUtxo.bitcoinSerialize()));
|
||||
}
|
||||
|
||||
for(Map.Entry<ECKey, TransactionSignature> entry : partialSignatures.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_PARTIAL_SIG, entry.getKey().getPubKey(), entry.getValue().encodeToBitcoin()));
|
||||
}
|
||||
|
||||
if(sigHash != null) {
|
||||
byte[] sigHashBytes = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(sigHash.intValue(), sigHashBytes, 0);
|
||||
entries.add(populateEntry(PSBT_IN_SIGHASH_TYPE, null, sigHashBytes));
|
||||
}
|
||||
|
||||
if(redeemScript != null) {
|
||||
entries.add(populateEntry(PSBT_IN_REDEEM_SCRIPT, null, redeemScript.getProgram()));
|
||||
}
|
||||
|
||||
if(witnessScript != null) {
|
||||
entries.add(populateEntry(PSBT_IN_WITNESS_SCRIPT, null, witnessScript.getProgram()));
|
||||
}
|
||||
|
||||
for(Map.Entry<ECKey, KeyDerivation> entry : derivedPublicKeys.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_BIP32_DERIVATION, entry.getKey().getPubKey(), serializeKeyDerivation(entry.getValue())));
|
||||
}
|
||||
|
||||
if(finalScriptSig != null) {
|
||||
entries.add(populateEntry(PSBT_IN_FINAL_SCRIPTSIG, null, finalScriptSig.getProgram()));
|
||||
}
|
||||
|
||||
if(finalScriptWitness != null) {
|
||||
entries.add(populateEntry(PSBT_IN_FINAL_SCRIPTWITNESS, null, finalScriptWitness.toByteArray()));
|
||||
}
|
||||
|
||||
if(porCommitment != null) {
|
||||
entries.add(populateEntry(PSBT_IN_POR_COMMITMENT, null, porCommitment.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : proprietary.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_IN_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public Transaction getNonWitnessUtxo() {
|
||||
return nonWitnessUtxo;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
package com.sparrowwallet.drongo.psbt;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation;
|
||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||
|
||||
public class PSBTOutput {
|
||||
public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00;
|
||||
|
@ -40,13 +41,13 @@ public class PSBTOutput {
|
|||
entry.checkOneByteKey();
|
||||
Script redeemScript = new Script(entry.getData());
|
||||
this.redeemScript = redeemScript;
|
||||
log.debug("Found output redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript);
|
||||
log.debug("Found output redeem script hex " + Utils.bytesToHex(redeemScript.getProgram()) + " script " + redeemScript);
|
||||
break;
|
||||
case PSBT_OUT_WITNESS_SCRIPT:
|
||||
entry.checkOneByteKey();
|
||||
Script witnessScript = new Script(entry.getData());
|
||||
this.witnessScript = witnessScript;
|
||||
log.debug("Found output witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript);
|
||||
log.debug("Found output witness script hex " + Utils.bytesToHex(witnessScript.getProgram()) + " script " + witnessScript);
|
||||
break;
|
||||
case PSBT_OUT_BIP32_DERIVATION:
|
||||
entry.checkOneBytePlusPubKey();
|
||||
|
@ -56,8 +57,8 @@ public class PSBTOutput {
|
|||
log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey);
|
||||
break;
|
||||
case PSBT_OUT_PROPRIETARY:
|
||||
proprietary.put(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData()));
|
||||
log.debug("Found proprietary output " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData()));
|
||||
proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
|
||||
log.debug("Found proprietary output " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
|
||||
break;
|
||||
default:
|
||||
log.warn("PSBT output not recognized key type: " + entry.getKeyType());
|
||||
|
@ -65,6 +66,28 @@ public class PSBTOutput {
|
|||
}
|
||||
}
|
||||
|
||||
public List<PSBTEntry> getOutputEntries() {
|
||||
List<PSBTEntry> entries = new ArrayList<>();
|
||||
|
||||
if(redeemScript != null) {
|
||||
entries.add(populateEntry(PSBT_OUT_REDEEM_SCRIPT, null, redeemScript.getProgram()));
|
||||
}
|
||||
|
||||
if(witnessScript != null) {
|
||||
entries.add(populateEntry(PSBT_OUT_WITNESS_SCRIPT, null, witnessScript.getProgram()));
|
||||
}
|
||||
|
||||
for(Map.Entry<ECKey, KeyDerivation> entry : derivedPublicKeys.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_OUT_BIP32_DERIVATION, entry.getKey().getPubKey(), serializeKeyDerivation(entry.getValue())));
|
||||
}
|
||||
|
||||
for(Map.Entry<String, String> entry : proprietary.entrySet()) {
|
||||
entries.add(populateEntry(PSBT_OUT_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public Script getRedeemScript() {
|
||||
return redeemScript;
|
||||
}
|
||||
|
|
|
@ -290,4 +290,23 @@ public class PSBTTest {
|
|||
Assert.assertEquals("2200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903", psbt1.getPsbtInputs().get(1).getFinalScriptSig().getProgramAsHex());
|
||||
Assert.assertEquals("0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae", Hex.toHexString(psbt1.getPsbtInputs().get(1).getFinalScriptWitness().toByteArray()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializeRoundTrip() throws PSBTParseException {
|
||||
String psbtStr1 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==";
|
||||
PSBT psbt1 = PSBT.fromString(psbtStr1);
|
||||
Assert.assertEquals(psbtStr1, psbt1.toBase64String());
|
||||
|
||||
String psbtStr2 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA";
|
||||
PSBT psbt2 = PSBT.fromString(psbtStr2);
|
||||
Assert.assertEquals(psbtStr2, psbt2.toBase64String());
|
||||
|
||||
String psbtStr3 = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
|
||||
PSBT psbt3 = PSBT.fromString(psbtStr3);
|
||||
Assert.assertEquals(psbtStr3, psbt3.toBase64String());
|
||||
|
||||
String psbtStr4 = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==";
|
||||
PSBT psbt4 = PSBT.fromString(psbtStr4);
|
||||
Assert.assertEquals(psbtStr4, psbt4.toBase64String());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue