diff --git a/src/main/java/com/craigraw/drongo/protocol/Script.java b/src/main/java/com/craigraw/drongo/protocol/Script.java index 409c688..d79ab9b 100644 --- a/src/main/java/com/craigraw/drongo/protocol/Script.java +++ b/src/main/java/com/craigraw/drongo/protocol/Script.java @@ -2,6 +2,7 @@ package com.craigraw.drongo.protocol; import com.craigraw.drongo.Utils; import com.craigraw.drongo.address.*; +import org.bouncycastle.util.encoders.Hex; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -101,6 +102,10 @@ public class Script { } } + public String getProgramAsHex() { + return Hex.toHexString(getProgram()); + } + /** * Returns true if this script has the required form to contain a destination address */ diff --git a/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java b/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java index 947336a..1fd30a5 100644 --- a/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java +++ b/src/main/java/com/craigraw/drongo/protocol/TransactionWitness.java @@ -1,10 +1,12 @@ package com.craigraw.drongo.protocol; import com.craigraw.drongo.Utils; +import org.bouncycastle.util.encoders.Hex; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class TransactionWitness { @@ -35,4 +37,42 @@ public class TransactionWitness { stream.write(push); } } + + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + for (byte[] push : pushes) { + if (push == null) { + buffer.append("NULL"); + } else if (push.length == 0) { + buffer.append("EMPTY"); + } else { + buffer.append(Hex.toHexString(push)); + } + buffer.append(" "); + } + + return buffer.toString().trim(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransactionWitness other = (TransactionWitness) o; + if (pushes.size() != other.pushes.size()) return false; + for (int i = 0; i < pushes.size(); i++) { + if (!Arrays.equals(pushes.get(i), other.pushes.get(i))) return false; + } + return true; + } + + @Override + public int hashCode() { + int hashCode = 1; + for (byte[] push : pushes) { + hashCode = 31 * hashCode + (push == null ? 0 : Arrays.hashCode(push)); + } + return hashCode; + } } diff --git a/src/main/java/com/craigraw/drongo/psbt/PSBT.java b/src/main/java/com/craigraw/drongo/psbt/PSBT.java index fd10eb0..b4fee56 100644 --- a/src/main/java/com/craigraw/drongo/psbt/PSBT.java +++ b/src/main/java/com/craigraw/drongo/psbt/PSBT.java @@ -3,9 +3,6 @@ package com.craigraw.drongo.psbt; import com.craigraw.drongo.ExtendedPublicKey; import com.craigraw.drongo.KeyDerivation; import com.craigraw.drongo.Utils; -import com.craigraw.drongo.crypto.ChildNumber; -import com.craigraw.drongo.crypto.ECKey; -import com.craigraw.drongo.crypto.LazyECPoint; import com.craigraw.drongo.protocol.*; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; @@ -17,29 +14,14 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.*; +import static com.craigraw.drongo.psbt.PSBTEntry.parseKeyDerivation; + public class PSBT { public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00; public static final byte PSBT_GLOBAL_BIP32_PUBKEY = 0x01; public static final byte PSBT_GLOBAL_VERSION = (byte)0xfb; public static final byte PSBT_GLOBAL_PROPRIETARY = (byte)0xfc; - public static final byte PSBT_IN_NON_WITNESS_UTXO = 0x00; - public static final byte PSBT_IN_WITNESS_UTXO = 0x01; - public static final byte PSBT_IN_PARTIAL_SIG = 0x02; - public static final byte PSBT_IN_SIGHASH_TYPE = 0x03; - public static final byte PSBT_IN_REDEEM_SCRIPT = 0x04; - public static final byte PSBT_IN_WITNESS_SCRIPT = 0x05; - public static final byte PSBT_IN_BIP32_DERIVATION = 0x06; - public static final byte PSBT_IN_FINAL_SCRIPTSIG = 0x07; - public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08; - public static final byte PSBT_IN_POR_COMMITMENT = 0x09; - public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc; - - public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00; - public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01; - public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02; - public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc; - public static final String PSBT_MAGIC = "70736274"; private static final int STATE_GLOBALS = 1; @@ -51,11 +33,8 @@ public class PSBT { private int inputs = 0; private int outputs = 0; - private boolean parseOK = false; - private String strPSBT = null; - private byte[] psbtBytes = null; - private ByteBuffer psbtByteBuffer = null; + private byte[] psbtBytes; private Transaction transaction = null; private Integer version = null; @@ -67,270 +46,120 @@ public class PSBT { private static final Logger log = LoggerFactory.getLogger(PSBT.class); - public PSBT(String strPSBT) throws Exception { - if (!isPSBT(strPSBT)) { - log.debug("Provided string is not a PSBT"); - return; - } - - if (Utils.isBase64(strPSBT) && !Utils.isHex(strPSBT)) { - this.strPSBT = Hex.toHexString(Base64.decode(strPSBT)); - } else { - this.strPSBT = strPSBT; - } - - psbtBytes = Hex.decode(this.strPSBT); - psbtByteBuffer = ByteBuffer.wrap(psbtBytes); - - read(); + public PSBT(byte[] psbt) { + this.psbtBytes = psbt; + parse(); } - public PSBT(byte[] psbt) throws Exception { - this(Hex.toHexString(psbt)); - } - - public void read() throws Exception { + private void parse() { int seenInputs = 0; int seenOutputs = 0; - psbtBytes = Hex.decode(strPSBT); - psbtByteBuffer = ByteBuffer.wrap(psbtBytes); - - log.debug("--- ***** START ***** ---"); - log.debug("--- PSBT length:" + psbtBytes.length + "---"); - log.debug("--- parsing header ---"); + ByteBuffer psbtByteBuffer = ByteBuffer.wrap(psbtBytes); byte[] magicBuf = new byte[4]; psbtByteBuffer.get(magicBuf); - if (!PSBT.PSBT_MAGIC.equalsIgnoreCase(Hex.toHexString(magicBuf))) { - throw new Exception("Invalid magic value"); + if (!PSBT_MAGIC.equalsIgnoreCase(Hex.toHexString(magicBuf))) { + throw new IllegalStateException("PSBT has invalid magic value"); } byte sep = psbtByteBuffer.get(); if (sep != (byte) 0xff) { - throw new Exception("Bad 0xff separator:" + Hex.toHexString(new byte[]{sep})); + throw new IllegalStateException("PSBT has bad initial separator:" + Hex.toHexString(new byte[]{sep})); } int currentState = STATE_GLOBALS; - PSBTInput currentInput = new PSBTInput(); - PSBTOutput currentOutput = new PSBTOutput(); + List globalEntries = new ArrayList<>(); + List> inputEntryLists = new ArrayList<>(); + List> outputEntryLists = new ArrayList<>(); + + List inputEntries = new ArrayList<>(); + List outputEntries = new ArrayList<>(); while (psbtByteBuffer.hasRemaining()) { - if (currentState == STATE_GLOBALS) { - log.debug("--- parsing globals ---"); - } else if (currentState == STATE_INPUTS) { - log.debug("--- parsing inputs ---"); - } else if (currentState == STATE_OUTPUTS) { - log.debug("--- parsing outputs ---"); - } + PSBTEntry entry = parseEntry(psbtByteBuffer); - PSBTEntry entry = parse(); - if (entry == null) { - log.debug("PSBT parse returned null entry"); - } - - if (entry.getKey() == null) { // length == 0 + if(entry.getKey() == null) { // length == 0 switch (currentState) { case STATE_GLOBALS: currentState = STATE_INPUTS; + parseGlobalEntries(globalEntries); break; case STATE_INPUTS: - psbtInputs.add(currentInput); - currentInput = new PSBTInput(); + inputEntryLists.add(inputEntries); + inputEntries = new ArrayList<>(); seenInputs++; if (seenInputs == inputs) { currentState = STATE_OUTPUTS; + parseInputEntries(inputEntryLists); } break; case STATE_OUTPUTS: - psbtOutputs.add(currentOutput); - currentOutput = new PSBTOutput(); + outputEntryLists.add(outputEntries); + outputEntries = new ArrayList<>(); seenOutputs++; if (seenOutputs == outputs) { currentState = STATE_END; + parseOutputEntries(outputEntryLists); } break; case STATE_END: - parseOK = true; break; default: - log.debug("PSBT read is in unknown state"); - break; + throw new IllegalStateException("PSBT read is in unknown state"); } } else if (currentState == STATE_GLOBALS) { - switch (entry.getKeyType()[0]) { - case PSBT.PSBT_GLOBAL_UNSIGNED_TX: - Transaction transaction = new Transaction(entry.getData()); - inputs = transaction.getInputs().size(); - outputs = transaction.getOutputs().size(); - log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime()); - for(TransactionInput input: transaction.getInputs()) { - log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript()); - } - for(TransactionOutput output: transaction.getOutputs()) { - log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript()); - } - setTransaction(transaction); - break; - case PSBT.PSBT_GLOBAL_BIP32_PUBKEY: - KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); - ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivationPath(), Base58.encodeChecked(entry.getKeyData()), null); - addExtendedPublicKey(pubKey, keyDerivation); - log.debug("Pubkey with master fingerprint " + pubKey.getMasterFingerprint() + " at path " + pubKey.getKeyDerivationPath() + ": " + pubKey.getExtendedPublicKey()); - break; - case PSBT.PSBT_GLOBAL_VERSION: - int version = (int)Utils.readUint32(entry.getData(), 0); - setVersion(version); - log.debug("PSBT version: " + version); - break; - case PSBT.PSBT_GLOBAL_PROPRIETARY: - addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData())); - log.debug("PSBT global proprietary data: " + Hex.toHexString(entry.getData())); - break; - default: - log.debug("PSBT global not recognized key type: " + entry.getKeyType()[0]); - break; - } + globalEntries.add(entry); } else if (currentState == STATE_INPUTS) { - switch (entry.getKeyType()[0]) { - case PSBT.PSBT_IN_NON_WITNESS_UTXO: - Transaction nonWitnessTx = new Transaction(entry.getData()); - currentInput.setNonWitnessUtxo(nonWitnessTx); - log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLockTime()); - for(TransactionInput input: nonWitnessTx.getInputs()) { - log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript()); - } - for(TransactionOutput output: nonWitnessTx.getOutputs()) { - log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript()); - } - break; - case PSBT.PSBT_IN_WITNESS_UTXO: - TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0); - currentInput.setWitnessUtxo(witnessTxOutput); - log.debug("Found input witness utxo amount " + witnessTxOutput.getValue() + " script hex " + Hex.toHexString(witnessTxOutput.getScript().getProgram()) + " script " + witnessTxOutput.getScript() + " addresses " + Arrays.asList(witnessTxOutput.getScript().getToAddresses())); - break; - case PSBT.PSBT_IN_PARTIAL_SIG: - LazyECPoint sigPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); - currentInput.addPartialSignature(sigPublicKey, entry.getData()); - log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData())); - break; - case PSBT.PSBT_IN_SIGHASH_TYPE: - long sighashType = Utils.readUint32(entry.getData(), 0); - Transaction.SigHash sigHash = Transaction.SigHash.fromInt((int)sighashType); - currentInput.setSigHash(sigHash); - log.debug("Found input sighash_type " + sigHash.toString()); - break; - case PSBT.PSBT_IN_REDEEM_SCRIPT: - Script redeemScript = new Script(entry.getData()); - currentInput.setRedeemScript(redeemScript); - log.debug("Found input redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript); - break; - case PSBT.PSBT_IN_WITNESS_SCRIPT: - Script witnessScript = new Script(entry.getData()); - currentInput.setWitnessScript(witnessScript); - log.debug("Found input witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript); - break; - case PSBT.PSBT_IN_BIP32_DERIVATION: - LazyECPoint derivedPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); - KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); - currentInput.addDerivedPublicKey(derivedPublicKey, keyDerivation); - log.debug("Found input bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey); - break; - case PSBT.PSBT_IN_FINAL_SCRIPTSIG: - Script finalScriptSig = new Script(entry.getData()); - currentInput.setFinalScriptSig(finalScriptSig); - log.debug("Found input final scriptSig script hex " + Hex.toHexString(finalScriptSig.getProgram()) + " script " + finalScriptSig.toString()); - break; - case PSBT.PSBT_IN_FINAL_SCRIPTWITNESS: - Script finalScriptWitness = new Script(entry.getData()); - currentInput.setFinalScriptWitness(finalScriptWitness); - log.debug("Found input final scriptWitness script hex " + Hex.toHexString(finalScriptWitness.getProgram()) + " script " + finalScriptWitness.toString()); - break; - case PSBT.PSBT_IN_POR_COMMITMENT: - String porMessage = new String(entry.getData(), "UTF-8"); - currentInput.setPorCommitment(porMessage); - log.debug("Found input POR commitment message " + porMessage); - break; - case PSBT.PSBT_IN_PROPRIETARY: - currentInput.addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData())); - log.debug("Found proprietary input " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData())); - break; - default: - log.debug("PSBT input not recognized key type:" + entry.getKeyType()[0]); - break; - } + inputEntries.add(entry); } else if (currentState == STATE_OUTPUTS) { - switch (entry.getKeyType()[0]) { - case PSBT.PSBT_OUT_REDEEM_SCRIPT: - Script redeemScript = new Script(entry.getData()); - currentOutput.setRedeemScript(redeemScript); - log.debug("Found output redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript); - break; - case PSBT.PSBT_OUT_WITNESS_SCRIPT: - Script witnessScript = new Script(entry.getData()); - currentOutput.setWitnessScript(witnessScript); - log.debug("Found output witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript); - break; - case PSBT.PSBT_OUT_BIP32_DERIVATION: - LazyECPoint publicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); - KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); - currentOutput.addDerivedPublicKey(publicKey, keyDerivation); - log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + publicKey); - break; - case PSBT.PSBT_OUT_PROPRIETARY: - currentOutput.addProprietary(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData())); - log.debug("Found proprietary output " + Hex.toHexString(entry.getKeyData()) + ": " + Hex.toHexString(entry.getData())); - break; - default: - log.debug("PSBT output not recognized key type:" + entry.getKeyType()[0]); - break; - } + outputEntries.add(entry); } else { - log.debug("PSBT structure invalid"); + throw new IllegalStateException("PSBT structure invalid"); } - } - if (currentState == STATE_END) { - log.debug("--- ***** END ***** ---"); + if(currentState != STATE_END) { + if(transaction == null) { + throw new IllegalStateException("Missing transaction"); + } + + if(currentState == STATE_INPUTS) { + throw new IllegalStateException("Missing inputs"); + } + + if(currentState == STATE_OUTPUTS) { + throw new IllegalStateException("Missing outputs"); + } } } - private PSBTEntry parse() { + private PSBTEntry parseEntry(ByteBuffer psbtByteBuffer) { PSBTEntry entry = new PSBTEntry(); try { int keyLen = PSBT.readCompactInt(psbtByteBuffer); - log.debug("PSBT entry key length: " + keyLen); if (keyLen == 0x00) { - log.debug("PSBT entry separator 0x00"); return entry; } byte[] key = new byte[keyLen]; psbtByteBuffer.get(key); - log.debug("PSBT entry key: " + Hex.toHexString(key)); - byte[] keyType = new byte[1]; - keyType[0] = key[0]; - log.debug("PSBT entry key type: " + Hex.toHexString(keyType)); + byte keyType = key[0]; byte[] keyData = null; if (key.length > 1) { keyData = new byte[key.length - 1]; System.arraycopy(key, 1, keyData, 0, keyData.length); - log.debug("PSBT entry key data: " + Hex.toHexString(keyData)); } int dataLen = PSBT.readCompactInt(psbtByteBuffer); - log.debug("PSBT entry data length: " + dataLen); - byte[] data = new byte[dataLen]; psbtByteBuffer.get(data); - log.debug("PSBT entry data: " + Hex.toHexString(data)); entry.setKey(key); entry.setKeyType(keyType); @@ -340,14 +169,13 @@ public class PSBT { return entry; } catch (Exception e) { - log.debug("Error parsing PSBT entry", e); - return null; + throw new IllegalStateException("Error parsing PSBT entry", e); } } private PSBTEntry populateEntry(byte type, byte[] keydata, byte[] data) throws Exception { PSBTEntry entry = new PSBTEntry(); - entry.setKeyType(new byte[]{type}); + entry.setKeyType(type); entry.setKey(new byte[]{type}); if (keydata != null) { entry.setKeyData(keydata); @@ -357,6 +185,89 @@ public class PSBT { return entry; } + private void parseGlobalEntries(List globalEntries) { + PSBTEntry duplicate = findDuplicateKey(globalEntries); + if(duplicate != null) { + throw new IllegalStateException("Found duplicate key for PSBT global: " + Hex.toHexString(duplicate.getKey())); + } + + for(PSBTEntry entry : globalEntries) { + switch(entry.getKeyType()) { + case PSBT_GLOBAL_UNSIGNED_TX: + entry.checkOneByteKey(); + Transaction transaction = new Transaction(entry.getData()); + inputs = transaction.getInputs().size(); + outputs = transaction.getOutputs().size(); + log.debug("Transaction with txid: " + transaction.getTxId() + " version " + transaction.getVersion() + " size " + transaction.getMessageSize() + " locktime " + transaction.getLockTime()); + for(TransactionInput input: transaction.getInputs()) { + if(input.getScript().getProgram().length != 0) { + throw new IllegalStateException("Unsigned tx input does not have empty scriptSig"); + } + log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript()); + } + for(TransactionOutput output: transaction.getOutputs()) { + log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript()); + } + this.transaction = transaction; + break; + case PSBT_GLOBAL_BIP32_PUBKEY: + entry.checkOneBytePlusXpubKey(); + KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); + ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivationPath(), Base58.encodeChecked(entry.getKeyData()), null); + this.extendedPublicKeys.put(pubKey, keyDerivation); + log.debug("Pubkey with master fingerprint " + pubKey.getMasterFingerprint() + " at path " + pubKey.getKeyDerivationPath() + ": " + pubKey.getExtendedPublicKey()); + break; + case PSBT_GLOBAL_VERSION: + entry.checkOneByteKey(); + int version = (int)Utils.readUint32(entry.getData(), 0); + this.version = version; + 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())); + break; + default: + throw new IllegalStateException("PSBT global not recognized key type: " + entry.getKeyType()); + } + } + } + + private void parseInputEntries(List> inputEntryLists) { + for(List inputEntries : inputEntryLists) { + PSBTEntry duplicate = findDuplicateKey(inputEntries); + if(duplicate != null) { + throw new IllegalStateException("Found duplicate key for PSBT input: " + Hex.toHexString(duplicate.getKey())); + } + + PSBTInput input = new PSBTInput(inputEntries); + this.psbtInputs.add(input); + } + } + + private void parseOutputEntries(List> outputEntryLists) { + for(List outputEntries : outputEntryLists) { + PSBTEntry duplicate = findDuplicateKey(outputEntries); + if(duplicate != null) { + throw new IllegalStateException("Found duplicate key for PSBT output: " + Hex.toHexString(duplicate.getKey())); + } + + PSBTOutput output = new PSBTOutput(outputEntries); + this.psbtOutputs.add(output); + } + } + + private PSBTEntry findDuplicateKey(List entries) { + Set checkSet = new HashSet(); + for(PSBTEntry entry: entries) { + if(!checkSet.add(Hex.toHexString(entry.getKey())) ) { + return entry; + } + } + + return null; + } + public byte[] serialize() throws IOException { ByteArrayOutputStream transactionbaos = new ByteArrayOutputStream(); transaction.bitcoinSerialize(transactionbaos); @@ -365,7 +276,7 @@ public class PSBT { ByteArrayOutputStream baos = new ByteArrayOutputStream(); // magic - baos.write(Hex.decode(PSBT.PSBT_MAGIC), 0, Hex.decode(PSBT.PSBT_MAGIC).length); + baos.write(Hex.decode(PSBT_MAGIC), 0, Hex.decode(PSBT_MAGIC).length); // separator baos.write((byte) 0xff); @@ -412,8 +323,6 @@ public class PSBT { baos.write((byte) 0x00); psbtBytes = baos.toByteArray(); - strPSBT = Hex.toHexString(psbtBytes); - log.debug("Wrote PSBT: " + strPSBT); return psbtBytes; } @@ -430,20 +339,10 @@ public class PSBT { return transaction; } - public void setTransaction(Transaction transaction) { - testIfNull(this.transaction); - this.transaction = transaction; - } - public Integer getVersion() { return version; } - public void setVersion(Integer version) { - testIfNull(this.version); - this.version = version; - } - public KeyDerivation getKeyDerivation(ExtendedPublicKey publicKey) { return extendedPublicKeys.get(publicKey); } @@ -452,24 +351,6 @@ public class PSBT { return new ArrayList(extendedPublicKeys.keySet()); } - public void addExtendedPublicKey(ExtendedPublicKey publicKey, KeyDerivation derivation) { - if(extendedPublicKeys.containsKey(publicKey)) { - throw new IllegalStateException("Duplicate public key in scope"); - } - - this.extendedPublicKeys.put(publicKey, derivation); - } - - public void addProprietary(String key, String data) { - globalProprietary.put(key, data); - } - - private void testIfNull(Object obj) { - if(obj != null) { - throw new IllegalStateException("Duplicate keys in scope"); - } - } - public String toString() { try { return Hex.toHexString(serialize()); @@ -558,41 +439,6 @@ public class PSBT { return ret; } - public KeyDerivation parseKeyDerivation(byte[] data) { - String masterFingerprint = getMasterFingerprint(Arrays.copyOfRange(data, 0, 4)); - List bip32pathList = readBIP32Derivation(Arrays.copyOfRange(data, 4, data.length)); - String bip32path = KeyDerivation.writePath(bip32pathList); - return new KeyDerivation(masterFingerprint, bip32path); - } - - public static String getMasterFingerprint(byte[] data) { - return Hex.toHexString(data); - } - - public static List readBIP32Derivation(byte[] data) { - List path = new ArrayList<>(); - - ByteBuffer bb = ByteBuffer.wrap(data); - byte[] buf = new byte[4]; - - do { - bb.get(buf); - reverse(buf); - ByteBuffer pbuf = ByteBuffer.wrap(buf); - path.add(new ChildNumber(pbuf.getInt())); - } while(bb.hasRemaining()); - - return path; - } - - private static void reverse(byte[] array) { - for (int i = 0; i < array.length / 2; i++) { - byte temp = array[i]; - array[i] = array[array.length - i - 1]; - array[array.length - i - 1] = temp; - } - } - public static byte[] writeBIP32Derivation(byte[] fingerprint, int purpose, int type, int account, int chain, int index) { // fingerprint and integer values to BIP32 derivation buffer byte[] bip32buf = new byte[24]; @@ -634,17 +480,30 @@ public class PSBT { } public static boolean isPSBT(String s) { - if (Utils.isHex(s) && s.startsWith(PSBT.PSBT_MAGIC)) { + if (Utils.isHex(s) && s.startsWith(PSBT_MAGIC)) { return true; - } else if (Utils.isBase64(s) && Hex.toHexString(Base64.decode(s)).startsWith(PSBT.PSBT_MAGIC)) { + } else if (Utils.isBase64(s) && Hex.toHexString(Base64.decode(s)).startsWith(PSBT_MAGIC)) { return true; } else { return false; } } + public static PSBT fromString(String strPSBT) { + if (!isPSBT(strPSBT)) { + throw new IllegalArgumentException("Provided string is not a PSBT"); + } + + if (Utils.isBase64(strPSBT) && !Utils.isHex(strPSBT)) { + strPSBT = Hex.toHexString(Base64.decode(strPSBT)); + } + + byte[] psbtBytes = Hex.decode(strPSBT); + return new PSBT(psbtBytes); + } + public static void main(String[] args) throws Exception { - String psbtBase64 = "cHNidP8BAMkCAAAAA3lxWr8zSZt5tiGZegyFWmd8b62cew6qi/4rTZGGif8OAAAAAAD/////td4T4zmwdQ8R2SbwRjRj+alAy1VX8mYZD2o9ZmefNIsAAAAAAP////+k9Xvvp9Lpap1TWd51NWu+MIfojG+MCqmguPyjII+5YgAAAAAA/////wKMz/AIAAAAABl2qRSE7GtWKUoaFcVQ8n9qfMYi41Yh0YisjM/wCAAAAAAZdqkUmka3O8TiIRG8h+a1mDLFQVTfJEiIrAAAAAAAAQBVAgAAAAGt3gAAAAAAAO++AAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAAAAA/////wEA4fUFAAAAABl2qRSvQiRNb8B3El3G+KdspA3+DRvH1IisAAAAACIGA383lPO+TErMCGrITWkCwCVxPqv4iQ8g9ErPCzTjwPD3DHSXSzsAAAAAAAAAAAABAFUCAAAAAa3eAAAAAAAA774AAAAAAAAAAAAAAAAAAAAAAAAAAAAASQAAAAD/////AQDh9QUAAAAAGXapFAn8nw1IXPh34v8wuhJrcu34Xg8qiKwAAAAAIgYDTr6iJ7sP/u+0gz4wi+Muuc4IxEoJaGYedN/uqwmSfbgMdJdLOwAAAAABAAAAAAEAVQIAAAABrd4AAAAAAADvvgAAAAAAAAAAAAAAAAAAAAAAAAAAAABJAAAAAP////8BAOH1BQAAAAAZdqkUGMIzFJsgyFIYzDbThZ5S2zTnvRiIrAAAAAAiBgK7oYu+Z/kEK6XK3urdEDW2ngkwnXD1gZBjEgRW0wD7Igx0l0s7AAAAAAIAAAAAACICAyw+nsM8JYHohVqRsQ2qilEwjZPh+OkGPqkO2kYZczCZEHSXSzsMAAAAIgAAADcBAAAA"; + String psbtBase64 = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA"; PSBT psbt = null; String filename = "default.psbt"; @@ -656,7 +515,7 @@ public class PSBT { stream.close(); psbt = new PSBT(psbtBytes); } else { - psbt = new PSBT(psbtBase64); + psbt = PSBT.fromString(psbtBase64); } System.out.println(psbt); diff --git a/src/main/java/com/craigraw/drongo/psbt/PSBTEntry.java b/src/main/java/com/craigraw/drongo/psbt/PSBTEntry.java index 6b0e7f1..1e4a161 100644 --- a/src/main/java/com/craigraw/drongo/psbt/PSBTEntry.java +++ b/src/main/java/com/craigraw/drongo/psbt/PSBTEntry.java @@ -1,8 +1,17 @@ package com.craigraw.drongo.psbt; +import com.craigraw.drongo.KeyDerivation; +import com.craigraw.drongo.crypto.ChildNumber; +import org.bouncycastle.util.encoders.Hex; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + public class PSBTEntry { private byte[] key = null; - private byte[] keyType = null; + private byte keyType; private byte[] keyData = null; private byte[] data = null; @@ -14,11 +23,11 @@ public class PSBTEntry { this.key = key; } - public byte[] getKeyType() { + public byte getKeyType() { return keyType; } - public void setKeyType(byte[] keyType) { + public void setKeyType(byte keyType) { this.keyType = keyType; } @@ -37,4 +46,57 @@ public class PSBTEntry { public void setData(byte[] data) { this.data = data; } + + public static KeyDerivation parseKeyDerivation(byte[] data) { + String masterFingerprint = getMasterFingerprint(Arrays.copyOfRange(data, 0, 4)); + List bip32pathList = readBIP32Derivation(Arrays.copyOfRange(data, 4, data.length)); + String bip32path = KeyDerivation.writePath(bip32pathList); + return new KeyDerivation(masterFingerprint, bip32path); + } + + public static String getMasterFingerprint(byte[] data) { + return Hex.toHexString(data); + } + + public static List readBIP32Derivation(byte[] data) { + List path = new ArrayList<>(); + + ByteBuffer bb = ByteBuffer.wrap(data); + byte[] buf = new byte[4]; + + do { + bb.get(buf); + reverse(buf); + ByteBuffer pbuf = ByteBuffer.wrap(buf); + path.add(new ChildNumber(pbuf.getInt())); + } while(bb.hasRemaining()); + + return path; + } + + private static void reverse(byte[] array) { + for (int i = 0; i < array.length / 2; i++) { + byte temp = array[i]; + array[i] = array[array.length - i - 1]; + array[array.length - i - 1] = temp; + } + } + + public void checkOneByteKey() { + if(this.getKey().length != 1) { + throw new IllegalStateException("PSBT key type must be one byte"); + } + } + + public void checkOneBytePlusXpubKey() { + if(this.getKey().length != 79) { + throw new IllegalStateException("PSBT key type must be one byte"); + } + } + + public void checkOneBytePlusPubKey() { + if(this.getKey().length != 34) { + throw new IllegalStateException("PSBT key type must be one byte"); + } + } } diff --git a/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java b/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java index 767b7f5..045c193 100644 --- a/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/craigraw/drongo/psbt/PSBTInput.java @@ -1,15 +1,38 @@ package com.craigraw.drongo.psbt; import com.craigraw.drongo.KeyDerivation; +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.crypto.ECKey; import com.craigraw.drongo.crypto.LazyECPoint; import com.craigraw.drongo.protocol.Script; import com.craigraw.drongo.protocol.Transaction; +import com.craigraw.drongo.protocol.TransactionInput; import com.craigraw.drongo.protocol.TransactionOutput; +import org.bouncycastle.util.encoders.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import static com.craigraw.drongo.psbt.PSBTEntry.parseKeyDerivation; + public class PSBTInput { + public static final byte PSBT_IN_NON_WITNESS_UTXO = 0x00; + public static final byte PSBT_IN_WITNESS_UTXO = 0x01; + public static final byte PSBT_IN_PARTIAL_SIG = 0x02; + public static final byte PSBT_IN_SIGHASH_TYPE = 0x03; + public static final byte PSBT_IN_REDEEM_SCRIPT = 0x04; + public static final byte PSBT_IN_WITNESS_SCRIPT = 0x05; + public static final byte PSBT_IN_BIP32_DERIVATION = 0x06; + public static final byte PSBT_IN_FINAL_SCRIPTSIG = 0x07; + public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08; + public static final byte PSBT_IN_POR_COMMITMENT = 0x09; + public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc; + private Transaction nonWitnessUtxo; private TransactionOutput witnessUtxo; private Map partialSignatures = new LinkedHashMap<>(); @@ -22,109 +45,138 @@ public class PSBTInput { private String porCommitment; private Map proprietary = new LinkedHashMap<>(); - public Transaction getNonWitnessUtxo() { - return nonWitnessUtxo; + private static final Logger log = LoggerFactory.getLogger(PSBTInput.class); + + PSBTInput(List inputEntries) { + for(PSBTEntry entry : inputEntries) { + switch(entry.getKeyType()) { + case PSBT_IN_NON_WITNESS_UTXO: + entry.checkOneByteKey(); + Transaction nonWitnessTx = new Transaction(entry.getData()); + this.nonWitnessUtxo = nonWitnessTx; + log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLockTime()); + for(TransactionInput input: nonWitnessTx.getInputs()) { + log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScript()); + } + for(TransactionOutput output: nonWitnessTx.getOutputs()) { + log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Hex.toHexString(output.getScript().getProgram()) + " to script " + output.getScript()); + } + break; + case PSBT_IN_WITNESS_UTXO: + entry.checkOneByteKey(); + TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0); + this.witnessUtxo = witnessTxOutput; + log.debug("Found input witness utxo amount " + witnessTxOutput.getValue() + " script hex " + Hex.toHexString(witnessTxOutput.getScript().getProgram()) + " script " + witnessTxOutput.getScript() + " addresses " + Arrays.asList(witnessTxOutput.getScript().getToAddresses())); + break; + case PSBT_IN_PARTIAL_SIG: + entry.checkOneBytePlusPubKey(); + LazyECPoint sigPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); + this.partialSignatures.put(sigPublicKey, entry.getData()); + log.debug("Found input partial signature with public key " + sigPublicKey + " signature " + Hex.toHexString(entry.getData())); + break; + case PSBT_IN_SIGHASH_TYPE: + entry.checkOneByteKey(); + long sighashType = Utils.readUint32(entry.getData(), 0); + Transaction.SigHash sigHash = Transaction.SigHash.fromInt((int)sighashType); + this.sigHash = sigHash; + log.debug("Found input sighash_type " + sigHash.toString()); + break; + case PSBT_IN_REDEEM_SCRIPT: + entry.checkOneByteKey(); + Script redeemScript = new Script(entry.getData()); + this.redeemScript = redeemScript; + log.debug("Found input redeem script hex " + Hex.toHexString(redeemScript.getProgram()) + " script " + redeemScript); + break; + case PSBT_IN_WITNESS_SCRIPT: + entry.checkOneByteKey(); + Script witnessScript = new Script(entry.getData()); + this.witnessScript = witnessScript; + log.debug("Found input witness script hex " + Hex.toHexString(witnessScript.getProgram()) + " script " + witnessScript); + break; + case PSBT_IN_BIP32_DERIVATION: + entry.checkOneBytePlusPubKey(); + LazyECPoint derivedPublicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); + KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); + this.derivedPublicKeys.put(derivedPublicKey, keyDerivation); + log.debug("Found input bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey); + break; + case PSBT_IN_FINAL_SCRIPTSIG: + 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()); + break; + case PSBT_IN_FINAL_SCRIPTWITNESS: + entry.checkOneByteKey(); + Script finalScriptWitness = new Script(entry.getData()); + this.finalScriptWitness = finalScriptWitness; + log.debug("Found input final scriptWitness script hex " + Hex.toHexString(finalScriptWitness.getProgram()) + " script " + finalScriptWitness.toString()); + break; + case PSBT_IN_POR_COMMITMENT: + entry.checkOneByteKey(); + String porMessage = new String(entry.getData(), StandardCharsets.UTF_8); + this.porCommitment = porMessage; + 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())); + break; + default: + throw new IllegalStateException("PSBT input not recognized key type: " + entry.getKeyType()); + } + } } - public void setNonWitnessUtxo(Transaction nonWitnessUtxo) { - testIfNull(this.nonWitnessUtxo); - this.nonWitnessUtxo = nonWitnessUtxo; + public Transaction getNonWitnessUtxo() { + return nonWitnessUtxo; } public TransactionOutput getWitnessUtxo() { return witnessUtxo; } - public void setWitnessUtxo(TransactionOutput witnessUtxo) { - testIfNull(this.witnessUtxo); - this.witnessUtxo = witnessUtxo; - } - public byte[] getPartialSignature(LazyECPoint publicKey) { return partialSignatures.get(publicKey); } - public void addPartialSignature(LazyECPoint publicKey, byte[] partialSignature) { - if(partialSignatures.containsKey(publicKey)) { - throw new IllegalStateException("Duplicate public key signature in scope"); - } - - this.partialSignatures.put(publicKey, partialSignature); - } - public Transaction.SigHash getSigHash() { return sigHash; } - public void setSigHash(Transaction.SigHash sigHash) { - testIfNull(this.sigHash); - this.sigHash = sigHash; - } - public Script getRedeemScript() { return redeemScript; } - public void setRedeemScript(Script redeemScript) { - testIfNull(this.redeemScript); - this.redeemScript = redeemScript; - } - public Script getWitnessScript() { return witnessScript; } - public void setWitnessScript(Script witnessScript) { - testIfNull(this.witnessScript); - this.witnessScript = witnessScript; - } - public KeyDerivation getKeyDerivation(LazyECPoint publicKey) { return derivedPublicKeys.get(publicKey); } - public void addDerivedPublicKey(LazyECPoint publicKey, KeyDerivation derivation) { - if(derivedPublicKeys.containsKey(publicKey)) { - throw new IllegalStateException("Duplicate public key in scope"); - } - - this.derivedPublicKeys.put(publicKey, derivation); - } - public Script getFinalScriptSig() { return finalScriptSig; } - public void setFinalScriptSig(Script finalScriptSig) { - testIfNull(this.finalScriptSig); - this.finalScriptSig = finalScriptSig; - } - public Script getFinalScriptWitness() { return finalScriptWitness; } - public void setFinalScriptWitness(Script finalScriptWitness) { - testIfNull(this.finalScriptWitness); - this.finalScriptWitness = finalScriptWitness; - } - public String getPorCommitment() { return porCommitment; } - public void setPorCommitment(String porCommitment) { - testIfNull(this.porCommitment); - this.porCommitment = porCommitment; + public Map getPartialSignatures() { + return partialSignatures; } - public void addProprietary(String key, String data) { - proprietary.put(key, data); + public Map getDerivedPublicKeys() { + return derivedPublicKeys; } - private void testIfNull(Object obj) { - if(obj != null) { - throw new IllegalStateException("Duplicate keys in scope"); - } + public Map getProprietary() { + return proprietary; } } diff --git a/src/main/java/com/craigraw/drongo/psbt/PSBTOutput.java b/src/main/java/com/craigraw/drongo/psbt/PSBTOutput.java index 4243c45..b7474c2 100644 --- a/src/main/java/com/craigraw/drongo/psbt/PSBTOutput.java +++ b/src/main/java/com/craigraw/drongo/psbt/PSBTOutput.java @@ -1,55 +1,79 @@ package com.craigraw.drongo.psbt; import com.craigraw.drongo.KeyDerivation; +import com.craigraw.drongo.crypto.ECKey; import com.craigraw.drongo.crypto.LazyECPoint; import com.craigraw.drongo.protocol.Script; +import org.bouncycastle.util.encoders.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; public class PSBTOutput { + public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00; + public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01; + public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02; + public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc; + private Script redeemScript; private Script witnessScript; private Map derivedPublicKeys = new LinkedHashMap<>(); private Map proprietary = new LinkedHashMap<>(); - public Script getRedeemScript() { - return redeemScript; + private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class); + + PSBTOutput(List outputEntries) { + for(PSBTEntry entry : outputEntries) { + switch (entry.getKeyType()) { + case PSBT_OUT_REDEEM_SCRIPT: + entry.checkOneByteKey(); + Script redeemScript = new Script(entry.getData()); + this.redeemScript = redeemScript; + log.debug("Found output redeem script hex " + Hex.toHexString(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); + break; + case PSBT_OUT_BIP32_DERIVATION: + entry.checkOneBytePlusPubKey(); + LazyECPoint publicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); + KeyDerivation keyDerivation = PSBTEntry.parseKeyDerivation(entry.getData()); + this.derivedPublicKeys.put(publicKey, keyDerivation); + log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + publicKey); + 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())); + break; + default: + throw new IllegalStateException("PSBT output not recognized key type: " + entry.getKeyType()); + } + } } - public void setRedeemScript(Script redeemScript) { - testIfNull(this.redeemScript); - this.redeemScript = redeemScript; + public Script getRedeemScript() { + return redeemScript; } public Script getWitnessScript() { return witnessScript; } - public void setWitnessScript(Script witnessScript) { - testIfNull(this.witnessScript); - this.witnessScript = witnessScript; - } - public KeyDerivation getKeyDerivation(LazyECPoint publicKey) { return derivedPublicKeys.get(publicKey); } - public void addDerivedPublicKey(LazyECPoint publicKey, KeyDerivation derivation) { - if(derivedPublicKeys.containsKey(publicKey)) { - throw new IllegalStateException("Duplicate public key in scope"); - } - - this.derivedPublicKeys.put(publicKey, derivation); + public Map getDerivedPublicKeys() { + return derivedPublicKeys; } - public void addProprietary(String key, String data) { - proprietary.put(key, data); - } - - private void testIfNull(Object obj) { - if(obj != null) { - throw new IllegalStateException("Duplicate keys in scope"); - } + public Map getProprietary() { + return proprietary; } } diff --git a/src/test/java/com/craigraw/drongo/psbt/PSBTTest.java b/src/test/java/com/craigraw/drongo/psbt/PSBTTest.java new file mode 100644 index 0000000..9388794 --- /dev/null +++ b/src/test/java/com/craigraw/drongo/psbt/PSBTTest.java @@ -0,0 +1,151 @@ +package com.craigraw.drongo.psbt; + +import org.junit.Assert; +import org.junit.Test; + +public class PSBTTest { + + @Test(expected = IllegalArgumentException.class) + public void invalidNotPSBT() { + String psbt = "AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA=="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void missingOutputs() { + String psbt = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA=="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void unsignedTxWithScriptSig() { + String psbt = "cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void noTransactionOnlyInputs() { + String psbt = "cHNidP8AAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA=="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void duplicateInputKeys() { + String psbt = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQA/AgAAAAH//////////////////////////////////////////wAAAAAA/////wEAAAAAAAAAAANqAQAAAAAAAAAA"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidGlobalTransactionKeyType() { + String psbt = "cHNidP8CAAFVAgAAAAEnmiMjpd+1H8RfIg+liw/BPh4zQnkqhdfjbNYzO1y8OQAAAAAA/////wGgWuoLAAAAABl2qRT/6cAGEJfMO2NvLLBGD6T8Qn0rRYisAAAAAAABASCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputWitnessUtxoKeyType() { + String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAIBACCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputPartialSignatureKeyType() { + String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIQIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYwQwIgBCS1jv+qppThVZ6lyTu/1KiQZCJAVc3wcLZ3FGlELQcCH1yOsP6mUW1guKyzOtZO3mDoeFv7OqlLmb34YVHbmpoBAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GIQPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvVKuIgYDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYQtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA=="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputRedeemScriptKeyType() { + String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQIEACIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputWitnessScriptKeyType() { + String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoECBQBHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputBip32KeyType() { + String psbt = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriEGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb0QtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA=="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputNonWitnessUtxoKeyType() { + String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAIAALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQfaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputFinalScriptSigKeyType() { + String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAACBwDaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputFinalScriptWitnessKeyType() { + String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAggA2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA="; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidOutputBip32PubKey() { + String psbt = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidInputSigHashKeyType() { + String psbt = "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidOutputRedeemScriptKeyType() { + String psbt = "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A"; + PSBT.fromString(psbt); + } + + @Test(expected = IllegalStateException.class) + public void invalidOutputWitnessScriptKeyType() { + String psbt = "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnQbVf4qHUa4A"; + PSBT.fromString(psbt); + } + + @Test + public void validP2pkhOneInputEmptyOutputs() { + String psbt = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA"; + PSBT psbt1 = PSBT.fromString(psbt); + Assert.assertEquals(1, psbt1.getPsbtInputs().size()); + Assert.assertEquals(2, psbt1.getPsbtOutputs().size()); + + Assert.assertEquals("af2cac1e0e33d896d9d0751d66fcb2fa54b737c7a13199281fb57e4f497bb652", psbt1.getTransaction().getTxId().toString()); + Assert.assertEquals("f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", psbt1.getTransaction().getInputs().get(0).getOutpoint().getHash().toString()); + Assert.assertEquals(0, psbt1.getTransaction().getInputs().get(0).getOutpoint().getIndex()); + Assert.assertEquals(0, psbt1.getTransaction().getInputs().get(0).getScriptBytes().length); + Assert.assertEquals(4294967294L, psbt1.getTransaction().getInputs().get(0).getSequenceNumber()); + + Assert.assertEquals(99999699L, psbt1.getTransaction().getOutputs().get(0).getValue()); + Assert.assertEquals("1L2tGENeoh4mSoiUZrSbs1J3jazSdJH9QS", psbt1.getTransaction().getOutputs().get(0).getScript().getToAddresses()[0].toString()); + Assert.assertEquals("76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac", psbt1.getTransaction().getOutputs().get(0).getScript().getProgramAsHex()); + Assert.assertEquals("OP_DUP OP_HASH160 d0c59903c5bac2868760e90fd521a4665aa76520 OP_EQUALVERIFY OP_CHECKSIG", psbt1.getTransaction().getOutputs().get(0).getScript().toString()); + Assert.assertEquals(100000000L, psbt1.getTransaction().getOutputs().get(1).getValue()); + Assert.assertEquals("36YhUacEtcnkfhSbxwm11wDCexLGBLgJF6", psbt1.getTransaction().getOutputs().get(1).getScript().getToAddresses()[0].toString()); + Assert.assertEquals("a9143545e6e33b832c47050f24d3eeb93c9c03948bc787", psbt1.getTransaction().getOutputs().get(1).getScript().getProgramAsHex()); + Assert.assertEquals("OP_HASH160 3545e6e33b832c47050f24d3eeb93c9c03948bc7 OP_EQUAL", psbt1.getTransaction().getOutputs().get(1).getScript().toString()); + + Assert.assertEquals("f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getTxId().toString()); + Assert.assertEquals(421, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getMessageSize()); + Assert.assertEquals("e567952fb6cc33857f392efa3a46c995a28f69cca4bb1b37e0204dab1ec7a389", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getOutpoint().getHash().toString()); + Assert.assertEquals("160014be18d152a9b012039daf3da7de4f53349eecb985", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getScript().getProgramAsHex()); + Assert.assertEquals("304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c01 03d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f2105", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(0).getWitness().toString()); + Assert.assertEquals("b490486aec3ae671012dddb2bb08466bef37720a533a894814ff1da743aaf886", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getInputs().get(1).getOutpoint().getHash().toString()); + Assert.assertEquals(200000000L, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(0).getValue()); + Assert.assertEquals("76a91485cff1097fd9e008bb34af709c62197b38978a4888ac", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(0).getScript().getProgramAsHex()); + Assert.assertEquals(190303501938L, psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(1).getValue()); + Assert.assertEquals("a914339725ba21efd62ac753a9bcd067d6c7a6a39d0587", psbt1.getPsbtInputs().get(0).getNonWitnessUtxo().getOutputs().get(1).getScript().getProgramAsHex()); + + Assert.assertEquals(0, psbt1.getPsbtOutputs().get(0).getDerivedPublicKeys().size()); + } +}