From 96df6284e11399eeda22f85aaeffc5da2ad61b5b Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 15 Nov 2024 12:15:41 +0200 Subject: [PATCH] add psbt v2 support --- .../com/sparrowwallet/drongo/psbt/PSBT.java | 412 ++++++++++++++++-- .../sparrowwallet/drongo/psbt/PSBTEntry.java | 14 +- .../sparrowwallet/drongo/psbt/PSBTInput.java | 298 ++++++++++++- .../sparrowwallet/drongo/psbt/PSBTOutput.java | 100 ++++- .../sparrowwallet/drongo/psbt/PSBTTest.java | 314 ++++++++++++- 5 files changed, 1074 insertions(+), 64 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index b756865..04fe11a 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -23,16 +23,21 @@ import static com.sparrowwallet.drongo.wallet.Wallet.addDummySpendingInput; 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_TX_VERSION = 0x02; + public static final byte PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03; + public static final byte PSBT_GLOBAL_INPUT_COUNT = 0x04; + public static final byte PSBT_GLOBAL_OUTPUT_COUNT = 0x05; + public static final byte PSBT_GLOBAL_TX_MODIFIABLE = 0x06; public static final byte PSBT_GLOBAL_VERSION = (byte)0xfb; public static final byte PSBT_GLOBAL_PROPRIETARY = (byte)0xfc; public static final String PSBT_MAGIC_HEX = "70736274"; public static final int PSBT_MAGIC_INT = 1886610036; - private static final int STATE_GLOBALS = 1; - private static final int STATE_INPUTS = 2; - private static final int STATE_OUTPUTS = 3; - private static final int STATE_END = 4; + public static final int STATE_GLOBALS = 1; + public static final int STATE_INPUTS = 2; + public static final int STATE_OUTPUTS = 3; + public static final int STATE_END = 4; private int inputs = 0; private int outputs = 0; @@ -44,20 +49,28 @@ public class PSBT { private final Map extendedPublicKeys = new LinkedHashMap<>(); private final Map globalProprietary = new LinkedHashMap<>(); + //PSBTv2 fields + private Long txVersion = null; + private Long fallbackLocktime = null; + private Long inputCount = null; + private Long outputCount = null; + private Byte modifiable = null; + private final List psbtInputs = new ArrayList<>(); private final List psbtOutputs = new ArrayList<>(); private static final Logger log = LoggerFactory.getLogger(PSBT.class); + private boolean verifyPrevTxids = true; public PSBT(Transaction transaction) { this.transaction = transaction; for(int i = 0; i < transaction.getInputs().size(); i++) { - psbtInputs.add(new PSBTInput(this, transaction, i)); + psbtInputs.add(new PSBTInput(this, i)); } for(int i = 0; i < transaction.getOutputs().size(); i++) { - psbtOutputs.add(new PSBTOutput()); + psbtOutputs.add(new PSBTOutput(this, i)); } } @@ -126,7 +139,7 @@ public class PSBT { } } - PSBTInput psbtInput = new PSBTInput(this, signingWallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, alwaysIncludeWitnessUtxo); + PSBTInput psbtInput = new PSBTInput(this, signingWallet.getScriptType(), inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, alwaysIncludeWitnessUtxo); psbtInputs.add(psbtInput); } @@ -148,7 +161,7 @@ public class PSBT { for(int outputIndex = 0; outputIndex < outputNodes.size(); outputIndex++) { WalletNode outputNode = outputNodes.get(outputIndex); if(outputNode == null) { - PSBTOutput externalRecipientOutput = new PSBTOutput(null, null, null, Collections.emptyMap(), Collections.emptyMap(), null); + PSBTOutput externalRecipientOutput = new PSBTOutput(this, outputIndex, null, null, null, Collections.emptyMap(), Collections.emptyMap(), null); psbtOutputs.add(externalRecipientOutput); } else { TransactionOutput txOutput = transaction.getOutputs().get(outputIndex); @@ -179,7 +192,7 @@ public class PSBT { } } - PSBTOutput walletOutput = new PSBTOutput(recipientWallet.getScriptType(), redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey); + PSBTOutput walletOutput = new PSBTOutput(this, outputIndex, recipientWallet.getScriptType(), redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey); psbtOutputs.add(walletOutput); } } @@ -190,7 +203,12 @@ public class PSBT { } public PSBT(byte[] psbt, boolean verifySignatures) throws PSBTParseException { + this(psbt, verifySignatures, true); + } + + public PSBT(byte[] psbt, boolean verifySignatures, boolean verifyPrevTxids) throws PSBTParseException { this.psbtBytes = psbt; + this.verifyPrevTxids = verifyPrevTxids; parse(verifySignatures); } @@ -235,7 +253,7 @@ public class PSBT { seenInputs++; if (seenInputs == inputs) { currentState = STATE_OUTPUTS; - parseInputEntries(inputEntryLists, verifySignatures); + parseInputEntries(inputEntryLists); } break; case STATE_OUTPUTS: @@ -265,11 +283,15 @@ public class PSBT { } if(currentState != STATE_END) { - if(transaction == null) { + if(getPsbtVersion() == 0 && transaction == null) { throw new PSBTParseException("Missing transaction"); } } + if(verifySignatures) { + verifySignatures(psbtInputs); + } + if(log.isDebugEnabled()) { log.debug("Calculated fee at " + getFee()); } @@ -282,7 +304,7 @@ public class PSBT { } for(PSBTEntry entry : globalEntries) { - switch(entry.getKeyType()) { + switch((byte)entry.getKeyType()) { case PSBT_GLOBAL_UNSIGNED_TX: entry.checkOneByteKey(); Transaction transaction = new Transaction(entry.getData()); @@ -312,6 +334,40 @@ public class PSBT { this.extendedPublicKeys.put(pubKey, keyDerivation); log.debug("Pubkey with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + ": " + pubKey.getExtendedKey()); break; + case PSBT_GLOBAL_TX_VERSION: + entry.checkOneByteKey(); + long txVersion = Utils.readUint32(entry.getData(), 0); + this.txVersion = txVersion; + log.debug("PSBT tx version: " + txVersion); + break; + case PSBT_GLOBAL_FALLBACK_LOCKTIME: + entry.checkOneByteKey(); + long fallbackLocktime = Utils.readUint32(entry.getData(), 0); + this.fallbackLocktime = fallbackLocktime; + log.debug("PSBT fallback locktime: " + fallbackLocktime); + break; + case PSBT_GLOBAL_INPUT_COUNT: + entry.checkOneByteKey(); + VarInt varIntInputCount = new VarInt(entry.getData(), 0); + this.inputCount = varIntInputCount.value; + this.inputs = inputCount.intValue(); + log.debug("PSBT input count: " + inputCount); + break; + case PSBT_GLOBAL_OUTPUT_COUNT: + entry.checkOneByteKey(); + VarInt varIntOutputCount = new VarInt(entry.getData(), 0); + this.outputCount = varIntOutputCount.value; + this.outputs = outputCount.intValue(); + log.debug("PSBT output count: " + outputCount); + break; + case PSBT_GLOBAL_TX_MODIFIABLE: + entry.checkOneByteKey(); + if(entry.getData().length != 1) { + throw new PSBTParseException("Tx modifiable field was not a single byte"); + } + this.modifiable = entry.getData()[0]; + log.debug("PSBT tx modifiable: " + String.format("%8s", Integer.toBinaryString(modifiable & 0xFF)).replace(' ', '0')); + break; case PSBT_GLOBAL_VERSION: entry.checkOneByteKey(); int version = (int)Utils.readUint32(entry.getData(), 0); @@ -326,9 +382,45 @@ public class PSBT { log.warn("PSBT global not recognized key type: " + entry.getKeyType()); } } + + if(getPsbtVersion() == 0) { + if(transaction == null) { + throw new PSBTParseException("PSBT_GLOBAL_UNSIGNED_TX is required in PSBTv0"); + } + if(txVersion != null) { + throw new PSBTParseException("PSBT_GLOBAL_TX_VERSION is not allowed in PSBTv0"); + } + if(fallbackLocktime != null) { + throw new PSBTParseException("PSBT_GLOBAL_FALLBACK_LOCKTIME is not allowed in PSBTv0"); + } + if(inputCount != null) { + throw new PSBTParseException("PSBT_GLOBAL_INPUT_COUNT is not allowed in PSBTv0"); + } + if(outputCount != null) { + throw new PSBTParseException("PSBT_GLOBAL_OUTPUT_COUNT is not allowed in PSBTv0"); + } + if(modifiable != null) { + throw new PSBTParseException("PSBT_GLOBAL_TX_MODIFIABLE is not allowed in PSBTv0"); + } + } else if(getPsbtVersion() == 1) { + throw new PSBTParseException("There is no PSBTv1"); + } else if(getPsbtVersion() >= 2) { + if(transaction != null) { + throw new PSBTParseException("PSBT_GLOBAL_UNSIGNED_TX is not allowed in PSBTv2"); + } + if(txVersion == null) { + throw new PSBTParseException("PSBT_GLOBAL_TX_VERSION is required in PSBTv2"); + } + if(inputCount == null) { + throw new PSBTParseException("PSBT_GLOBAL_INPUT_COUNT is required in PSBTv2"); + } + if(outputCount == null) { + throw new PSBTParseException("PSBT_GLOBAL_OUTPUT_COUNT is required in PSBTv2"); + } + } } - private void parseInputEntries(List> inputEntryLists, boolean verifySignatures) throws PSBTParseException { + private void parseInputEntries(List> inputEntryLists) throws PSBTParseException { for(List inputEntries : inputEntryLists) { PSBTEntry duplicate = findDuplicateKey(inputEntries); if(duplicate != null) { @@ -336,12 +428,34 @@ public class PSBT { } int inputIndex = this.psbtInputs.size(); - PSBTInput input = new PSBTInput(this, inputEntries, transaction, inputIndex); - this.psbtInputs.add(input); - } + PSBTInput input = new PSBTInput(this, inputEntries, inputIndex); - if(verifySignatures) { - verifySignatures(psbtInputs); + if(getPsbtVersion() == 0) { + if(input.prevTxid() != null) { + throw new PSBTParseException("PSBT_IN_PREV_TXID is not allowed in PSBTv0"); + } + if(input.prevIndex() != null) { + throw new PSBTParseException("PSBT_IN_OUTPUT_INDEX is not allowed in PSBTv0"); + } + if(input.sequence() != null) { + throw new PSBTParseException("PSBT_IN_SEQUENCE is not allowed in PSBTv0"); + } + if(input.getRequiredTimeLocktime() != null) { + throw new PSBTParseException("PSBT_IN_REQUIRED_TIME_LOCKTIME is not allowed in PSBTv0"); + } + if(input.getRequiredHeightLocktime() != null) { + throw new PSBTParseException("PSBT_IN_REQUIRED_HEIGHT_LOCKTIME is not allowed in PSBTv0"); + } + } else if(getPsbtVersion() >= 2) { + if(input.prevTxid() == null) { + throw new PSBTParseException("PSBT_IN_PREV_TXID is required in PSBTv2"); + } + if(input.prevIndex() == null) { + throw new PSBTParseException("PSBT_IN_OUTPUT_INDEX is required in PSBTv2"); + } + } + + this.psbtInputs.add(input); } } @@ -352,11 +466,33 @@ public class PSBT { throw new PSBTParseException("Found duplicate key for PSBT output: " + Utils.bytesToHex(duplicate.getKey())); } - PSBTOutput output = new PSBTOutput(outputEntries); + int outputIndex = this.psbtOutputs.size(); + PSBTOutput output = new PSBTOutput(this, outputEntries, outputIndex); + + if(getPsbtVersion() == 0) { + if(output.amount() != null) { + throw new PSBTParseException("PSBT_OUT_AMOUNT is not allowed in PSBTv0"); + } + if(output.script() != null) { + throw new PSBTParseException("PSBT_OUT_SCRIPT is not allowed in PSBTv0"); + } + } else if(getPsbtVersion() >= 2) { + if(output.amount() == null) { + throw new PSBTParseException("PSBT_OUT_AMOUNT is required in PSBTv2"); + } + if(output.script() == null) { + throw new PSBTParseException("PSBT_OUT_SCRIPT is required in PSBTv2"); + } + } + this.psbtOutputs.add(output); } } + int getPsbtVersion() { + return version == null ? 0 : version; + } + private PSBTEntry findDuplicateKey(List entries) { Set checkSet = new HashSet<>(); for(PSBTEntry entry: entries) { @@ -382,9 +518,8 @@ public class PSBT { } } - for (int i = 0; i < transaction.getOutputs().size(); i++) { - TransactionOutput output = transaction.getOutputs().get(i); - fee -= output.getValue(); + for(PSBTOutput output : psbtOutputs) { + fee -= output.getAmount(); } return fee; @@ -397,7 +532,7 @@ public class PSBT { private void verifySignatures(List psbtInputs) throws PSBTSignatureException { for(PSBTInput input : psbtInputs) { boolean verified = input.verifySignatures(); - if(!verified && input.getPartialSignatures().size() > 0) { + if(!verified && !input.getPartialSignatures().isEmpty()) { throw new PSBTSignatureException("Unverifiable partial signatures provided"); } if(!verified && input.isTaproot() && input.getTapKeyPathSignature() != null) { @@ -439,7 +574,7 @@ public class PSBT { private List getGlobalEntries() { List entries = new ArrayList<>(); - if(transaction != null) { + if(getPsbtVersion() == 0 && transaction != null) { entries.add(populateEntry(PSBT_GLOBAL_UNSIGNED_TX, null, transaction.bitcoinSerialize(false))); } @@ -447,6 +582,30 @@ public class PSBT { entries.add(populateEntry(PSBT_GLOBAL_BIP32_PUBKEY, entry.getKey().getExtendedKeyBytes(), serializeKeyDerivation(entry.getValue()))); } + if(getPsbtVersion() >= 2) { + if(txVersion != null) { + byte[] txVersionBytes = new byte[4]; + Utils.uint32ToByteArrayLE(txVersion, txVersionBytes, 0); + entries.add(populateEntry(PSBT_GLOBAL_TX_VERSION, null, txVersionBytes)); + } + if(fallbackLocktime != null) { + byte[] fallbackLocktimeBytes = new byte[4]; + Utils.uint32ToByteArrayLE(fallbackLocktime, fallbackLocktimeBytes, 0); + entries.add(populateEntry(PSBT_GLOBAL_FALLBACK_LOCKTIME, null, fallbackLocktimeBytes)); + } + if(inputCount != null) { + VarInt varIntInputCount = new VarInt(inputCount); + entries.add(populateEntry(PSBT_GLOBAL_INPUT_COUNT, null, varIntInputCount.encode())); + } + if(outputCount != null) { + VarInt varIntOutputCount = new VarInt(outputCount); + entries.add(populateEntry(PSBT_GLOBAL_OUTPUT_COUNT, null, varIntOutputCount.encode())); + } + if(modifiable != null) { + entries.add(populateEntry(PSBT_GLOBAL_TX_MODIFIABLE, null, new byte[] { modifiable })); + } + } + if(version != null) { byte[] versionBytes = new byte[4]; Utils.uint32ToByteArrayLE(version, versionBytes, 0); @@ -479,7 +638,7 @@ public class PSBT { baos.writeBytes(new byte[] {(byte)0x00}); for(PSBTInput psbtInput : getPsbtInputs()) { - List inputEntries = psbtInput.getInputEntries(); + List inputEntries = psbtInput.getInputEntries(getPsbtVersion()); for(PSBTEntry entry : inputEntries) { if((includeXpubs || (entry.getKeyType() != PSBT_IN_BIP32_DERIVATION && entry.getKeyType() != PSBT_IN_PROPRIETARY && entry.getKeyType() != PSBT_IN_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_IN_TAP_BIP32_DERIVATION)) @@ -491,7 +650,7 @@ public class PSBT { } for(PSBTOutput psbtOutput : getPsbtOutputs()) { - List outputEntries = psbtOutput.getOutputEntries(); + List outputEntries = psbtOutput.getOutputEntries(getPsbtVersion()); for(PSBTEntry entry : outputEntries) { if(includeXpubs || (entry.getKeyType() != PSBT_OUT_REDEEM_SCRIPT && entry.getKeyType() != PSBT_OUT_WITNESS_SCRIPT && entry.getKeyType() != PSBT_OUT_BIP32_DERIVATION && entry.getKeyType() != PSBT_OUT_PROPRIETARY @@ -512,7 +671,11 @@ public class PSBT { } public void combine(PSBT psbt) { - byte[] txBytes = transaction.bitcoinSerialize(); + if(getPsbtVersion() != psbt.getPsbtVersion()) { + psbt.convertVersion(getPsbtVersion()); + } + + byte[] txBytes = getTransaction().bitcoinSerialize(); byte[] psbtTxBytes = psbt.getTransaction().bitcoinSerialize(); if(!Arrays.equals(txBytes, psbtTxBytes)) { @@ -551,7 +714,7 @@ public class PSBT { } } - Transaction finalTransaction = new Transaction(transaction.bitcoinSerialize()); + Transaction finalTransaction = new Transaction(getTransaction().bitcoinSerialize()); if(hasWitness && !finalTransaction.isSegwit()) { finalTransaction.setSegwitFlag(Transaction.DEFAULT_SEGWIT_FLAG); @@ -596,7 +759,7 @@ public class PSBT { public void moveInput(int fromIndex, int toIndex) { moveItem(psbtInputs, fromIndex, toIndex); - transaction.moveInput(fromIndex, toIndex); + getTransaction().moveInput(fromIndex, toIndex); for(int i = 0; i < psbtInputs.size(); i++) { psbtInputs.get(i).setIndex(i); } @@ -604,7 +767,10 @@ public class PSBT { public void moveOutput(int fromIndex, int toIndex) { moveItem(psbtOutputs, fromIndex, toIndex); - transaction.moveOutput(fromIndex, toIndex); + getTransaction().moveOutput(fromIndex, toIndex); + for(int i = 0; i < psbtOutputs.size(); i++) { + psbtOutputs.get(i).setIndex(i); + } } private void moveItem(List list, int fromIndex, int toIndex) { @@ -625,6 +791,33 @@ public class PSBT { } public Transaction getTransaction() { + return getTransaction(true); + } + + public Transaction getTransaction(boolean setSequence) { + if(getPsbtVersion() >= 2) { + Transaction transaction = new Transaction(); + transaction.setVersion(txVersion); + transaction.setLocktime(getLocktime(psbtInputs, fallbackLocktime)); + for(PSBTInput psbtInput : getPsbtInputs()) { + TransactionInput transactionInput = transaction.addInput(psbtInput.getPrevTxid(), psbtInput.getPrevIndex(), new Script(new byte[0])); + if(setSequence) { + if(psbtInput.getSequence() == null) { + transactionInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED); + } else { + transactionInput.setSequenceNumber(psbtInput.getSequence()); + } + } else { + //Sequence number is set to zero to provide a static txid while updating + transactionInput.setSequenceNumber(0); + } + } + for(PSBTOutput psbtOutput : getPsbtOutputs()) { + transaction.addOutput(psbtOutput.getAmount(), psbtOutput.getScript()); + } + return transaction; + } + return transaction; } @@ -640,6 +833,72 @@ public class PSBT { return extendedPublicKeys; } + public Long getTxVersion() { + if(getPsbtVersion() >= 2) { + return txVersion; + } + + return getTransaction().getVersion(); + } + + public Long getFallbackLocktime() { + if(getPsbtVersion() >= 2) { + return fallbackLocktime; + } + + return getTransaction().getLocktime(); + } + + public Long getInputCount() { + if(getPsbtVersion() >= 2) { + return inputCount; + } + + return (long)getTransaction().getInputs().size(); + } + + public Long getOutputCount() { + if(getPsbtVersion() >= 2) { + return outputCount; + } + + return (long)getTransaction().getOutputs().size(); + } + + public Byte getModifiable() { + return modifiable; + } + + public Boolean isInputsModifiable() { + return modifiable == null ? null : (modifiable & (byte)0x01) > 0; + } + + public void setInputsModifiable(boolean inputsModifiable) { + if(modifiable != null) { + modifiable = inputsModifiable ? (byte)(modifiable | (byte)0x01) : (byte)(modifiable & (byte)0xFE); + } + } + + public Boolean isOutputsModifiable() { + return modifiable == null ? null : (modifiable & (byte)0x02) > 0; + } + + public void setOutputsModifiable(boolean outputsModifiable) { + if(modifiable != null) { + modifiable = outputsModifiable ? (byte)(modifiable | (byte)0x02) : (byte)(modifiable & (byte)0xFD); + } + } + + public Boolean isSigHashSingleSignaturePresent() { + return modifiable == null ? null : (modifiable & (byte)0x04) > 0; + } + + public void setSigHashSingleSignaturePresent(boolean sigHashSingleSignaturePresent) { + if(modifiable != null) { + modifiable = sigHashSingleSignaturePresent ? (byte)(modifiable | (byte)0x04) : (byte)(modifiable & (byte)0xFB); + } + } + public Map getGlobalProprietary() { return globalProprietary; } @@ -656,6 +915,93 @@ public class PSBT { return Base64.toBase64String(serialize(includeXpubs, true)); } + public void convertVersion(int version) { + if(version < 0) { + throw new IllegalArgumentException("Version must be zero or positive"); + } + + //Convert from PSBTv2+ to PSBTv0 + if(getPsbtVersion() >= 2 && version == 0) { + this.transaction = getTransaction(); + this.txVersion = null; + this.fallbackLocktime = null; + this.inputCount = null; + this.outputCount = null; + this.modifiable = null; + + for(PSBTInput psbtInput : getPsbtInputs()) { + psbtInput.setPrevTxid(null); + psbtInput.setPrevIndex(null); + psbtInput.setSequence(null); + psbtInput.setRequiredTimeLocktime(null); + psbtInput.setRequiredHeightLocktime(null); + } + + for(PSBTOutput psbtOutput : getPsbtOutputs()) { + psbtOutput.setAmount(null); + psbtOutput.setScript(null); + } + } + + //Convert from PSBTv0 to PSBTv2+ + if(getPsbtVersion() == 0 && version >= 2) { + this.txVersion = transaction.getVersion(); + this.fallbackLocktime = transaction.getLocktime(); + this.inputCount = (long)transaction.getInputs().size(); + this.outputCount = (long)transaction.getOutputs().size(); + this.modifiable = null; + + for(PSBTInput psbtInput : getPsbtInputs()) { + psbtInput.setPrevTxid(psbtInput.getPrevTxid()); + psbtInput.setPrevIndex(psbtInput.getPrevIndex()); + psbtInput.setSequence(psbtInput.getSequence()); + psbtInput.setRequiredTimeLocktime(null); + psbtInput.setRequiredHeightLocktime(null); + } + + for(PSBTOutput psbtOutput : getPsbtOutputs()) { + psbtOutput.setAmount(psbtOutput.getAmount()); + psbtOutput.setScript(psbtOutput.getScript()); + } + + this.transaction = null; + } + + this.version = version; + } + + private long getLocktime(List psbtInputs, Long fallbackLocktime) { + long fallback = (fallbackLocktime != null) ? fallbackLocktime : 0L; + + OptionalLong maxHeightLocktime = psbtInputs.stream().map(PSBTInput::getRequiredHeightLocktime).filter(Objects::nonNull).mapToLong(Long::longValue).max(); + OptionalLong maxTimeLocktime = psbtInputs.stream().map(PSBTInput::getRequiredTimeLocktime).filter(Objects::nonNull).mapToLong(Long::longValue).max(); + + boolean allHeight = psbtInputs.stream().map(PSBTInput::getRequiredHeightLocktime).allMatch(Objects::nonNull); + boolean allTime = psbtInputs.stream().map(PSBTInput::getRequiredTimeLocktime).allMatch(Objects::nonNull); + + if(maxHeightLocktime.isEmpty() && maxTimeLocktime.isEmpty()) { + return fallback; + } + + if(maxHeightLocktime.isPresent() && allHeight) { + return maxHeightLocktime.getAsLong(); + } + + if(maxTimeLocktime.isPresent() && allTime) { + return maxTimeLocktime.getAsLong(); + } + + if(maxHeightLocktime.isPresent() && maxTimeLocktime.isPresent()) { + return maxHeightLocktime.getAsLong(); + } + + return maxHeightLocktime.orElse(maxTimeLocktime.orElse(fallback)); + } + + boolean isVerifyPrevTxids() { + return verifyPrevTxids; + } + public static boolean isPSBT(byte[] b) { try { ByteBuffer buffer = ByteBuffer.wrap(b); @@ -687,6 +1033,10 @@ public class PSBT { } public static PSBT fromString(String strPSBT, boolean verifySignatures) throws PSBTParseException { + return fromString(strPSBT, verifySignatures, true); + } + + static PSBT fromString(String strPSBT, boolean verifySignatures, boolean verifyPrevTxids) throws PSBTParseException { if (!isPSBT(strPSBT)) { throw new PSBTParseException("Provided string is not a PSBT"); } @@ -696,6 +1046,6 @@ public class PSBT { } byte[] psbtBytes = Utils.hexToBytes(strPSBT); - return new PSBT(psbtBytes, verifySignatures); + return new PSBT(psbtBytes, verifySignatures, verifyPrevTxids); } } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java index 1010baa..e0f6080 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java @@ -27,7 +27,7 @@ public class PSBTEntry { this.data = data; } - PSBTEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException { + public PSBTEntry(ByteBuffer psbtByteBuffer) throws PSBTParseException { int keyLen = readCompactInt(psbtByteBuffer); if (keyLen == 0x00) { @@ -259,4 +259,16 @@ public class PSBTEntry { throw new PSBTParseException("PSBT key type must be one byte plus x only pub key"); } } + + public void checkOneBytePlusRipe160Key() throws PSBTParseException { + if(this.getKey().length != 21) { + throw new PSBTParseException("PSBT key type must be one byte plus Ripe160MD hash"); + } + } + + public void checkOneBytePlusSha256Key() throws PSBTParseException { + if(this.getKey().length != 33) { + throw new PSBTParseException("PSBT key type must be one byte plus SHA256 hash"); + } + } } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index ee0e5a4..716a960 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -26,6 +26,15 @@ public class PSBTInput { 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_RIPEMD160 = 0x0a; + public static final byte PSBT_IN_SHA256 = 0x0b; + public static final byte PSBT_IN_HASH160 = 0x0c; + public static final byte PSBT_IN_HASH256 = 0x0d; + public static final byte PSBT_IN_PREVIOUS_TXID = 0x0e; + public static final byte PSBT_IN_OUTPUT_INDEX = 0x0f; + public static final byte PSBT_IN_SEQUENCE = 0x10; + public static final byte PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11; + public static final byte PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12; public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc; public static final byte PSBT_IN_TAP_KEY_SIG = 0x13; public static final byte PSBT_IN_TAP_BIP32_DERIVATION = 0x16; @@ -42,24 +51,33 @@ public class PSBTInput { private Script finalScriptSig; private TransactionWitness finalScriptWitness; private String porCommitment; + private byte[] ripeMd160Preimage; + private byte[] sha256Preimage; + private byte[] hash160Preimage; + private byte[] hash256Preimage; private final Map proprietary = new LinkedHashMap<>(); private TransactionSignature tapKeyPathSignature; private Map>> tapDerivedPublicKeys = new LinkedHashMap<>(); private ECKey tapInternalKey; - private final Transaction transaction; + //PSBTv2 fields + private Sha256Hash prevTxid; + private Long prevIndex; + private Long sequence; + private Long requiredTimeLocktime; + private Long requiredHeightLocktime; + private int index; private static final Logger log = LoggerFactory.getLogger(PSBTInput.class); - PSBTInput(PSBT psbt, Transaction transaction, int index) { + PSBTInput(PSBT psbt, int index) { this.psbt = psbt; - this.transaction = transaction; this.index = index; } - PSBTInput(PSBT psbt, ScriptType scriptType, Transaction transaction, int index, Transaction utxo, int utxoIndex, Script redeemScript, Script witnessScript, Map derivedPublicKeys, Map proprietary, ECKey tapInternalKey, boolean alwaysAddNonWitnessTx) { - this(psbt, transaction, index); + PSBTInput(PSBT psbt, ScriptType scriptType, int index, Transaction utxo, int utxoIndex, Script redeemScript, Script witnessScript, Map derivedPublicKeys, Map proprietary, ECKey tapInternalKey, boolean alwaysAddNonWitnessTx) { + this(psbt, index); if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) { this.witnessUtxo = utxo.getOutputs().get(utxoIndex); @@ -91,18 +109,30 @@ public class PSBTInput { this.sigHash = getDefaultSigHash(); } - PSBTInput(PSBT psbt, List inputEntries, Transaction transaction, int index) throws PSBTParseException { - this.psbt = psbt; - for(PSBTEntry entry : inputEntries) { - switch(entry.getKeyType()) { + PSBTInput(PSBT psbt, List inputEntries, int index) throws PSBTParseException { + this(psbt, index); + List sortedEntries = new ArrayList<>(inputEntries); + sortedEntries.sort((o1, o2) -> { + int found1 = o1.getKeyType() == PSBT_IN_PREVIOUS_TXID || o1.getKeyType() == PSBT_IN_OUTPUT_INDEX ? 1 : 0; + int found2 = o2.getKeyType() == PSBT_IN_PREVIOUS_TXID || o2.getKeyType() == PSBT_IN_OUTPUT_INDEX ? 1 : 0; + return found2 - found1; + }); + + for(PSBTEntry entry : sortedEntries) { + switch((byte)entry.getKeyType()) { case PSBT_IN_NON_WITNESS_UTXO: entry.checkOneByteKey(); Transaction nonWitnessTx = new Transaction(entry.getData()); nonWitnessTx.verify(); - Sha256Hash inputHash = nonWitnessTx.calculateTxId(false); - Sha256Hash outpointHash = transaction.getInputs().get(index).getOutpoint().getHash(); - if(!outpointHash.equals(inputHash)) { - throw new PSBTParseException("Hash of provided non witness utxo transaction " + inputHash + " does not match transaction input outpoint hash " + outpointHash + " at index " + index); + if(psbt.isVerifyPrevTxids()) { + Sha256Hash inputHash = nonWitnessTx.calculateTxId(false); + Sha256Hash outpointHash = getPrevTxid(); + if(outpointHash == null) { + throw new PSBTParseException("Outpoint hash not present for input " + index); + } + if(!outpointHash.equals(inputHash)) { + throw new PSBTParseException("Hash of provided non witness utxo transaction " + inputHash + " does not match transaction input outpoint hash " + outpointHash + " at index " + index); + } } this.nonWitnessUtxo = nonWitnessTx; @@ -151,7 +181,11 @@ public class PSBTInput { Script redeemScript = new Script(entry.getData()); Script scriptPubKey = null; if(this.nonWitnessUtxo != null) { - scriptPubKey = this.nonWitnessUtxo.getOutputs().get((int)transaction.getInputs().get(index).getOutpoint().getIndex()).getScript(); + Long prevIndex = getPrevIndex(); + if(prevIndex == null) { + throw new PSBTParseException("Outpoint index not present for input " + index); + } + scriptPubKey = this.nonWitnessUtxo.getOutputs().get(prevIndex.intValue()).getScript(); } else if(this.witnessUtxo != null) { scriptPubKey = this.witnessUtxo.getScript(); if(!P2WPKH.isScriptType(redeemScript) && !P2WSH.isScriptType(redeemScript)) { //Witness UTXO should only be provided for P2SH-P2WPKH or P2SH-P2WSH @@ -214,6 +248,71 @@ public class PSBTInput { this.porCommitment = porMessage; log.debug("Found input POR commitment message " + porMessage); break; + case PSBT_IN_RIPEMD160: + entry.checkOneBytePlusRipe160Key(); + if(!Arrays.equals(entry.getKeyData(), Ripemd160.getHash(entry.getData()))) { + throw new PSBTParseException("Hash of PSBT_IN_RIPEMD160 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData())); + } + this.ripeMd160Preimage = entry.getData(); + log.debug("Found input RIPEMD160 preimage " + Utils.bytesToHex(entry.getData())); + break; + case PSBT_IN_SHA256: + entry.checkOneBytePlusSha256Key(); + if(!Arrays.equals(entry.getKeyData(), Sha256Hash.hash(entry.getData()))) { + throw new PSBTParseException("Hash of PSBT_IN_SHA256 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData())); + } + this.sha256Preimage = entry.getData(); + log.debug("Found input SHA256 preimage " + Utils.bytesToHex(entry.getData())); + break; + case PSBT_IN_HASH160: + entry.checkOneBytePlusRipe160Key(); + if(!Arrays.equals(entry.getKeyData(), Utils.sha256hash160(entry.getData()))) { + throw new PSBTParseException("Hash of PSBT_IN_HASH160 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData())); + } + this.hash160Preimage = entry.getData(); + log.debug("Found input HASH160 preimage " + Utils.bytesToHex(entry.getData())); + break; + case PSBT_IN_HASH256: + entry.checkOneBytePlusSha256Key(); + if(!Arrays.equals(entry.getKeyData(), Sha256Hash.hashTwice(entry.getData()))) { + throw new PSBTParseException("Hash of PSBT_IN_HASH256 preimage did not match provided hash " + Utils.bytesToHex(entry.getKeyData()) + " " + Utils.bytesToHex(entry.getData())); + } + this.hash256Preimage = entry.getData(); + log.debug("Found input HASH256 preimage " + Utils.bytesToHex(entry.getData())); + break; + case PSBT_IN_PREVIOUS_TXID: + entry.checkOneByteKey(); + this.prevTxid = Sha256Hash.wrap(entry.getData()); + log.debug("Found input previous txid " + Utils.bytesToHex(entry.getData())); + break; + case PSBT_IN_OUTPUT_INDEX: + entry.checkOneByteKey(); + this.prevIndex = Utils.readUint32(entry.getData(), 0); + log.debug("Found input previous output index " + this.prevIndex); + break; + case PSBT_IN_SEQUENCE: + entry.checkOneByteKey(); + this.sequence = Utils.readUint32(entry.getData(), 0); + log.debug("Found input sequence " + this.sequence); + break; + case PSBT_IN_REQUIRED_TIME_LOCKTIME: + entry.checkOneByteKey(); + long requiredTimeLocktime = Utils.readUint32(entry.getData(), 0); + if(requiredTimeLocktime < 500000000) { + throw new PSBTParseException("Required time locktime is less than 500000000"); + } + this.requiredTimeLocktime = requiredTimeLocktime; + log.debug("Found input required time locktime " + this.requiredTimeLocktime); + break; + case PSBT_IN_REQUIRED_HEIGHT_LOCKTIME: + entry.checkOneByteKey(); + long requiredHeightLocktime = Utils.readUint32(entry.getData(), 0); + if(requiredHeightLocktime >= 500000000) { + throw new PSBTParseException("Required time locktime is greater than or equal to 500000000"); + } + this.requiredHeightLocktime = requiredHeightLocktime; + log.debug("Found input required height locktime " + this.requiredHeightLocktime); + break; case PSBT_IN_PROPRIETARY: this.proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData())); log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData())); @@ -245,12 +344,9 @@ public class PSBTInput { log.warn("PSBT input not recognized key type: " + entry.getKeyType()); } } - - this.transaction = transaction; - this.index = index; } - public List getInputEntries() { + public List getInputEntries(int psbtVersion) { List entries = new ArrayList<>(); if(nonWitnessUtxo != null) { @@ -296,6 +392,32 @@ public class PSBTInput { entries.add(populateEntry(PSBT_IN_POR_COMMITMENT, null, porCommitment.getBytes(StandardCharsets.UTF_8))); } + if(psbtVersion >= 2) { + if(prevTxid != null) { + entries.add(populateEntry(PSBT_IN_PREVIOUS_TXID, null, prevTxid.getBytes())); + } + if(prevIndex != null) { + byte[] prevIndexBytes = new byte[4]; + Utils.uint32ToByteArrayLE(prevIndex, prevIndexBytes, 0); + entries.add(populateEntry(PSBT_IN_OUTPUT_INDEX, null, prevIndexBytes)); + } + if(sequence != null) { + byte[] sequenceBytes = new byte[4]; + Utils.uint32ToByteArrayLE(sequence, sequenceBytes, 0); + entries.add(populateEntry(PSBT_IN_SEQUENCE, null, sequenceBytes)); + } + if(requiredTimeLocktime != null) { + byte[] requiredTimeLocktimeBytes = new byte[4]; + Utils.uint32ToByteArrayLE(requiredTimeLocktime, requiredTimeLocktimeBytes, 0); + entries.add(populateEntry(PSBT_IN_REQUIRED_TIME_LOCKTIME, null, requiredTimeLocktimeBytes)); + } + if(requiredHeightLocktime != null) { + byte[] requiredHeightLocktimeBytes = new byte[4]; + Utils.uint32ToByteArrayLE(requiredHeightLocktime, requiredHeightLocktimeBytes, 0); + entries.add(populateEntry(PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, null, requiredHeightLocktimeBytes)); + } + } + for(Map.Entry entry : proprietary.entrySet()) { entries.add(populateEntry(PSBT_IN_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue()))); } @@ -346,6 +468,42 @@ public class PSBTInput { porCommitment = psbtInput.porCommitment; } + if(psbtInput.ripeMd160Preimage != null) { + ripeMd160Preimage = psbtInput.ripeMd160Preimage; + } + + if(psbtInput.sha256Preimage != null) { + sha256Preimage = psbtInput.sha256Preimage; + } + + if(psbtInput.hash160Preimage != null) { + hash160Preimage = psbtInput.hash160Preimage; + } + + if(psbtInput.hash256Preimage != null) { + hash256Preimage = psbtInput.hash256Preimage; + } + + if(psbtInput.prevTxid != null) { + prevTxid = psbtInput.prevTxid; + } + + if(psbtInput.prevIndex != null) { + prevIndex = psbtInput.prevIndex; + } + + if(psbtInput.sequence != null) { + sequence = psbtInput.sequence; + } + + if(psbtInput.requiredTimeLocktime != null) { + requiredTimeLocktime = psbtInput.requiredTimeLocktime; + } + + if(psbtInput.requiredHeightLocktime != null) { + requiredHeightLocktime = psbtInput.requiredHeightLocktime; + } + proprietary.putAll(psbtInput.proprietary); if(psbtInput.tapKeyPathSignature != null) { @@ -481,6 +639,102 @@ public class PSBTInput { return getUtxo() != null && getScriptType() == P2TR; } + public byte[] getRipeMd160Preimage() { + return ripeMd160Preimage; + } + + public void setRipeMd160Preimage(byte[] ripeMd160Preimage) { + this.ripeMd160Preimage = ripeMd160Preimage; + } + + public byte[] getSha256Preimage() { + return sha256Preimage; + } + + public void setSha256Preimage(byte[] sha256Preimage) { + this.sha256Preimage = sha256Preimage; + } + + public byte[] getHash160Preimage() { + return hash160Preimage; + } + + public void setHash160Preimage(byte[] hash160Preimage) { + this.hash160Preimage = hash160Preimage; + } + + public byte[] getHash256Preimage() { + return hash256Preimage; + } + + public void setHash256Preimage(byte[] hash256Preimage) { + this.hash256Preimage = hash256Preimage; + } + + public Sha256Hash getPrevTxid() { + if(psbt.getPsbtVersion() >= 2) { + return prevTxid; + } + + return getInput().getOutpoint().getHash(); + } + + Sha256Hash prevTxid() { + return prevTxid; + } + + public void setPrevTxid(Sha256Hash prevTxid) { + this.prevTxid = prevTxid; + } + + public Long getPrevIndex() { + if(psbt.getPsbtVersion() >= 2) { + return prevIndex; + } + + return getInput().getOutpoint().getIndex(); + } + + Long prevIndex() { + return prevIndex; + } + + public void setPrevIndex(Long prevIndex) { + this.prevIndex = prevIndex; + } + + public Long getSequence() { + if(psbt.getPsbtVersion() >= 2) { + return sequence; + } + + return getInput().getSequenceNumber(); + } + + Long sequence() { + return sequence; + } + + public void setSequence(Long sequence) { + this.sequence = sequence; + } + + public Long getRequiredTimeLocktime() { + return requiredTimeLocktime; + } + + public void setRequiredTimeLocktime(Long requiredTimeLocktime) { + this.requiredTimeLocktime = requiredTimeLocktime; + } + + public Long getRequiredHeightLocktime() { + return requiredHeightLocktime; + } + + public void setRequiredHeightLocktime(Long requiredHeightLocktime) { + this.requiredHeightLocktime = requiredHeightLocktime; + } + public boolean isSigned() { if(getTapKeyPathSignature() != null) { return true; @@ -676,7 +930,7 @@ public class PSBTInput { } public TransactionInput getInput() { - return transaction.getInputs().get(index); + return psbt.getTransaction().getInputs().get(index); } public TransactionOutput getUtxo() { @@ -705,12 +959,12 @@ public class PSBTInput { ScriptType scriptType = getScriptType(); if(scriptType == ScriptType.P2TR) { List spentUtxos = psbt.getPsbtInputs().stream().map(PSBTInput::getUtxo).collect(Collectors.toList()); - hash = transaction.hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null); + hash = psbt.getTransaction().hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null); } else if(Arrays.asList(WITNESS_TYPES).contains(scriptType)) { long prevValue = getUtxo().getValue(); - hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, localSigHash); + hash = psbt.getTransaction().hashForWitnessSignature(index, connectedScript, prevValue, localSigHash); } else { - hash = transaction.hashForLegacySignature(index, connectedScript, localSigHash); + hash = psbt.getTransaction().hashForLegacySignature(index, connectedScript, localSigHash); } return hash; diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java index fb10a71..d98ae7f 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java @@ -3,9 +3,7 @@ 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 com.sparrowwallet.drongo.protocol.ScriptType; -import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.protocol.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +16,8 @@ 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_AMOUNT = 0x03; + public static final byte PSBT_OUT_SCRIPT = 0x04; public static final byte PSBT_OUT_TAP_INTERNAL_KEY = 0x05; public static final byte PSBT_OUT_TAP_BIP32_DERIVATION = 0x07; public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc; @@ -29,13 +29,23 @@ public class PSBTOutput { private Map>> tapDerivedPublicKeys = new LinkedHashMap<>(); private ECKey tapInternalKey; + //PSBTv2 fields + private Long amount; + private Script script; + private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class); - PSBTOutput() { - //empty constructor + private final PSBT psbt; + private int index; + + PSBTOutput(PSBT psbt, int index) { + this.psbt = psbt; + this.index = index; } - PSBTOutput(ScriptType scriptType, Script redeemScript, Script witnessScript, Map derivedPublicKeys, Map proprietary, ECKey tapInternalKey) { + PSBTOutput(PSBT psbt, int index, ScriptType scriptType, Script redeemScript, Script witnessScript, Map derivedPublicKeys, Map proprietary, ECKey tapInternalKey) { + this(psbt, index); + this.redeemScript = redeemScript; this.witnessScript = witnessScript; @@ -53,9 +63,10 @@ public class PSBTOutput { } } - PSBTOutput(List outputEntries) throws PSBTParseException { + PSBTOutput(PSBT psbt, List outputEntries, int index) throws PSBTParseException { + this(psbt, index); for(PSBTEntry entry : outputEntries) { - switch (entry.getKeyType()) { + switch((byte)entry.getKeyType()) { case PSBT_OUT_REDEEM_SCRIPT: entry.checkOneByteKey(); Script redeemScript = new Script(entry.getData()); @@ -75,6 +86,17 @@ public class PSBTOutput { this.derivedPublicKeys.put(derivedPublicKey, keyDerivation); log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey); break; + case PSBT_OUT_AMOUNT: + entry.checkOneByteKey(); + this.amount = Utils.readInt64(entry.getData(), 0); + log.debug("Found output amount " + this.amount); + break; + case PSBT_OUT_SCRIPT: + entry.checkOneByteKey(); + Script script = new Script(entry.getData()); + this.script = script; + log.debug("Found output script hex " + Utils.bytesToHex(script.getProgram()) + " script " + script); + break; case PSBT_OUT_PROPRIETARY: proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData())); log.debug("Found proprietary output " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData())); @@ -103,7 +125,7 @@ public class PSBTOutput { } } - public List getOutputEntries() { + public List getOutputEntries(int psbtVersion) { List entries = new ArrayList<>(); if(redeemScript != null) { @@ -118,6 +140,17 @@ public class PSBTOutput { entries.add(populateEntry(PSBT_OUT_BIP32_DERIVATION, entry.getKey().getPubKey(), serializeKeyDerivation(entry.getValue()))); } + if(psbtVersion >= 2) { + if(amount != null) { + byte[] amountBytes = new byte[64]; + Utils.int64ToByteArrayLE(amount, amountBytes, 0); + entries.add(populateEntry(PSBT_OUT_AMOUNT, null, amountBytes)); + } + if(script != null) { + entries.add(populateEntry(PSBT_OUT_SCRIPT, null, script.getProgram())); + } + } + for(Map.Entry entry : proprietary.entrySet()) { entries.add(populateEntry(PSBT_OUT_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue()))); } @@ -145,6 +178,15 @@ public class PSBTOutput { } derivedPublicKeys.putAll(psbtOutput.derivedPublicKeys); + + if(psbtOutput.amount != null) { + amount = psbtOutput.amount; + } + + if(psbtOutput.script != null) { + script = psbtOutput.script; + } + proprietary.putAll(psbtOutput.proprietary); tapDerivedPublicKeys.putAll(psbtOutput.tapDerivedPublicKeys); @@ -178,6 +220,38 @@ public class PSBTOutput { return derivedPublicKeys; } + public Long getAmount() { + if(psbt.getPsbtVersion() >= 2) { + return amount; + } + + return getOutput().getValue(); + } + + Long amount() { + return amount; + } + + public void setAmount(Long amount) { + this.amount = amount; + } + + public Script getScript() { + if(psbt.getPsbtVersion() >= 2) { + return script; + } + + return getOutput().getScript(); + } + + Script script() { + return script; + } + + public void setScript(Script script) { + this.script = script; + } + public Map getProprietary() { return proprietary; } @@ -198,6 +272,14 @@ public class PSBTOutput { this.tapInternalKey = tapInternalKey; } + public TransactionOutput getOutput() { + return psbt.getTransaction().getOutputs().get(index); + } + + void setIndex(int index) { + this.index = index; + } + public void clearNonFinalFields() { tapDerivedPublicKeys.clear(); } diff --git a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java index 0fbfbd9..016c80e 100644 --- a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java +++ b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java @@ -207,7 +207,7 @@ public class PSBTTest { @Test public void validUnknownInputs() throws PSBTParseException { - String psbt = "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA="; + String psbt = "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACvABAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA="; PSBT psbt1 = PSBT.fromString(psbt); Assertions.assertEquals(1, psbt1.getPsbtInputs().size()); @@ -406,6 +406,318 @@ public class PSBTTest { Assertions.assertEquals("3fd9781d", tapOutKeyDerivations.keySet().iterator().next().getMasterFingerprint()); } + @Test + public void testPSBTv0InvalidVersion() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAH7BAIAAAAAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BCGsCRzBEAiAFJ1pIVzTgrh87lxI3WG8OctyFgz0njA5HTNIxEsD6XgIgawSMg868PEHQuTzH2nYYXO29Aw0AWwgBi+K5i7rL33sBIQN2DcygXzmX3GWykwYPfynxUUyMUnBI4SgCsEHU/DQKJwAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_UNSIGNED_TX is not allowed in PSBTv2", e.getMessage()); + } + + @Test + public void testPSBTv0TxVersion() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAECBAIAAAAAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BCGsCRzBEAiAFJ1pIVzTgrh87lxI3WG8OctyFgz0njA5HTNIxEsD6XgIgawSMg868PEHQuTzH2nYYXO29Aw0AWwgBi+K5i7rL33sBIQN2DcygXzmX3GWykwYPfynxUUyMUnBI4SgCsEHU/DQKJwAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_TX_VERSION is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0FallbackLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAEDBAIAAAAAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BCGsCRzBEAiAFJ1pIVzTgrh87lxI3WG8OctyFgz0njA5HTNIxEsD6XgIgawSMg868PEHQuTzH2nYYXO29Aw0AWwgBi+K5i7rL33sBIQN2DcygXzmX3GWykwYPfynxUUyMUnBI4SgCsEHU/DQKJwAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_FALLBACK_LOCKTIME is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0InputCount() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAEEAQIAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BCGsCRzBEAiAFJ1pIVzTgrh87lxI3WG8OctyFgz0njA5HTNIxEsD6XgIgawSMg868PEHQuTzH2nYYXO29Aw0AWwgBi+K5i7rL33sBIQN2DcygXzmX3GWykwYPfynxUUyMUnBI4SgCsEHU/DQKJwAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_INPUT_COUNT is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0OutputCount() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAEFAQIAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BCGsCRzBEAiAFJ1pIVzTgrh87lxI3WG8OctyFgz0njA5HTNIxEsD6XgIgawSMg868PEHQuTzH2nYYXO29Aw0AWwgBi+K5i7rL33sBIQN2DcygXzmX3GWykwYPfynxUUyMUnBI4SgCsEHU/DQKJwAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_OUTPUT_COUNT is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0Modifiable() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAEGAQAAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BCGsCRzBEAiAFJ1pIVzTgrh87lxI3WG8OctyFgz0njA5HTNIxEsD6XgIgawSMg868PEHQuTzH2nYYXO29Aw0AWwgBi+K5i7rL33sBIQN2DcygXzmX3GWykwYPfynxUUyMUnBI4SgCsEHU/DQKJwAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_TX_MODIFIABLE is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0PrevTxid() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEIawJHMEQCIAUnWkhXNOCuHzuXEjdYbw5y3IWDPSeMDkdM0jESwPpeAiBrBIyDzrw8QdC5PMfadhhc7b0DDQBbCAGL4rmLusvfewEhA3YNzKBfOZfcZbKTBg9/KfFRTIxScEjhKAKwQdT8NAonAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gAIgIC1gH4SEamdV93a+AOPZ3o+xCsyTX7g8RfsBYtTK1at5IY9p2HPlQAAIABAACAAAAAgAAAAAAqAAAAACICA27+LCVWIZhlU7qdZcPdxkFlyhQ24FqjWkxusCRRz3ltGPadhz5UAACAAQAAgAAAAIABAAAAYgAAAAA="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_IN_PREV_TXID is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0OutputIndex() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEIawJHMEQCIAUnWkhXNOCuHzuXEjdYbw5y3IWDPSeMDkdM0jESwPpeAiBrBIyDzrw8QdC5PMfadhhc7b0DDQBbCAGL4rmLusvfewEhA3YNzKBfOZfcZbKTBg9/KfFRTIxScEjhKAKwQdT8NAonAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_IN_OUTPUT_INDEX is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0Sequence() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEIawJHMEQCIAUnWkhXNOCuHzuXEjdYbw5y3IWDPSeMDkdM0jESwPpeAiBrBIyDzrw8QdC5PMfadhhc7b0DDQBbCAGL4rmLusvfewEhA3YNzKBfOZfcZbKTBg9/KfFRTIxScEjhKAKwQdT8NAonARAE/////wAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_IN_SEQUENCE is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0RequiredTimeLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEIawJHMEQCIAUnWkhXNOCuHzuXEjdYbw5y3IWDPSeMDkdM0jESwPpeAiBrBIyDzrw8QdC5PMfadhhc7b0DDQBbCAGL4rmLusvfewEhA3YNzKBfOZfcZbKTBg9/KfFRTIxScEjhKAKwQdT8NAonAREEjI3EYgAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_IN_REQUIRED_TIME_LOCKTIME is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0RequiredHeightLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEIawJHMEQCIAUnWkhXNOCuHzuXEjdYbw5y3IWDPSeMDkdM0jESwPpeAiBrBIyDzrw8QdC5PMfadhhc7b0DDQBbCAGL4rmLusvfewEhA3YNzKBfOZfcZbKTBg9/KfFRTIxScEjhKAKwQdT8NAonARIEECcAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAAAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_IN_REQUIRED_HEIGHT_LOCKTIME is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0OutputAmount() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEIawJHMEQCIAUnWkhXNOCuHzuXEjdYbw5y3IWDPSeMDkdM0jESwPpeAiBrBIyDzrw8QdC5PMfadhhc7b0DDQBbCAGL4rmLusvfewEhA3YNzKBfOZfcZbKTBg9/KfFRTIxScEjhKAKwQdT8NAonACICAtYB+EhGpnVfd2vgDj2d6PsQrMk1+4PEX7AWLUytWreSGPadhz5UAACAAQAAgAAAAIAAAAAAKgAAAAEDCAAIry8AAAAAACICA27+LCVWIZhlU7qdZcPdxkFlyhQ24FqjWkxusCRRz3ltGPadhz5UAACAAQAAgAAAAIABAAAAYgAAAAA="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_OUT_AMOUNT is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv0OutputScript() throws PSBTParseException { + String strPsbt = "cHNidP8BAHECAAAAAQsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAAAAAAD+////AgAIry8AAAAAFgAUxDD2TEdW2jENvRoIVXLvKZkmJyyLvesLAAAAABYAFKB9rIq2ypQtN57Xlfg1unHJzGiFAAAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEIawJHMEQCIAUnWkhXNOCuHzuXEjdYbw5y3IWDPSeMDkdM0jESwPpeAiBrBIyDzrw8QdC5PMfadhhc7b0DDQBbCAGL4rmLusvfewEhA3YNzKBfOZfcZbKTBg9/KfFRTIxScEjhKAKwQdT8NAonACICAtYB+EhGpnVfd2vgDj2d6PsQrMk1+4PEX7AWLUytWreSGPadhz5UAACAAQAAgAAAAIAAAAAAKgAAAAEEFgAUoH2sirbKlC03nteV+DW6ccnMaIUAIgIDbv4sJVYhmGVTup1lw93GQWXKFDbgWqNaTG6wJFHPeW0Y9p2HPlQAAIABAACAAAAAgAEAAABiAAAAAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_OUT_SCRIPT is not allowed in PSBTv0", e.getMessage()); + } + + @Test + public void testPSBTv2MissingInputCount() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAARAE/v///wAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_INPUT_COUNT is required in PSBTv2", e.getMessage()); + } + + @Test + public void testPSBTv2MissingOutputCount() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAARAE/v///wAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt)); + Assertions.assertEquals("PSBT_GLOBAL_OUTPUT_COUNT is required in PSBTv2", e.getMessage()); + } + + @Test + public void testPSBTv2MissingPrevTxid() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEPBAAAAAABEAT+////ACICAtYB+EhGpnVfd2vgDj2d6PsQrMk1+4PEX7AWLUytWreSGPadhz5UAACAAQAAgAAAAIAAAAAAKgAAAAEDCAAIry8AAAAAAQQWABTEMPZMR1baMQ29GghVcu8pmSYnLAAiAgLjb7/1PdU0Bwz4/TlmFGgPNXqbhdtzQL8c+nRdKtezQBj2nYc+VAAAgAEAAIAAAACAAQAAAGQAAAABAwiLvesLAAAAAAEEFgAUTdGTrJZKVqwbnhzKhFT+L0dPhRMA"; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt, true, false)); + Assertions.assertEquals("PSBT_IN_PREV_TXID is required in PSBTv2", e.getMessage()); + } + + @Test + public void testPSBTv2MissingOutputIndex() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IARAE/v///wAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt, true, false)); + Assertions.assertEquals("PSBT_IN_OUTPUT_INDEX is required in PSBTv2", e.getMessage()); + } + + @Test + public void testPSBTv2MissingOutputAmount() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAEQBP7///8AIgIC1gH4SEamdV93a+AOPZ3o+xCsyTX7g8RfsBYtTK1at5IY9p2HPlQAAIABAACAAAAAgAAAAAAqAAAAAQQWABTEMPZMR1baMQ29GghVcu8pmSYnLAAiAgLjb7/1PdU0Bwz4/TlmFGgPNXqbhdtzQL8c+nRdKtezQBj2nYc+VAAAgAEAAIAAAACAAQAAAGQAAAABAwiLvesLAAAAAAEEFgAUTdGTrJZKVqwbnhzKhFT+L0dPhRMA"; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt, true, false)); + Assertions.assertEquals("PSBT_OUT_AMOUNT is required in PSBTv2", e.getMessage()); + } + + @Test + public void testPSBTv2MissingOutputScript() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAEQBP7///8AIgIC1gH4SEamdV93a+AOPZ3o+xCsyTX7g8RfsBYtTK1at5IY9p2HPlQAAIABAACAAAAAgAAAAAAqAAAAAQMIAAivLwAAAAAAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt, true, false)); + Assertions.assertEquals("PSBT_OUT_SCRIPT is required in PSBTv2", e.getMessage()); + } + + @Test + public void testPSBTv2SmallTimeLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAAREE/2TNHQAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt, true, false)); + Assertions.assertEquals("Required time locktime is less than 500000000", e.getMessage()); + } + + @Test + public void testPSBTv2LargeHeightLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAARIEAGXNHQAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + Exception e = Assertions.assertThrows(PSBTParseException.class, () -> PSBT.fromString(strPsbt, true, false)); + Assertions.assertEquals("Required time locktime is greater than or equal to 500000000", e.getMessage()); + } + + @Test + public void testPSBTv21Input2OutputsRequiredOnly() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + PSBT psbt = PSBT.fromString(strPsbt); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + } + + @Test + public void testPSBTv21Input2OutputsUpdated() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAACICAtYB+EhGpnVfd2vgDj2d6PsQrMk1+4PEX7AWLUytWreSGPadhz5UAACAAQAAgAAAAIAAAAAAKgAAAAEDCAAIry8AAAAAAQQWABTEMPZMR1baMQ29GghVcu8pmSYnLAAiAgLjb7/1PdU0Bwz4/TlmFGgPNXqbhdtzQL8c+nRdKtezQBj2nYc+VAAAgAEAAIAAAACAAQAAAGQAAAABAwiLvesLAAAAAAEEFgAUTdGTrJZKVqwbnhzKhFT+L0dPhRMA"; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + } + + @Test + public void testPSBTv21Input2OutputsSequence() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAARAE/v///wAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + Assertions.assertEquals(4294967294L, psbt.getPsbtInputs().get(0).getSequence()); + } + + @Test + public void testPSBTv21Input2OutputsSequenceLocktimes() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAEQBP7///8BEQSMjcRiARIEECcAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + Assertions.assertEquals(4294967294L, psbt.getPsbtInputs().get(0).getSequence()); + Assertions.assertEquals(1657048460, psbt.getPsbtInputs().get(0).getRequiredTimeLocktime()); + Assertions.assertEquals(10000, psbt.getPsbtInputs().get(0).getRequiredHeightLocktime()); + } + + @Test + public void testPSBTv21Input2OutputsModifiableBit0() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEBAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + Assertions.assertEquals((byte)0x01, psbt.getModifiable()); + } + + @Test + public void testPSBTv21Input2OutputsModifiableBit1() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgECAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + Assertions.assertEquals((byte)0x02, psbt.getModifiable()); + } + + @Test + public void testPSBTv21Input2OutputsModifiableAllBits() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIBBgEHAfsEAgAAAAABAFICAAAAAcGqJW4hS5ahgi+T3kK/87Xz/40FGTBuNRXXUVpegFsSAAAAAAD/////ARjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4AAAAAAQEfGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + Assertions.assertEquals((byte)0x07, psbt.getModifiable()); + } + + @Test + public void testPSBTv21Input2OutputsAllFields() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAQYBBwH7BAIAAAAAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BDiALCtkhQZwchxlzXXLcc5+eqeBjjR/kwe7w+ZRAhIFfyAEPBAAAAAABEAT+////AREEjI3EYgESBBAnAAAAIgIC1gH4SEamdV93a+AOPZ3o+xCsyTX7g8RfsBYtTK1at5IY9p2HPlQAAIABAACAAAAAgAAAAAAqAAAAAQMIAAivLwAAAAABBBYAFMQw9kxHVtoxDb0aCFVy7ymZJicsACICAuNvv/U91TQHDPj9OWYUaA81epuF23NAvxz6dF0q17NAGPadhz5UAACAAQAAgAAAAIABAAAAZAAAAAEDCIu96wsAAAAAAQQWABRN0ZOslkpWrBueHMqEVP4vR0+FEwA="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1, psbt.getInputCount()); + Assertions.assertEquals(2, psbt.getOutputCount()); + Assertions.assertEquals((byte)0x07, psbt.getModifiable()); + Assertions.assertEquals(4294967294L, psbt.getPsbtInputs().get(0).getSequence()); + Assertions.assertEquals(1657048460, psbt.getPsbtInputs().get(0).getRequiredTimeLocktime()); + Assertions.assertEquals(10000, psbt.getPsbtInputs().get(0).getRequiredHeightLocktime()); + Assertions.assertEquals(800000000, psbt.getPsbtOutputs().get(0).getAmount()); + Assertions.assertEquals(new Script(Utils.hexToBytes("0014c430f64c4756da310dbd1a085572ef299926272c")), psbt.getPsbtOutputs().get(0).getScript()); + Assertions.assertEquals(199998859, psbt.getPsbtOutputs().get(1).getAmount()); + Assertions.assertEquals(new Script(Utils.hexToBytes("00144dd193ac964a56ac1b9e1cca8454fe2f474f8513")), psbt.getPsbtOutputs().get(1).getScript()); + } + + @Test + public void testPSBTv2NoLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA=="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(0, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv2ZeroFallbackLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAAAAQ4gOhs7PIN9ZInqejHY5sfdUDwAG+8+BpWOdXSAjWjKeKUBDwQAAAAAAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(0, psbt.getFallbackLocktime()); + Assertions.assertEquals(0, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv21InputLocktime() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAABAwhPkzV3AAAAAAEEFgAUCxNSys0Dz2qht/PI1jiGcbNKXhEA"; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(10000, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv22InputsLocktimeHeight() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAESBCgjAAAAAQMIT5M1dwAAAAABBBYAFAsTUsrNA89qobfzyNY4hnGzSl4RAA=="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(10000, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv22InputsLocktimeMixed() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAERBIyNxGIBEgQoIwAAAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(10000, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv22InputsLocktimeMixed2() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEQSLjcRiARIEECcAAAABDiA6Gzs8g31kiep6Mdjmx91QPAAb7z4GlY51dICNaMp4pQEPBAAAAAABEQSMjcRiARIEKCMAAAABAwhPkzV3AAAAAAEEFgAUCxNSys0Dz2qht/PI1jiGcbNKXhEA"; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(10000, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv22InputsLocktimeTimeMixed() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEQSLjcRiAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAERBIyNxGIBEgQoIwAAAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1657048460, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv22InputsLocktimeTimeMixed2() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEQSLjcRiARIEECcAAAABDiA6Gzs8g31kiep6Mdjmx91QPAAb7z4GlY51dICNaMp4pQEPBAAAAAABEQSMjcRiAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA="; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1657048460, psbt.getTransaction().getLocktime()); + } + + @Test + public void testPSBTv22InputsLocktimeTimeMixed3() throws PSBTParseException { + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAAAAQ4gOhs7PIN9ZInqejHY5sfdUDwAG+8+BpWOdXSAjWjKeKUBDwQAAAAAAREEjI3EYgABAwhPkzV3AAAAAAEEFgAUCxNSys0Dz2qht/PI1jiGcbNKXhEA"; + PSBT psbt = PSBT.fromString(strPsbt, true, false); + Assertions.assertEquals(1657048460, psbt.getTransaction().getLocktime()); + } + + @Test + public void convertPSBTv0ToPSBTv2() throws PSBTParseException { + Network.set(Network.TESTNET); + String strPsbt = "cHNidP8BAH0CAAAAAdPUSBYKaQKOqAMgU2IcGuM6z7JtbkLe69OmYZoa4UxYAAAAAADmdQAAAp4CAAAAAAAAFgAUg+/zyGWbtKbIJb8ZasbGYDtItZhgrgEAAAAAACJRIJMQKWeQsI/WiRS+lmeeyeJaKFVnlVoBtXeuGTO7XIbrAAAAAE8BBDWHzwM27GqkgAAAAO1YZge2AP67ozhdoF9wgg2hpJw1jbVEXLKfxRQUNGEIAlog/wK83w7jxD37prhWrPenLxjGAJzkJKrj6h0ZPK8WED/ZeB1WAACAAQAAgAAAAIAAAQCJAgAAAAGBuBuu5OIheS4SKtYJufScCJDTWWLopBoXtFWhPDuRWQEAAAAA/f///wKNsQEAAAAAACJRIAjXFSnHwcD+J/obgd9CxVneHsUyFKM9xU7NY5K3DgCxHwIAAAAAAAAiUSAhUe66hJMCB1esHqXtxRVJHmviQ4ZjzFIwDWDPk+1KY6VyIQABASuNsQEAAAAAACJRIAjXFSnHwcD+J/obgd9CxVneHsUyFKM9xU7NY5K3DgCxAQMEAAAAAAETQCG/ZGuefjDVqBhmgVEuV1HbdxoZKDDWWTvTUrq6MJreRzj22k/WcFni6yPn9PGZkptZSNx9waf8ouP28ogJz24hFnRZPMvN82XI+lnim7dRKwFgpHnqiDoMGnIoFoSQRX7bGQA/2XgdVgAAgAEAAIAAAACAAQAAAAIAAAABFyB0WTzLzfNlyPpZ4pu3USsBYKR56og6DBpyKBaEkEV+2wAAIQflpK6JYWolG3K2DU7FU3hlkwSdU/69bglhZxaprqSvyxkAP9l4HVYAAIABAACAAAAAgAEAAAADAAAAAQUg5aSuiWFqJRtytg1OxVN4ZZMEnVP+vW4JYWcWqa6kr8sA"; + PSBT origPsbtv0 = PSBT.fromString(strPsbt); + PSBT psbtv0 = PSBT.fromString(strPsbt); + psbtv0.convertVersion(2); + PSBT psbtv2 = PSBT.fromString(psbtv0.toBase64String()); + Assertions.assertEquals(origPsbtv0.getTransaction().getTxId(), psbtv2.getTransaction().getTxId()); + } + + @Test + public void convertPSBTv2ToPSBTv0() throws PSBTParseException { + Network.set(Network.TESTNET); + String strPsbt = "cHNidP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAQYBBwH7BAIAAAAAAQBSAgAAAAHBqiVuIUuWoYIvk95Cv/O18/+NBRkwbjUV11FaXoBbEgAAAAAA/////wEYxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAAAAAAEBHxjGmjsAAAAAFgAUsKOvFEIIQSaTyn0WaFK1LbCu8G4BDiALCtkhQZwchxlzXXLcc5+eqeBjjR/kwe7w+ZRAhIFfyAEPBAAAAAABEAT+////AREEjI3EYgESBBAnAAAAIgIC1gH4SEamdV93a+AOPZ3o+xCsyTX7g8RfsBYtTK1at5IY9p2HPlQAAIABAACAAAAAgAAAAAAqAAAAAQMIAAivLwAAAAABBBYAFMQw9kxHVtoxDb0aCFVy7ymZJicsACICAuNvv/U91TQHDPj9OWYUaA81epuF23NAvxz6dF0q17NAGPadhz5UAACAAQAAgAAAAIABAAAAZAAAAAEDCIu96wsAAAAAAQQWABRN0ZOslkpWrBueHMqEVP4vR0+FEwA="; + PSBT origPsbtv2 = PSBT.fromString(strPsbt, true, false); + PSBT psbtv2 = PSBT.fromString(strPsbt, true, false); + psbtv2.convertVersion(0); + PSBT psbtv0 = PSBT.fromString(psbtv2.toBase64String(), true, false); + Assertions.assertEquals(origPsbtv2.getTransaction().getTxId(), psbtv0.getTransaction().getTxId()); + } + @AfterEach public void tearDown() throws Exception { Network.set(null);