From 0cfe954463ab5230fdb5bde4a7c4d90e4f035579 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 23 Jul 2020 12:00:22 +0200 Subject: [PATCH] refactor psbt classes and add serialization --- .../drongo/protocol/SigHash.java | 4 + .../drongo/protocol/TransactionOutput.java | 13 + .../com/sparrowwallet/drongo/psbt/PSBT.java | 322 ++++-------------- .../sparrowwallet/drongo/psbt/PSBTEntry.java | 183 ++++++++-- .../sparrowwallet/drongo/psbt/PSBTInput.java | 75 +++- .../sparrowwallet/drongo/psbt/PSBTOutput.java | 35 +- .../sparrowwallet/drongo/psbt/PSBTTest.java | 19 ++ 7 files changed, 342 insertions(+), 309 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/SigHash.java b/src/main/java/com/sparrowwallet/drongo/protocol/SigHash.java index eb041ee..97ea175 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/SigHash.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/SigHash.java @@ -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; } diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java index 32559bc..d4519d7 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionOutput.java @@ -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. diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index 0aed3c8..7521b08 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -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 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 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 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 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 entries) { Set 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 getGlobalEntries() { + List entries = new ArrayList<>(); + if(transaction != null) { + entries.add(populateEntry(PSBT_GLOBAL_UNSIGNED_TX, null, transaction.bitcoinSerialize())); + } + + for(Map.Entry 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 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 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 inputEntries = psbtInput.getInputEntries(); + for(PSBTEntry entry : inputEntries) { + entry.serializeToStream(baos); + } + baos.writeBytes(new byte[] {(byte)0x00}); + } - psbtBytes = baos.toByteArray(); + for(PSBTOutput psbtOutput : getPsbtOutputs()) { + List outputEntries = psbtOutput.getOutputEntries(); + for(PSBTEntry entry : outputEntries) { + entry.serializeToStream(baos); + } + baos.writeBytes(new byte[] {(byte)0x00}); + } - return psbtBytes; + return baos.toByteArray(); } public List 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); - } } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java index 3c38cfb..2f80dbe 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java @@ -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 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]; diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index db03ef9..9a1ea27 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -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 getInputEntries() { + List 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 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 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 entry : proprietary.entrySet()) { + entries.add(populateEntry(PSBT_IN_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue()))); + } + + return entries; + } + public Transaction getNonWitnessUtxo() { return nonWitnessUtxo; } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java index d259432..a1c11a6 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java @@ -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 getOutputEntries() { + List 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 entry : derivedPublicKeys.entrySet()) { + entries.add(populateEntry(PSBT_OUT_BIP32_DERIVATION, entry.getKey().getPubKey(), serializeKeyDerivation(entry.getValue()))); + } + + for(Map.Entry entry : proprietary.entrySet()) { + entries.add(populateEntry(PSBT_OUT_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue()))); + } + + return entries; + } + public Script getRedeemScript() { return redeemScript; } diff --git a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java index 3a0eff2..904dc0f 100644 --- a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java +++ b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java @@ -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()); + } }