diff --git a/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala b/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala index d930595910..394564db4a 100644 --- a/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala @@ -348,14 +348,14 @@ class PSBTUnitTest extends BitcoinSAsyncTest { } def dummyTx( + prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty, scriptSig: ScriptSignature = EmptyScriptSignature, spk: ScriptPubKey = EmptyScriptPubKey): Transaction = { BaseTransaction( version = Int32.zero, inputs = Vector( - TransactionInput(outPoint = - TransactionOutPoint(txId = DoubleSha256Digest.empty, - vout = UInt32.zero), + TransactionInput(outPoint = TransactionOutPoint(txId = prevTxId, + vout = UInt32.zero), scriptSignature = scriptSig, sequenceNumber = UInt32.zero)), outputs = Vector(TransactionOutput(CurrencyUnits.oneBTC, spk)), @@ -364,9 +364,10 @@ class PSBTUnitTest extends BitcoinSAsyncTest { } def dummyPSBT( + prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty, scriptSig: ScriptSignature = EmptyScriptSignature, spk: ScriptPubKey = EmptyScriptPubKey): PSBT = { - PSBT.fromUnsignedTx(dummyTx(scriptSig, spk)) + PSBT.fromUnsignedTx(dummyTx(prevTxId, scriptSig, spk)) } it must "successfully change a NonWitnessUTXO to a WitnessUTXO when compressing" in { @@ -517,4 +518,28 @@ class PSBTUnitTest extends BitcoinSAsyncTest { "70736274ff01005502000000010e43b32dc23232b9c74bb0d4b940e6242c8acc875f41a6e4f6063e99dcbe4eda0000000000000000000180f0fa02000000001976a9149d936768e7338f716548af87b61ad83b80ea422188ac000000000001002a02000000000100e1f5050000000017a914e9b5fdcca093fde8d0424238a517034664c4715a8700000000010717160014cbcdc27013a54990a8e468dbdabf95166ca614e701086b024730440220489caebd034c1c1caf869d1463543248d78b9c8fde4cdd6044220c123ba9489c022007214b4ac24f5b4e278172b8af2fed27990fb10fa1961b3288dd0fc9226ef05d012102b38f6ec85730be3e8fa5a0da221209781de35c2e572e82f2d707e7b67be1b9c20000") assert(psbt.verifyFinalizedInput(0)) } + + it must "correctly add a non-witness utxo when there is a witness v0 redeem script" in { + + val witScript = P2PKHScriptPubKey(ECPublicKey.freshPublicKey) + val witness = P2WSHWitnessV0(witScript) + val redeemScript = P2WSHWitnessSPKV0(witScript) + val p2sh = P2SHScriptPubKey(redeemScript) + + val utxoTx = dummyTx(spk = p2sh) + + val psbtTx = dummyTx(prevTxId = utxoTx.txId) + + val psbt = PSBT + .fromUnsignedTx(psbtTx) + .addScriptWitnessToInput(witness, 0) + .addRedeemOrWitnessScriptToInput(redeemScript, 0) + + val withUtxo = psbt.addUTXOToInput(utxoTx, 0) + + assert( + withUtxo.inputMaps.head.nonWitnessOrUnknownUTXOOpt + .map(_.transactionSpent) + .contains(utxoTx)) + } } diff --git a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala index 435e8b194c..aa69da5d60 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala @@ -225,8 +225,13 @@ case class PSBT( inputMap.redeemScriptOpt.isDefined && WitnessScriptPubKey .isWitnessScriptPubKey( inputMap.redeemScriptOpt.get.redeemScript.asm) - val notBIP143Vulnerable = - !out.scriptPubKey.isInstanceOf[WitnessScriptPubKeyV0] + val notBIP143Vulnerable = { + !out.scriptPubKey.isInstanceOf[WitnessScriptPubKeyV0] && !( + hasWitRedeemScript && + inputMap.redeemScriptOpt.get.redeemScript + .isInstanceOf[WitnessScriptPubKeyV0] + ) + } if ( (outIsWitnessScript || hasWitScript || hasWitRedeemScript) && notBIP143Vulnerable @@ -369,6 +374,46 @@ case class PSBT( PSBT(globalMap, newInputMaps, outputMaps) } + def addFinalizedScriptWitnessToInput( + scriptSignature: ScriptSignature, + scriptWitness: ScriptWitness, + index: Int): PSBT = { + require(index >= 0, + s"index must be greater than or equal to 0, got: $index") + require( + index < inputMaps.size, + s"index must be less than the number of input maps present in the psbt, $index >= ${inputMaps.size}") + require(!inputMaps(index).isFinalized, + s"Cannot update an InputPSBTMap that is finalized, index: $index") + + val prevInput = inputMaps(index) + + require( + prevInput.elements.forall { + case _: NonWitnessOrUnknownUTXO | _: WitnessUTXO | _: Unknown => true + case redeemScript: RedeemScript => + // RedeemScript is okay if it matches the script signature given + redeemScript.redeemScript match { + case _: NonWitnessScriptPubKey => false + case wspk: WitnessScriptPubKey => + P2SHScriptSignature(wspk) == scriptSignature + } + case _: InputPSBTRecord => false + }, + s"Input already contains fields: ${prevInput.elements}" + ) + + val finalizedScripts = Vector(FinalizedScriptSig(scriptSignature), + FinalizedScriptWitness(scriptWitness)) + + // Replace RedeemScripts with FinalizedScriptSignatures and add FinalizedScriptWitnesses + val records = prevInput.elements.filterNot( + _.isInstanceOf[RedeemScript]) ++ finalizedScripts + val newMap = InputPSBTMap(records) + val newInputMaps = inputMaps.updated(index, newMap) + PSBT(globalMap, newInputMaps, outputMaps) + } + /** * Adds script to the indexed OutputPSBTMap to either the RedeemScript * or WitnessScript field depending on the script and available information in the PSBT @@ -816,6 +861,30 @@ object PSBT extends Factory[PSBT] with StringFactory[PSBT] { PSBT(globalMap, inputMaps, outputMaps) } + def fromUnsignedTxWithP2SHScript(tx: Transaction): PSBT = { + val inputs = tx.inputs.toVector + val utxInputs = inputs.map { input => + TransactionInput(input.previousOutput, + EmptyScriptSignature, + input.sequence) + } + val utx = BaseTransaction(tx.version, utxInputs, tx.outputs, tx.lockTime) + val psbt = fromUnsignedTx(utx) + + val p2shScriptSigs = inputs + .map(_.scriptSignature) + .zipWithIndex + .collect { + case (p2sh: P2SHScriptSignature, index) => (p2sh, index) + } + .filter(_._1.scriptSignatureNoRedeemScript == EmptyScriptSignature) + + p2shScriptSigs.foldLeft(psbt) { + case (psbtSoFar, (p2sh, index)) => + psbtSoFar.addRedeemOrWitnessScriptToInput(p2sh.redeemScript, index) + } + } + /** Constructs a full (ready to be finalized) but unfinalized PSBT from an * unsigned transaction and a SpendingInfoAndNonWitnessTxs */