refactor psbt classes and add serialization

This commit is contained in:
Craig Raw 2020-07-23 12:00:22 +02:00
parent f2ee57cc4d
commit 0cfe954463
7 changed files with 342 additions and 309 deletions

View file

@ -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;
}

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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];

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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());
}
}