@@ -1102,6 +1102,86 @@ class PsbtTestsCommon {
11021102 assertNotNull(finalTx.right)
11031103 }
11041104
1105+ @Test
1106+ fun `bump lightning commit tx fee from cold wallet with P2A output` () {
1107+ // A lightning node prepares a PSBT that spends the anchor output of a commitment transaction.
1108+ val lightningPsbt = run {
1109+ val txToBump = Transaction (3 , listOf (), listOf (TxOut (0 .sat(), Script .pay2anchor)), 0 )
1110+ val lightningPsbt = Psbt (Transaction (3 , listOf (TxIn (OutPoint (txToBump, 0 ), ByteVector .empty, 0 , Script .witnessPay2anchor)), listOf (), 0 ))
1111+ .updateWitnessInput(OutPoint (txToBump, 0 ), txToBump.txOut[0 ], null , Script .pay2anchor, SIGHASH_ALL )
1112+ assertTrue(lightningPsbt.isRight)
1113+ lightningPsbt.right!!
1114+ }
1115+
1116+ // A cold wallet adds inputs and finalizes a transaction that bumps the fees of the commitment transaction.
1117+ val walletPrivKey = PrivateKey (ByteVector32 (" 0202020202020202020202020202020202020202020202020202020202020202" ))
1118+ val confirmedTx = Transaction (2 , listOf (), listOf (TxOut (100_000 .sat(), Script .pay2wpkh(walletPrivKey.publicKey()))), 0 )
1119+ val finalTx = Psbt .join(
1120+ lightningPsbt,
1121+ Psbt (Transaction (3 , listOf (TxIn (OutPoint (confirmedTx, 0 ), 0 )), listOf (TxOut (75_000 .sat(), Script .pay2wpkh(walletPrivKey.publicKey()))), 0 ))
1122+ ).flatMap {
1123+ it.updateWitnessInputTx(confirmedTx, 0 , null , Script .pay2pkh(walletPrivKey.publicKey()))
1124+ }.flatMap {
1125+ it.sign(walletPrivKey, 1 )
1126+ }.flatMap {
1127+ it.psbt.finalizeWitnessInput(0 , Script .witnessPay2anchor)
1128+ }.flatMap {
1129+ it.finalizeWitnessInput(1 , Script .witnessPay2wpkh(walletPrivKey.publicKey(), it.inputs[1 ].partialSigs.getValue(walletPrivKey.publicKey())))
1130+ }.flatMap {
1131+ it.extract()
1132+ }
1133+ assertTrue(finalTx.isRight)
1134+ assertNotNull(finalTx.right)
1135+ }
1136+
1137+ @Test
1138+ fun `read PSBT with P2A input` () {
1139+ val walletPrivKey = PrivateKey (ByteVector32 (" 0202020202020202020202020202020202020202020202020202020202020202" ))
1140+ val walletTx = Transaction (2 , listOf (TxIn (OutPoint (TxId (" 75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858" ), 2 ), ByteVector .empty, 0 )), listOf (TxOut (100_000 .sat(), Script .pay2wpkh(walletPrivKey.publicKey()))), 0 )
1141+ val txToBump = Transaction (3 , listOf (TxIn (OutPoint (TxId (" 0a6a357e2f7796444e02638749d9611c008b253fb55f5dc88b739b230ed0c4c3" ), 1 ), ByteVector .empty, 0 )), listOf (TxOut (0 .sat(), Script .pay2anchor), TxOut (50_000 .sat(), Script .pay2wpkh(walletPrivKey.publicKey()))), 0 )
1142+ val dummyTx = Transaction (
1143+ version = 3 ,
1144+ txIn = listOf (
1145+ TxIn (OutPoint (txToBump, 0 ), ByteVector .empty, 0 ),
1146+ TxIn (OutPoint (walletTx, 0 ), ByteVector .empty, 0 ),
1147+ ),
1148+ txOut = listOf (
1149+ TxOut (90_000 .sat(), Script .pay2wpkh(walletPrivKey.publicKey()))
1150+ ),
1151+ lockTime = 0
1152+ )
1153+ val psbt1 = Psbt (dummyTx)
1154+ .updateWitnessInput(OutPoint (walletTx, 0 ), walletTx.txOut[0 ], null , Script .pay2pkh(walletPrivKey.publicKey()))
1155+ .flatMap { it.updateWitnessInput(OutPoint (txToBump, 0 ), txToBump.txOut[0 ], null , null ) }
1156+ .flatMap { it.sign(walletPrivKey, 1 ) }
1157+ .flatMap { it.psbt.finalizeWitnessInput(1 , Script .witnessPay2wpkh(walletPrivKey.publicKey(), it.psbt.inputs[1 ].partialSigs.getValue(walletPrivKey.publicKey()))) }
1158+ .right
1159+ assertNotNull(psbt1)
1160+ val finalTx1 = psbt1.finalizeWitnessInput(0 , Script .witnessPay2anchor).flatMap { it.extract() }.right
1161+ assertNotNull(finalTx1)
1162+ Transaction .correctlySpends(finalTx1, listOf (walletTx, txToBump), ScriptFlags .STANDARD_SCRIPT_VERIFY_FLAGS )
1163+
1164+ val psbt2 = Psbt (dummyTx)
1165+ .updateWitnessInput(OutPoint (walletTx, 0 ), walletTx.txOut[0 ], null , Script .pay2pkh(walletPrivKey.publicKey()))
1166+ .flatMap { it.updateWitnessInputTx(txToBump, 0 ) }
1167+ .flatMap { it.sign(walletPrivKey, 1 ) }
1168+ .flatMap { it.psbt.finalizeWitnessInput(1 , Script .witnessPay2wpkh(walletPrivKey.publicKey(), it.psbt.inputs[1 ].partialSigs.getValue(walletPrivKey.publicKey()))) }
1169+ .right
1170+ assertNotNull(psbt2)
1171+ val finalTx2 = psbt2.finalizeWitnessInput(0 , Script .witnessPay2anchor).flatMap { it.extract() }.right
1172+ assertNotNull(finalTx2)
1173+ Transaction .correctlySpends(finalTx2, listOf (walletTx, txToBump), ScriptFlags .STANDARD_SCRIPT_VERIFY_FLAGS )
1174+
1175+ // When serializing PSBTs, Bitcoin Core skips empty witnesses instead of encoding explicitly an empty witness stack.
1176+ // This means that it essentially reverts the finalization of the P2A output that we may have previously done.
1177+ // But this is fine: when reading a P2A input, we should always set its witness to an empty witness, which finalizes it.
1178+ listOf (psbt1, psbt2).forEach { psbt ->
1179+ val finalTx = Psbt .read(Psbt .write(psbt)).right?.extract()?.right
1180+ assertNotNull(finalTx)
1181+ Transaction .correctlySpends(finalTx, listOf (walletTx, txToBump), ScriptFlags .STANDARD_SCRIPT_VERIFY_FLAGS )
1182+ }
1183+ }
1184+
11051185 @Test
11061186 fun `manual coinjoin workflow` () {
11071187 val alicePrivKey = DeterministicWallet .derivePrivateKey(masterPrivKey, KeyPath (" m/0'/0'/1'" )).privateKey
0 commit comments