Fixed P2SH(Segwit) bug and brought down DLC-used PSBT functionality (#2140)

* Fixed P2SH(Segwit) bug and allowed redeem scripts when calling addFinalizedScriptWitnessToInput

* Add test

Co-authored-by: Ben Carman <benthecarman@live.com>
This commit is contained in:
Nadav Kohen 2020-10-24 18:06:46 -05:00 committed by GitHub
parent e317fa4a89
commit 3cf6d3cb0a
2 changed files with 100 additions and 6 deletions

View File

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

View File

@ -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
*/