add psbt v2 support

This commit is contained in:
Craig Raw 2024-11-15 12:15:41 +02:00
parent 4564c5d25a
commit 96df6284e1
5 changed files with 1074 additions and 64 deletions

View file

@ -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<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
private final Map<String, String> 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<PSBTInput> psbtInputs = new ArrayList<>();
private final List<PSBTOutput> 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<List<PSBTEntry>> inputEntryLists, boolean verifySignatures) throws PSBTParseException {
private void parseInputEntries(List<List<PSBTEntry>> inputEntryLists) throws PSBTParseException {
for(List<PSBTEntry> 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(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");
}
}
if(verifySignatures) {
verifySignatures(psbtInputs);
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<PSBTEntry> entries) {
Set<String> 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<PSBTInput> 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<PSBTEntry> getGlobalEntries() {
List<PSBTEntry> 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<PSBTEntry> inputEntries = psbtInput.getInputEntries();
List<PSBTEntry> 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<PSBTEntry> outputEntries = psbtOutput.getOutputEntries();
List<PSBTEntry> 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 <T> void moveItem(List<T> 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<String, String> 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<PSBTInput> 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);
}
}

View file

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

View file

@ -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<String, String> proprietary = new LinkedHashMap<>();
private TransactionSignature tapKeyPathSignature;
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> 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<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> 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<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey, boolean alwaysAddNonWitnessTx) {
this(psbt, index);
if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) {
this.witnessUtxo = utxo.getOutputs().get(utxoIndex);
@ -91,19 +109,31 @@ public class PSBTInput {
this.sigHash = getDefaultSigHash();
}
PSBTInput(PSBT psbt, List<PSBTEntry> inputEntries, Transaction transaction, int index) throws PSBTParseException {
this.psbt = psbt;
for(PSBTEntry entry : inputEntries) {
switch(entry.getKeyType()) {
PSBTInput(PSBT psbt, List<PSBTEntry> inputEntries, int index) throws PSBTParseException {
this(psbt, index);
List<PSBTEntry> 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();
if(psbt.isVerifyPrevTxids()) {
Sha256Hash inputHash = nonWitnessTx.calculateTxId(false);
Sha256Hash outpointHash = transaction.getInputs().get(index).getOutpoint().getHash();
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;
log.debug("Found input non witness utxo with txid: " + nonWitnessTx.getTxId() + " version " + nonWitnessTx.getVersion() + " size " + nonWitnessTx.getMessageSize() + " locktime " + nonWitnessTx.getLocktime());
@ -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<PSBTEntry> getInputEntries() {
public List<PSBTEntry> getInputEntries(int psbtVersion) {
List<PSBTEntry> 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<String, String> 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<TransactionOutput> 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;

View file

@ -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<ECKey, Map<KeyDerivation, List<Sha256Hash>>> 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<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey) {
PSBTOutput(PSBT psbt, int index, ScriptType scriptType, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey) {
this(psbt, index);
this.redeemScript = redeemScript;
this.witnessScript = witnessScript;
@ -53,9 +63,10 @@ public class PSBTOutput {
}
}
PSBTOutput(List<PSBTEntry> outputEntries) throws PSBTParseException {
PSBTOutput(PSBT psbt, List<PSBTEntry> 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<PSBTEntry> getOutputEntries() {
public List<PSBTEntry> getOutputEntries(int psbtVersion) {
List<PSBTEntry> 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<String, String> 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<String, String> 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();
}

View file

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