| 1 | import "dart:typed_data"; |
| 2 | |
| 3 | import "package:ledger_bitcoin/src/psbt/constants.dart"; |
| 4 | import "package:ledger_bitcoin/src/psbt/psbtv2.dart"; |
| 5 | import "package:ledger_bitcoin/src/utils/buffer_writer.dart"; |
| 6 | |
| 7 | /// This roughly implements the "input finalizer" role of BIP370 (PSBTv2 |
| 8 | /// https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However |
| 9 | /// the role is documented in BIP174 (PSBTv0 |
| 10 | /// https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). |
| 11 | /// |
| 12 | /// Verify that all inputs have a signature, and set inputFinalScriptwitness |
| 13 | /// and/or inputFinalScriptSig depending on the type of the spent outputs. Clean |
| 14 | /// fields that aren't useful anymore, partial signatures, redeem script and |
| 15 | /// derivation paths. |
| 16 | /// |
| 17 | /// @param psbt The psbt with all signatures added as partial sigs, either |
| 18 | /// through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG |
| 19 | extension InputFinalizer on PsbtV2 { |
| 20 | void finalizeV0() { |
| 21 | // First check that each input has a signature |
| 22 | for (var i = 0; i < getGlobalInputCount(); i++) { |
| 23 | if (_isFinalized(i)) continue; |
| 24 | |
| 25 | final legacyPubkeys = getInputKeyDatas(i, PSBTIn.partialSig); |
| 26 | final taprootSig = getInputTapKeySig(i); |
| 27 | if (legacyPubkeys.isEmpty && taprootSig == null) { |
| 28 | continue; |
| 29 | // throw Exception('No signature for input $i present'); |
| 30 | } |
| 31 | if (legacyPubkeys.isNotEmpty) { |
| 32 | if (legacyPubkeys.length > 1) { |
| 33 | throw Exception('Expected exactly one signature, got ${legacyPubkeys.length}'); |
| 34 | } |
| 35 | if (taprootSig != null) { |
| 36 | throw Exception('Both taproot and non-taproot signatures present.'); |
| 37 | } |
| 38 | |
| 39 | final isSegwitV0 = getInputWitnessUtxo(i) != null; |
| 40 | final redeemScript = getInputRedeemScript(i); |
| 41 | final isWrappedSegwit = redeemScript != null; |
| 42 | final signature = getInputPartialSig(i, legacyPubkeys[0]); |
| 43 | if (signature == null) { |
| 44 | throw Exception('Expected partial signature for input $i'); |
| 45 | } |
| 46 | if (isSegwitV0) { |
| 47 | final witnessBuf = BufferWriter() |
| 48 | ..writeVarInt(2) |
| 49 | ..writeVarInt(signature.length) |
| 50 | ..writeSlice(signature) |
| 51 | ..writeVarInt(legacyPubkeys[0].length) |
| 52 | ..writeSlice(legacyPubkeys[0]); |
| 53 | setInputFinalScriptwitness(i, witnessBuf.buffer()); |
| 54 | if (isWrappedSegwit) { |
| 55 | if (redeemScript.isEmpty) { |
| 56 | throw Exception("Expected non-empty redeemscript. Can't finalize input $i"); |
| 57 | } |
| 58 | final scriptSigBuf = BufferWriter() |
| 59 | ..writeUInt8(redeemScript.length) // Push redeemScript length |
| 60 | ..writeSlice(redeemScript); |
| 61 | setInputFinalScriptsig(i, scriptSigBuf.buffer()); |
| 62 | } |
| 63 | } else { |
| 64 | // Legacy input |
| 65 | final scriptSig = BufferWriter(); |
| 66 | _writePush(scriptSig, signature); |
| 67 | _writePush(scriptSig, legacyPubkeys[0]); |
| 68 | setInputFinalScriptsig(i, scriptSig.buffer()); |
| 69 | } |
| 70 | } else { |
| 71 | // Taproot input |
| 72 | final signature = getInputTapKeySig(i); |
| 73 | if (signature == null) { |
| 74 | throw Exception("No taproot signature found"); |
| 75 | } |
| 76 | if (signature.length != 64 && signature.length != 65) { |
| 77 | throw Exception("Unexpected length of schnorr signature."); |
| 78 | } |
| 79 | final witnessBuf = BufferWriter() |
| 80 | ..writeVarInt(1) |
| 81 | ..writeVarSlice(signature); |
| 82 | setInputFinalScriptwitness(i, witnessBuf.buffer()); |
| 83 | } |
| 84 | clearFinalizedInput(i); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | /// Deletes fields that are no longer necessary from the psbt. |
| 89 | /// |
| 90 | /// Note, the spec doesn't say anything about removing output fields |
| 91 | /// like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them |
| 92 | /// without actually knowing why. I think we should remove them too. |
| 93 | void clearFinalizedInput(int inputIndex) { |
| 94 | final keyTypes = [ |
| 95 | PSBTIn.bip32Derivation, |
| 96 | PSBTIn.partialSig, |
| 97 | PSBTIn.tapBip32Derivation, |
| 98 | PSBTIn.tapKeySig, |
| 99 | ]; |
| 100 | final witnessUtxoAvailable = getInputWitnessUtxo(inputIndex) != null; |
| 101 | final nonWitnessUtxoAvailable = getInputNonWitnessUtxo(inputIndex) != null; |
| 102 | if (witnessUtxoAvailable && nonWitnessUtxoAvailable) { |
| 103 | // Remove NON_WITNESS_UTXO for segwit v0 as it's only needed while signing. |
| 104 | // Segwit v1 doesn't have NON_WITNESS_UTXO set. |
| 105 | // See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#cite_note-7 |
| 106 | keyTypes.add(PSBTIn.nonWitnessUTXO); |
| 107 | } |
| 108 | deleteInputEntries(inputIndex, keyTypes); |
| 109 | } |
| 110 | |
| 111 | /// Writes a script push operation to buf, which looks different |
| 112 | /// depending on the size of the data. See |
| 113 | /// https://en.bitcoin.it/wiki/Script#finalants |
| 114 | /// |
| 115 | /// [buf] the BufferWriter to write to |
| 116 | /// [data] the Buffer to be pushed. |
| 117 | void _writePush(BufferWriter buf, Uint8List data) { |
| 118 | if (data.length <= 75) { |
| 119 | buf.writeUInt8(data.length); |
| 120 | } else if (data.length <= 256) { |
| 121 | buf.writeUInt8(76); |
| 122 | buf.writeUInt8(data.length); |
| 123 | } else if (data.length <= 256 * 256) { |
| 124 | buf.writeUInt8(77); |
| 125 | final b = ByteData(2)..setUint16(0, data.length, Endian.little); |
| 126 | buf.writeSlice(b.buffer.asUint8List()); |
| 127 | } |
| 128 | buf.writeSlice(data); |
| 129 | } |
| 130 | |
| 131 | bool _isFinalized(int i) { |
| 132 | if (getInputFinalScriptsig(i) != null) return true; |
| 133 | try { |
| 134 | getInputFinalScriptwitness(i); |
| 135 | return true; |
| 136 | } catch (_) { |
| 137 | return false; |
| 138 | } |
| 139 | } |
| 140 | } |