Merge pull request #569 from LedgerHQ/client-fixes
What changed, and why it matters
This commit fixes how Ledger's Bitcoin client libraries convert older PSBTv0 transaction data into the newer PSBTv2 format that Ledger hardware wallets expect. The changes correct several field-handling bugs: the transaction locktime is now written to the right place, missing input sequence numbers are treated as the default final value, and the Rust client now forwards unknown PSBTv2 fields (such as required locktimes) instead of dropping them. It also rejects malformed PSBTs where v0 and v2 fields contradict each other, rather than producing an invalid PSBT that the device would refuse. These are client-side robustness fixes; they do not appear to be a security patch for the device firmware itself.
Users of the Ledger Bitcoin client libraries (Python, JS, Rust) should upgrade to the versions released with this commit (Python 0.4.2, Rust 0.7.0) to obtain correct PSBTv2 handling and avoid transaction signing failures or malformed PSBTs. Developers integrating these libraries should review PSBTv2 field expectations in BIP-370 and ensure they do not rely on the previous incorrect conversion behavior.
Security signals we found
PSBTv0-to-v2 conversion bug fixes
Missing input sequence defaulted to 0xffffffff (final sequence)
Locktime assigned to transaction object instead of PSBT object
Unknown/proprietary PSBTv2 fields now forwarded in Rust client
Malformed hybrid v0/v2 PSBTs rejected to avoid duplicate/conflicting keys
Version bumps in Python (0.4.1→0.4.2) and Rust (0.6.2→0.7.0) clients
Evidence from the diff
The patch updates the Python, JavaScript, and Rust Bitcoin client libraries. Key changes in bitcoin_client/ledger_bitcoin/psbt.py: cache_unsigned_tx_pieces now checks self.version == 0 before rebuilding v2 fields; get_unsigned_tx assigns locktime to tx.nLockTime instead of self.nLockTime; and missing input sequences default to 0xffffffff. convert_to_v0 now builds the unsigned transaction before stripping v2 fields. In bitcoin_client_rs/src/psbt.rs, the old hand-rolled v2 pair generation is replaced by a parser that splits the serialized PSBTv0 into maps, removes PSBT_GLOBAL_UNSIGNED_TX, injects synthesized v2 global/input/output fields, and preserves unknown/proprietary fields. It returns an error if a synthesized v2 key already exists in the source PSBT, preventing duplicate/conflicting keys. Tests are added/updated to cover these behaviors, including a JS test comment noting that a zero required-height-locktime must be omitted per BIP-370.
Changed components
bitcoin_client/ledger_bitcoin/psbt.pybitcoin_client_js/src/__tests__/appClient.test.tsbitcoin_client_js/src/__tests__/psbtv2.test.tsbitcoin_client_rs/src/async_client.rsbitcoin_client_rs/src/client.rsbitcoin_client_rs/src/psbt.rsbitcoin_client_rs/src/protocol.rsInspect captured patch +368 / −859
### bitcoin_client/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Dates are in `dd-mm-yyyy` format.
+## [0.4.2] - 18-09-2026
+
+### Fixed
+
+- Improve support of PsbtV2 fields and V0-to-V2 conversion.
+
## [0.4.1] - 16-04-2026
### Added
### bitcoin_client/ledger_bitcoin/__init__.py
@@ -7,7 +7,7 @@
from .wallet import AddressType, WalletPolicy, MultisigWallet, WalletType
-__version__ = '0.4.1'
+__version__ = '0.4.2'
__all__ = [
"Client",
### bitcoin_client/ledger_bitcoin/psbt.py
@@ -1051,7 +1051,7 @@ def cache_unsigned_tx_pieces(self) -> None:
"""
# To make things easier, we split up the global transaction
# and use the PSBTv2 fields for PSBTv0
- if self.tx is not None:
+ if self.version == 0:
self.setup_from_tx(self.tx)
def setup_from_tx(self, tx: CTransaction):
@@ -1121,14 +1121,15 @@ def get_unsigned_tx(self) -> CTransaction:
tx = CTransaction()
tx.nVersion = self.tx_version
- self.nLockTime = self.compute_lock_time()
+ tx.nLockTime = self.compute_lock_time()
for psbt_in in self.inputs:
assert psbt_in.prev_txid is not None
assert psbt_in.prev_out is not None
- assert psbt_in.sequence is not None
- txin = CTxIn(COutPoint(uint256_from_str(psbt_in.prev_txid), psbt_in.prev_out), b"", psbt_in.sequence)
+ # If omitted, the sequence number is assumed to be the final sequence number
+ sequence = psbt_in.sequence if psbt_in.sequence is not None else 0xffffffff
+ txin = CTxIn(COutPoint(uint256_from_str(psbt_in.prev_txid), psbt_in.prev_out), b"", sequence)
tx.vin.append(txin)
for psbt_out in self.outputs:
@@ -1163,14 +1164,17 @@ def convert_to_v0(self) -> None:
Sets this PSBT to version 0
"""
+ # Build the unsigned transaction before stripping the v2 fields
+ tx = self.get_unsigned_tx()
+
if self.version == 2:
# strip PSBT version 2 fields
self.tx_version = None
self.fallback_locktime = None
self.tx_modifiable = None
self._convert_version(0)
- self.tx = self.get_unsigned_tx()
+ self.tx = tx
self.explicit_version = False
### bitcoin_client_js/src/__tests__/appClient.test.ts
@@ -413,9 +413,12 @@ describe("test AppClient", () => {
});
it("can sign a psbt", async () => {
- // psbt from test_sign_psbt_singlesig_wpkh_2to2 in the main test suite, converted to PSBTv2
+ // psbt from test_sign_psbt_singlesig_wpkh_2to2 in the main test suite, converted to PSBTv2.
+ // The conversion must not write a PSBT_IN_REQUIRED_HEIGHT_LOCKTIME of 0 for a transaction
+ // whose nLockTime is 0: BIP-370 requires that field to be in [1, 499999999], and the app
+ // rejects the PSBT. "No required lock time" is expressed by omitting the field, as here.
const psbtBuf = Buffer.from(
- "cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wESBAAAAAAAAQBxAQAAAAEORx706Sway1HvyGYPjT9pk26pybK/9y/5vIHFHvz0ZAEAAAAAAAAAAAJgrgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsrwYyAAAAAAAWABTcKG4M0ua9N86+nsNJ+18IkFZy/AAAAAABAR9grgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsIgYCcbW3ea2HCDhYd5e89vDHrsWr52pwnXJPSNLibPh08KAY9azC/VQAAIABAACAAAAAgAEAAAAAAAAAAQ4gr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBDwQAAAAAARAE/f///wESBAAAAAAAIgICKexHcnEx7SWIogxG7amrt9qm9J/VC6/nC5xappYcTswY9azC/VQAAIABAACAAAAAgAEAAAAKAAAAAQMIqDoGAAAAAAABBBYAFOs4+puBKPgfJule2wxf+uqDaQ/kAAEDCOCTBAAAAAAAAQQiACA/qWbJ3c3C/ZbkpeG8dlufr2zos+tPEQSq1r33cyTlvgA=",
+ "cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wABAHEBAAAAAQ5HHvTpLBrLUe/IZg+NP2mTbqnJsr/3L/m8gcUe/PRkAQAAAAAAAAAAAmCuCgAAAAAAFgAUNcbg3W08hLFrqIXcpzrIY9C1k+yvBjIAAAAAABYAFNwobgzS5r03zr6ew0n7XwiQVnL8AAAAAAEBH2CuCgAAAAAAFgAUNcbg3W08hLFrqIXcpzrIY9C1k+wiBgJxtbd5rYcIOFh3l7z28MeuxavnanCdck9I0uJs+HTwoBj1rML9VAAAgAEAAIAAAACAAQAAAAAAAAABDiCvv64GWQ90H/GvWbasRhEmM2pMSoLbVT32/vq3N6wz8wEPBAAAAAABEAT9////ACICAinsR3JxMe0liKIMRu2pq7fapvSf1Quv5wucWqaWHE7MGPWswv1UAACAAQAAgAAAAIABAAAACgAAAAEDCKg6BgAAAAAAAQQWABTrOPqbgSj4HybpXtsMX/rqg2kP5AABAwjgkwQAAAAAAAEEIgAgP6lmyd3Nwv2W5KXhvHZbn69s6LPrTxEEqta993Mk5b4A",
"base64"
);
### bitcoin_client_js/src/__tests__/psbtv2.test.ts
@@ -3,7 +3,7 @@ import { PsbtV2 } from "../lib/psbtv2";
describe("PsbtV2", () => {
it("deserializes a psbtV2 and reserializes it unchanged", async () => {
const psbtBuf = Buffer.from(
- "cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wESBAAAAAAAAQBxAQAAAAEORx706Sway1HvyGYPjT9pk26pybK/9y/5vIHFHvz0ZAEAAAAAAAAAAAJgrgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsrwYyAAAAAAAWABTcKG4M0ua9N86+nsNJ+18IkFZy/AAAAAABAR9grgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsIgYCcbW3ea2HCDhYd5e89vDHrsWr52pwnXJPSNLibPh08KAY9azC/VQAAIABAACAAAAAgAEAAAAAAAAAAQ4gr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBDwQAAAAAARAE/f///wESBAAAAAAAIgICKexHcnEx7SWIogxG7amrt9qm9J/VC6/nC5xappYcTswY9azC/VQAAIABAACAAAAAgAEAAAAKAAAAAQMIqDoGAAAAAAABBBYAFOs4+puBKPgfJule2wxf+uqDaQ/kAAEDCOCTBAAAAAAAAQQiACA/qWbJ3c3C/ZbkpeG8dlufr2zos+tPEQSq1r33cyTlvgA=",
+ "cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wABAHEBAAAAAQ5HHvTpLBrLUe/IZg+NP2mTbqnJsr/3L/m8gcUe/PRkAQAAAAAAAAAAAmCuCgAAAAAAFgAUNcbg3W08hLFrqIXcpzrIY9C1k+yvBjIAAAAAABYAFNwobgzS5r03zr6ew0n7XwiQVnL8AAAAAAEBH2CuCgAAAAAAFgAUNcbg3W08hLFrqIXcpzrIY9C1k+wiBgJxtbd5rYcIOFh3l7z28MeuxavnanCdck9I0uJs+HTwoBj1rML9VAAAgAEAAIAAAACAAQAAAAAAAAABDiCvv64GWQ90H/GvWbasRhEmM2pMSoLbVT32/vq3N6wz8wEPBAAAAAABEAT9////ACICAinsR3JxMe0liKIMRu2pq7fapvSf1Quv5wucWqaWHE7MGPWswv1UAACAAQAAgAAAAIABAAAACgAAAAEDCKg6BgAAAAAAAQQWABTrOPqbgSj4HybpXtsMX/rqg2kP5AABAwjgkwQAAAAAAAEEIgAgP6lmyd3Nwv2W5KXhvHZbn69s6LPrTxEEqta993Mk5b4A",
"base64"
);
### bitcoin_client_rs/Cargo.lock
@@ -363,7 +363,7 @@ dependencies = [
[[package]]
name = "ledger_bitcoin_client"
-version = "0.6.2"
+version = "0.7.0"
dependencies = [
"async-trait",
"base64",
### bitcoin_client_rs/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ledger_bitcoin_client"
-version = "0.6.2"
+version = "0.7.0"
authors = ["Ledger"]
edition = "2018"
description = "Ledger Bitcoin application client"
### bitcoin_client_rs/src/async_client.rs
@@ -281,42 +281,22 @@ impl<T: Transport> BitcoinClient<T> {
// necessary for version 1 of the protocol (introduced in version 2.1.0)
intpr.add_known_preimage(wallet.descriptor_template.as_bytes().to_vec());
- let global_map: Vec<(Vec<u8>, Vec<u8>)> = get_v2_global_pairs(psbt)
- .into_iter()
- .map(deserialize_pair)
- .collect();
- intpr.add_known_mapping(&global_map);
- let global_mapping_commitment = get_merkleized_map_commitment(&global_map);
-
- let mut input_commitments: Vec<Vec<u8>> = Vec::with_capacity(psbt.inputs.len());
- for (index, input) in psbt.inputs.iter().enumerate() {
- let txin = psbt
- .unsigned_tx
- .input
- .get(index)
- .ok_or(BitcoinClientError::InvalidPsbt)?;
- let input_map: Vec<(Vec<u8>, Vec<u8>)> = get_v2_input_pairs(input, txin)
- .into_iter()
- .map(deserialize_pair)
- .collect();
- intpr.add_known_mapping(&input_map);
- input_commitments.push(get_merkleized_map_commitment(&input_map));
+ let maps = get_v2_maps(psbt).map_err(|_| BitcoinClientError::InvalidPsbt)?;
+
+ intpr.add_known_mapping(&maps.global);
+ let global_mapping_commitment = get_merkleized_map_commitment(&maps.global);
+
+ let mut input_commitments: Vec<Vec<u8>> = Vec::with_capacity(maps.inputs.len());
+ for input_map in &maps.inputs {
+ intpr.add_known_mapping(input_map);
+ input_commitments.push(get_merkleized_map_commitment(input_map));
}
let input_commitments_root = intpr.add_known_list(&input_commitments);
- let mut output_commitments: Vec<Vec<u8>> = Vec::with_capacity(psbt.outputs.len());
- for (index, output) in psbt.outputs.iter().enumerate() {
- let txout = psbt
- .unsigned_tx
- .output
- .get(index)
- .ok_or(BitcoinClientError::InvalidPsbt)?;
- let output_map: Vec<(Vec<u8>, Vec<u8>)> = get_v2_output_pairs(output, txout)
- .into_iter()
- .map(deserialize_pair)
- .collect();
- intpr.add_known_mapping(&output_map);
- output_commitments.push(get_merkleized_map_commitment(&output_map));
+ let mut output_commitments: Vec<Vec<u8>> = Vec::with_capacity(maps.outputs.len());
+ for output_map in &maps.outputs {
+ intpr.add_known_mapping(output_map);
+ output_commitments.push(get_merkleized_map_commitment(output_map));
}
let output_commitments_root = intpr.add_known_list(&output_commitments);
### bitcoin_client_rs/src/client.rs
@@ -262,42 +262,22 @@ impl<T: Transport> BitcoinClient<T> {
// necessary for version 1 of the protocol (introduced in version 2.1.0)
intpr.add_known_preimage(wallet.descriptor_template.as_bytes().to_vec());
- let global_map: Vec<(Vec<u8>, Vec<u8>)> = get_v2_global_pairs(psbt)
- .into_iter()
- .map(deserialize_pair)
- .collect();
- intpr.add_known_mapping(&global_map);
- let global_mapping_commitment = get_merkleized_map_commitment(&global_map);
-
- let mut input_commitments: Vec<Vec<u8>> = Vec::with_capacity(psbt.inputs.len());
- for (index, input) in psbt.inputs.iter().enumerate() {
- let txin = psbt
- .unsigned_tx
- .input
- .get(index)
- .ok_or(BitcoinClientError::InvalidPsbt)?;
- let input_map: Vec<(Vec<u8>, Vec<u8>)> = get_v2_input_pairs(input, txin)
- .into_iter()
- .map(deserialize_pair)
- .collect();
- intpr.add_known_mapping(&input_map);
- input_commitments.push(get_merkleized_map_commitment(&input_map));
+ let maps = get_v2_maps(psbt).map_err(|_| BitcoinClientError::InvalidPsbt)?;
+
+ intpr.add_known_mapping(&maps.global);
+ let global_mapping_commitment = get_merkleized_map_commitment(&maps.global);
+
+ let mut input_commitments: Vec<Vec<u8>> = Vec::with_capacity(maps.inputs.len());
+ for input_map in &maps.inputs {
+ intpr.add_known_mapping(input_map);
+ input_commitments.push(get_merkleized_map_commitment(input_map));
}
let input_commitments_root = intpr.add_known_list(&input_commitments);
- let mut output_commitments: Vec<Vec<u8>> = Vec::with_capacity(psbt.outputs.len());
- for (index, output) in psbt.outputs.iter().enumerate() {
- let txout = psbt
- .unsigned_tx
- .output
- .get(index)
- .ok_or(BitcoinClientError::InvalidPsbt)?;
- let output_map: Vec<(Vec<u8>, Vec<u8>)> = get_v2_output_pairs(output, txout)
- .into_iter()
- .map(deserialize_pair)
- .collect();
- intpr.add_known_mapping(&output_map);
- output_commitments.push(get_merkleized_map_commitment(&output_map));
+ let mut output_commitments: Vec<Vec<u8>> = Vec::with_capacity(maps.outputs.len());
+ for output_map in &maps.outputs {
+ intpr.add_known_mapping(output_map);
+ output_commitments.push(get_merkleized_map_commitment(output_map));
}
let output_commitments_root = intpr.add_known_list(&output_commitments);
### bitcoin_client_rs/src/protocol.rs
@@ -79,18 +79,6 @@ impl Decodable for UncheckedVarInt {
}
}
-impl UncheckedVarInt {
- /// Returns the number of bytes this varint occupies when serialized.
- pub fn size(&self) -> usize {
- match self.0 {
- 0..=0xFC => 1,
- 0xFD..=0xFFFF => 3,
- 0x10000..=0xFFFFFFFF => 5,
- _ => 9,
- }
- }
-}
-
/// Tag yielded by the device to introduce a MuSig2 pubnonce payload.
pub const CCMD_YIELD_MUSIG_PUBNONCE_TAG: u64 = 0xFFFFFFFF;
/// Tag yielded by the device to introduce a MuSig2 partial-signature payload.
### bitcoin_client_rs/src/psbt.rs
@@ -1,394 +1,194 @@
-/// code is from github.com/rust-bitcoin/rust-bitcoin
-/// SPDX-License-Identifier: CC0-1.0
-///
-/// Note: Only psbt V2 is supported by the ledger bitcoin app.
-/// rust-bitcoin currently support V0.
+//! Builds the PSBTv2 key-value maps that the Ledger bitcoin app expects.
+//!
+//! Note: only PSBTv2 is supported by the Ledger bitcoin app, while rust-bitcoin's `Psbt` is a
+//! PSBTv0. This module implements the translation from rust-bitcoin's PSBTv0 to the PSBTv2
+//! key-value maps expected by the Ledger bitcoin app, while leaving the
+//! serialization/deserialization logic to rust-psbt (for known fields), and propagating unknown
+//! fields unchanged.
+
use bitcoin::{
- blockdata::transaction::{TxIn, TxOut},
- consensus::encode::{deserialize, serialize},
+ consensus::encode::{serialize, Decodable},
ecdsa,
hashes::Hash,
key::FromSliceError as KeyError,
- psbt::{raw, Input, Output, Psbt},
+ psbt::Psbt,
secp256k1::{self, XOnlyPublicKey},
taproot,
taproot::TapLeafHash,
PublicKey,
};
use crate::protocol::UncheckedVarInt;
-use serialize::Serialize;
-
-#[rustfmt::skip]
-macro_rules! impl_psbt_get_pair {
- ($rv:ident.push($slf:ident.$unkeyed_name:ident, $unkeyed_typeval:ident)) => {
- if let Some(ref $unkeyed_name) = $slf.$unkeyed_name {
- $rv.push(bitcoin::psbt::raw::Pair {
- key: bitcoin::psbt::raw::Key {
- type_value: $unkeyed_typeval,
- key: vec![],
- },
- value: Serialize::serialize($unkeyed_name),
- });
- }
- };
- ($rv:ident.push_map($slf:ident.$keyed_name:ident, $keyed_typeval:ident)) => {
- for (key, val) in &$slf.$keyed_name {
- $rv.push(bitcoin::psbt::raw::Pair {
- key: bitcoin::psbt::raw::Key {
- type_value: $keyed_typeval,
- key: Serialize::serialize(key),
- },
- value: Serialize::serialize(val),
- });
- }
- };
-}
-/// V0, Type: Unsigned Transaction PSBT_GLOBAL_UNSIGNED_TX = 0x00
-/// const PSBT_GLOBAL_UNSIGNED_TX: u8 = 0x00;
-/// Type: Extended Public Key PSBT_GLOBAL_XPUB = 0x01
-const PSBT_GLOBAL_XPUB: u8 = 0x01;
-/// V2 field
+/// V0 only: the unsigned transaction. Its contents are re-expressed by the global v2 fields
+/// below and by each input's and output's own keys, so it is dropped.
+const PSBT_GLOBAL_UNSIGNED_TX: u8 = 0x00;
const PSBT_GLOBAL_TX_VERSION: u8 = 0x02;
-/// V2 field
const PSBT_GLOBAL_FALLBACK_LOCKTIME: u8 = 0x03;
-/// V2 field
const PSBT_GLOBAL_INPUT_COUNT: u8 = 0x04;
-/// V2 field
const PSBT_GLOBAL_OUTPUT_COUNT: u8 = 0x05;
-/// V2 field
-/// const PSBT_GLOBAL_TX_MODIFIABLE: u8 = 0x06;
-/// Type: Version Number PSBT_GLOBAL_VERSION = 0xFB
const PSBT_GLOBAL_VERSION: u8 = 0xFB;
-pub fn get_v2_global_pairs(psbt: &Psbt) -> Vec<raw::Pair> {
- let mut rv: Vec<raw::Pair> = Default::default();
-
- for (xpub, (fingerprint, derivation)) in &psbt.xpub {
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_GLOBAL_XPUB,
- key: xpub.encode().to_vec(),
- },
- value: {
- let mut ret = Vec::with_capacity(4 + derivation.len() * 4);
- ret.extend(fingerprint.as_bytes());
- derivation
- .into_iter()
- .for_each(|n| ret.extend(u32::from(*n).to_le_bytes()));
- ret
- },
- });
- }
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_GLOBAL_FALLBACK_LOCKTIME,
- key: vec![],
- },
- value: serialize(&psbt.unsigned_tx.lock_time),
- });
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_GLOBAL_INPUT_COUNT,
- key: vec![],
- },
- value: serialize(&UncheckedVarInt(psbt.inputs.len() as u64)),
- });
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_GLOBAL_OUTPUT_COUNT,
- key: vec![],
- },
- value: serialize(&UncheckedVarInt(psbt.outputs.len() as u64)),
- });
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_GLOBAL_TX_VERSION,
- key: vec![],
- },
- value: psbt.unsigned_tx.version.0.to_le_bytes().to_vec(),
- });
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_GLOBAL_VERSION,
- key: vec![],
- },
- value: 2_u32.to_le_bytes().to_vec(),
- });
-
- for (key, value) in psbt.proprietary.iter() {
- rv.push(raw::Pair {
- key: key.to_key(),
- value: value.clone(),
- });
- }
-
- for (key, value) in psbt.unknown.iter() {
- rv.push(raw::Pair {
- key: key.clone(),
- value: value.clone(),
- });
- }
-
- rv
-}
-
-/// Type: Non-Witness UTXO PSBT_IN_NON_WITNESS_UTXO = 0x00
-const PSBT_IN_NON_WITNESS_UTXO: u8 = 0x00;
-/// Type: Witness UTXO PSBT_IN_WITNESS_UTXO = 0x01
-const PSBT_IN_WITNESS_UTXO: u8 = 0x01;
-/// Type: Partial Signature PSBT_IN_PARTIAL_SIG = 0x02
-const PSBT_IN_PARTIAL_SIG: u8 = 0x02;
-/// Type: Sighash Type PSBT_IN_SIGHASH_TYPE = 0x03
-const PSBT_IN_SIGHASH_TYPE: u8 = 0x03;
-/// Type: Redeem Script PSBT_IN_REDEEM_SCRIPT = 0x04
-const PSBT_IN_REDEEM_SCRIPT: u8 = 0x04;
-/// Type: Witness Script PSBT_IN_WITNESS_SCRIPT = 0x05
-const PSBT_IN_WITNESS_SCRIPT: u8 = 0x05;
-/// Type: BIP 32 Derivation Path PSBT_IN_BIP32_DERIVATION = 0x06
-const PSBT_IN_BIP32_DERIVATION: u8 = 0x06;
-/// Type: Finalized scriptSig PSBT_IN_FINAL_SCRIPTSIG = 0x07
-const PSBT_IN_FINAL_SCRIPTSIG: u8 = 0x07;
-/// Type: Finalized scriptWitness PSBT_IN_FINAL_SCRIPTWITNESS = 0x08
-const PSBT_IN_FINAL_SCRIPTWITNESS: u8 = 0x08;
-/// V2
-const PSBT_IN_PREVIOUS_TXID: u8 = 0x0e;
-/// V2
+const PSBT_IN_PREVIOUS_TXID: u8 = 0x0E;
+const PSBT_IN_OUTPUT_INDEX: u8 = 0x0F;
const PSBT_IN_SEQUENCE: u8 = 0x10;
-/// V2
-/// const PSBT_IN_REQUIRED_TIME_LOCKTIME: u8 = 0x11;
-/// V2
-///const PSBT_IN_REQUIRED_HEIGHT_LOCKTIME: u8 = 0x12;
-const PSBT_IN_OUTPUT_INDEX: u8 = 0x0f;
-/// Type: RIPEMD160 preimage PSBT_IN_RIPEMD160 = 0x0a
-const PSBT_IN_RIPEMD160: u8 = 0x0a;
-/// Type: SHA256 preimage PSBT_IN_SHA256 = 0x0b
-const PSBT_IN_SHA256: u8 = 0x0b;
-/// Type: HASH160 preimage PSBT_IN_HASH160 = 0x0c
-const PSBT_IN_HASH160: u8 = 0x0c;
-/// Type: HASH256 preimage PSBT_IN_HASH256 = 0x0d
-const PSBT_IN_HASH256: u8 = 0x0d;
-/// Type: Schnorr Signature in Key Spend PSBT_IN_TAP_KEY_SIG = 0x13
-const PSBT_IN_TAP_KEY_SIG: u8 = 0x13;
-/// Type: Schnorr Signature in Script Spend PSBT_IN_TAP_SCRIPT_SIG = 0x14
-const PSBT_IN_TAP_SCRIPT_SIG: u8 = 0x14;
-/// Type: Taproot Leaf Script PSBT_IN_TAP_LEAF_SCRIPT = 0x14
-const PSBT_IN_TAP_LEAF_SCRIPT: u8 = 0x15;
-/// Type: Taproot Key BIP 32 Derivation Path PSBT_IN_TAP_BIP32_DERIVATION = 0x16
-const PSBT_IN_TAP_BIP32_DERIVATION: u8 = 0x16;
-/// Type: Taproot Internal Key PSBT_IN_TAP_INTERNAL_KEY = 0x17
-const PSBT_IN_TAP_INTERNAL_KEY: u8 = 0x17;
-/// Type: Taproot Merkle Root PSBT_IN_TAP_MERKLE_ROOT = 0x18
-const PSBT_IN_TAP_MERKLE_ROOT: u8 = 0x18;
-
-pub fn get_v2_input_pairs(input: &Input, txin: &TxIn) -> Vec<raw::Pair> {
- let mut rv: Vec<raw::Pair> = Default::default();
-
- impl_psbt_get_pair! {
- rv.push(input.non_witness_utxo, PSBT_IN_NON_WITNESS_UTXO)
- }
-
- impl_psbt_get_pair! {
- rv.push(input.witness_utxo, PSBT_IN_WITNESS_UTXO)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.partial_sigs, PSBT_IN_PARTIAL_SIG)
- }
-
- impl_psbt_get_pair! {
- rv.push(input.sighash_type, PSBT_IN_SIGHASH_TYPE)
- }
-
- impl_psbt_get_pair! {
- rv.push(input.redeem_script, PSBT_IN_REDEEM_SCRIPT)
- }
-
- impl_psbt_get_pair! {
- rv.push(input.witness_script, PSBT_IN_WITNESS_SCRIPT)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.bip32_derivation, PSBT_IN_BIP32_DERIVATION)
- }
-
- impl_psbt_get_pair! {
- rv.push(input.final_script_sig, PSBT_IN_FINAL_SCRIPTSIG)
- }
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_IN_PREVIOUS_TXID,
- key: vec![],
- },
- value: serialize(&txin.previous_output.txid),
- });
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_IN_OUTPUT_INDEX,
- key: vec![],
- },
- value: serialize(&txin.previous_output.vout),
- });
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_IN_SEQUENCE,
- key: vec![],
- },
- value: serialize(&txin.sequence),
- });
-
- impl_psbt_get_pair! {
- rv.push(input.final_script_witness, PSBT_IN_FINAL_SCRIPTWITNESS)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.ripemd160_preimages, PSBT_IN_RIPEMD160)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.sha256_preimages, PSBT_IN_SHA256)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.hash160_preimages, PSBT_IN_HASH160)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.hash256_preimages, PSBT_IN_HASH256)
- }
- impl_psbt_get_pair! {
- rv.push(input.tap_key_sig, PSBT_IN_TAP_KEY_SIG)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.tap_script_sigs, PSBT_IN_TAP_SCRIPT_SIG)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.tap_scripts, PSBT_IN_TAP_LEAF_SCRIPT)
- }
-
- impl_psbt_get_pair! {
- rv.push_map(input.tap_key_origins, PSBT_IN_TAP_BIP32_DERIVATION)
- }
-
- impl_psbt_get_pair! {
- rv.push(input.tap_internal_key, PSBT_IN_TAP_INTERNAL_KEY)
- }
-
- impl_psbt_get_pair! {
- rv.push(input.tap_merkle_root, PSBT_IN_TAP_MERKLE_ROOT)
- }
-
- for (key, value) in input.proprietary.iter() {
- rv.push(raw::Pair {
- key: key.to_key(),
- value: value.clone(),
- });
- }
-
- for (key, value) in input.unknown.iter() {
- rv.push(raw::Pair {
- key: key.clone(),
- value: value.clone(),
- });
- }
-
- rv
-}
-
-/// Type: Redeem Script PSBT_OUT_REDEEM_SCRIPT = 0x00
-const PSBT_OUT_REDEEM_SCRIPT: u8 = 0x00;
-/// Type: Witness Script PSBT_OUT_WITNESS_SCRIPT = 0x01
-const PSBT_OUT_WITNESS_SCRIPT: u8 = 0x01;
-/// Type: BIP 32 Derivation Path PSBT_OUT_BIP32_DERIVATION = 0x02
-const PSBT_OUT_BIP32_DERIVATION: u8 = 0x02;
-/// V2
const PSBT_OUT_AMOUNT: u8 = 0x03;
-/// V2
const PSBT_OUT_SCRIPT: u8 = 0x04;
-/// Type: Taproot Internal Key PSBT_OUT_TAP_INTERNAL_KEY = 0x05
-const PSBT_OUT_TAP_INTERNAL_KEY: u8 = 0x05;
-/// Type: Taproot Tree PSBT_OUT_TAP_TREE = 0x06
-const PSBT_OUT_TAP_TREE: u8 = 0x06;
-/// Type: Taproot Key BIP 32 Derivation Path PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
-const PSBT_OUT_TAP_BIP32_DERIVATION: u8 = 0x07;
-
-pub fn get_v2_output_pairs(output: &Output, txout: &TxOut) -> Vec<raw::Pair> {
- let mut rv: Vec<raw::Pair> = Default::default();
-
- impl_psbt_get_pair! {
- rv.push(output.redeem_script, PSBT_OUT_REDEEM_SCRIPT)
- }
- impl_psbt_get_pair! {
- rv.push(output.witness_script, PSBT_OUT_WITNESS_SCRIPT)
- }
+/// The `psbt` magic and its `0xff` separator, which precede the global map.
+const PSBT_MAGIC_LEN: usize = 5;
- impl_psbt_get_pair! {
- rv.push_map(output.bip32_derivation, PSBT_OUT_BIP32_DERIVATION)
- }
+/// One PSBT key-value map, in the form the app's merkleized maps take: each key is
+/// `<keytype> || <keydata>` and each value is the raw value bytes, with no length prefixes.
+pub type PsbtMap = Vec<(Vec<u8>, Vec<u8>)>;
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_OUT_AMOUNT,
- key: vec![],
- },
- value: txout.value.to_sat().to_le_bytes().to_vec(),
- });
-
- rv.push(raw::Pair {
- key: raw::Key {
- type_value: PSBT_OUT_SCRIPT,
- key: vec![],
- },
- value: txout.script_pubkey.as_bytes().to_vec(),
- });
-
- impl_psbt_get_pair! {
- rv.push(output.tap_internal_key, PSBT_OUT_TAP_INTERNAL_KEY)
- }
+/// The key-value maps of a PSBT, translated to PSBTv2.
+pub struct PsbtV2Maps {
+ pub global: PsbtMap,
+ /// One map per PSBT input, in order.
+ pub inputs: Vec<PsbtMap>,
+ /// One map per PSBT output, in order.
+ pub outputs: Vec<PsbtMap>,
+}
- impl_psbt_get_pair! {
- rv.push(output.tap_tree, PSBT_OUT_TAP_TREE)
- }
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PsbtV2Error {
+ /// The number of inputs or outputs of the unsigned transaction differs from the number of
+ /// input or output maps, which BIP-174 requires to be equal.
+ TxMapCountMismatch,
+ /// The PSBT's own serialization could not be split into key-value maps. Unreachable for a
+ /// `Psbt` that rust-bitcoin built.
+ MalformedSerialization,
+ /// An unexpected PSBTv2 field was encountered in a Psbtv0.
+ UnexpectedV2Field(u8),
+}
- impl_psbt_get_pair! {
- rv.push_map(output.tap_key_origins, PSBT_OUT_TAP_BIP32_DERIVATION)
- }
+/// Returns the PSBTv2 maps for `psbt`: the global map, then one map per input, then one per
+/// output.
+pub fn get_v2_maps(psbt: &Psbt) -> Result<PsbtV2Maps, PsbtV2Error> {
+ let n_inputs = psbt.inputs.len();
+ let n_outputs = psbt.outputs.len();
+
+ if psbt.unsigned_tx.input.len() != n_inputs || psbt.unsigned_tx.output.len() != n_outputs {
+ return Err(PsbtV2Error::TxMapCountMismatch);
+ }
+
+ let mut maps = split_maps(&psbt.serialize(), 1 + n_inputs + n_outputs)
+ .ok_or(PsbtV2Error::MalformedSerialization)?;
+
+ let mut outputs = maps.split_off(1 + n_inputs);
+ let mut inputs = maps.split_off(1);
+ let mut global = maps.pop().ok_or(PsbtV2Error::MalformedSerialization)?;
+
+ global.retain(|(key, _)| key.as_slice() != [PSBT_GLOBAL_UNSIGNED_TX]);
+
+ push_v2_field(
+ &mut global,
+ PSBT_GLOBAL_TX_VERSION,
+ psbt.unsigned_tx.version.0.to_le_bytes().to_vec(),
+ )?;
+ push_v2_field(
+ &mut global,
+ PSBT_GLOBAL_FALLBACK_LOCKTIME,
+ serialize(&psbt.unsigned_tx.lock_time),
+ )?;
+ push_v2_field(
+ &mut global,
+ PSBT_GLOBAL_INPUT_COUNT,
+ serialize(&UncheckedVarInt(n_inputs as u64)),
+ )?;
+ push_v2_field(
+ &mut global,
+ PSBT_GLOBAL_OUTPUT_COUNT,
+ serialize(&UncheckedVarInt(n_outputs as u64)),
+ )?;
+ push_v2_field(
+ &mut global,
+ PSBT_GLOBAL_VERSION,
+ 2_u32.to_le_bytes().to_vec(),
+ )?;
+
+ for (map, txin) in inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) {
+ push_v2_field(
+ map,
+ PSBT_IN_PREVIOUS_TXID,
+ serialize(&txin.previous_output.txid),
+ )?;
+ push_v2_field(
+ map,
+ PSBT_IN_OUTPUT_INDEX,
+ serialize(&txin.previous_output.vout),
+ )?;
+ push_v2_field(map, PSBT_IN_SEQUENCE, serialize(&txin.sequence))?;
+ }
+
+ for (map, txout) in outputs.iter_mut().zip(psbt.unsigned_tx.output.iter()) {
+ push_v2_field(
+ map,
+ PSBT_OUT_AMOUNT,
+ txout.value.to_sat().to_le_bytes().to_vec(),
+ )?;
+ push_v2_field(
+ map,
+ PSBT_OUT_SCRIPT,
+ txout.script_pubkey.as_bytes().to_vec(),
+ )?;
+ }
+
+ Ok(PsbtV2Maps {
+ global,
+ inputs,
+ outputs,
+ })
+}
- for (key, value) in output.proprietary.iter() {
- rv.push(raw::Pair {
- key: key.to_key(),
- value: value.clone(),
- });
+/// Adds a keyless PSBTv2 field synthesized from the v0 unsigned transaction, erroring if the
+/// PSBT already carries that key.
+///
+/// Only fields with no keydata go through here, so no keydata argument is present.
+fn push_v2_field(map: &mut PsbtMap, key_type: u8, value: Vec<u8>) -> Result<(), PsbtV2Error> {
+ if map.iter().any(|(key, _)| key.as_slice() == [key_type]) {
+ return Err(PsbtV2Error::UnexpectedV2Field(key_type));
}
+ map.push((vec![key_type], value));
+ Ok(())
+}
- for (key, value) in output.unknown.iter() {
- rv.push(raw::Pair {
- key: key.clone(),
- value: value.clone(),
- });
+/// Splits a serialized PSBT into `n_maps` key-value maps, dropping the length prefixes.
+///
+/// Returns `None` if the bytes are not a well-formed PSBT holding exactly that many maps.
+fn split_maps(bytes: &[u8], n_maps: usize) -> Option<Vec<PsbtMap>> {
+ let mut d: &[u8] = bytes.get(PSBT_MAGIC_LEN..)?;
+
+ let mut maps = Vec::with_capacity(n_maps);
+ for _ in 0..n_maps {
+ let mut pairs: PsbtMap = Vec::new();
+ loop {
+ // <map> := <keypair>* 0x00, and a key is never empty, so a zero length is the
+ // separator rather than a pair.
+ if *d.first()? == 0x00 {
+ d = &d[1..];
+ break;
+ }
+ let key = read_prefixed(&mut d)?;
+ let value = read_prefixed(&mut d)?;
+ pairs.push((key, value));
+ }
+ maps.push(pairs);
}
-
- rv
+ Some(maps)
}
-pub fn deserialize_pair(pair: raw::Pair) -> (Vec<u8>, Vec<u8>) {
- (
- deserialize(&Serialize::serialize(&pair.key)).unwrap(),
- pair.value,
- )
+/// Reads one `<compact size length> <bytes>` field, advancing `d` past it.
+fn read_prefixed(d: &mut &[u8]) -> Option<Vec<u8>> {
+ let len = UncheckedVarInt::consensus_decode(d).ok()?.0;
+ if len > d.len() as u64 {
+ return None;
+ }
+ let (bytes, rest) = d.split_at(len as usize);
+ *d = rest;
+ Some(bytes.to_vec())
}
#[derive(Debug, Clone)]
@@ -442,426 +242,174 @@ pub enum PartialSignatureError {
TapLeaf(bitcoin::hashes::FromSliceError),
}
-mod serialize {
- use core::convert::{TryFrom, TryInto};
-
+#[cfg(test)]
+mod tests {
+ use super::*;
use bitcoin::{
- bip32::{ChildNumber, Fingerprint, KeySource},
- blockdata::{
- script::ScriptBuf,
- transaction::{Transaction, TxOut},
- witness::Witness,
- },
- consensus::encode::{self, deserialize_partial, serialize, Decodable, Encodable},
- ecdsa,
- hashes::{hash160, ripemd160, sha256, sha256d, Hash},
- key::PublicKey,
- psbt::{Error, PsbtSighashType},
- secp256k1::{self, XOnlyPublicKey},
- taproot,
- taproot::{ControlBlock, LeafVersion, TapLeafHash, TapNodeHash, TapTree, TaprootBuilder},
+ absolute::LockTime, psbt::raw, transaction::Version, Amount, OutPoint, ScriptBuf, Sequence,
+ Transaction, TxIn, TxOut, Witness,
};
- use crate::protocol::UncheckedVarInt;
-
- macro_rules! impl_psbt_de_serialize {
- ($thing:ty) => {
- impl_psbt_serialize!($thing);
- impl_psbt_deserialize!($thing);
- };
- }
-
- macro_rules! impl_psbt_deserialize {
- ($thing:ty) => {
- impl Deserialize for $thing {
- fn deserialize(bytes: &[u8]) -> Result<Self, bitcoin::psbt::Error> {
- bitcoin::consensus::deserialize(&bytes[..])
- .map_err(|e| bitcoin::psbt::Error::from(e))
- }
- }
- };
- }
-
- macro_rules! impl_psbt_serialize {
- ($thing:ty) => {
- impl Serialize for $thing {
- fn serialize(&self) -> Vec<u8> {
- bitcoin::consensus::serialize(self)
- }
- }
- };
- }
-
- // macros for serde of hashes
- macro_rules! impl_psbt_hash_de_serialize {
- ($hash_type:ty) => {
- impl_psbt_hash_serialize!($hash_type);
- impl_psbt_hash_deserialize!($hash_type);
- };
- }
-
- macro_rules! impl_psbt_hash_deserialize {
- ($hash_type:ty) => {
- impl $crate::psbt::serialize::Deserialize for $hash_type {
- fn deserialize(bytes: &[u8]) -> Result<Self, bitcoin::psbt::Error> {
- <$hash_type>::from_slice(&bytes[..]).map_err(|e| bitcoin::psbt::Error::from(e))
- }
- }
+ const LOCKTIME: u32 = 1_000;
+
+ fn unsigned_psbt() -> Psbt {
+ let tx = Transaction {
+ version: Version::TWO,
+ lock_time: LockTime::from_consensus(LOCKTIME),
+ input: vec![TxIn {
+ previous_output: OutPoint::null(),
+ script_sig: ScriptBuf::new(),
+ sequence: Sequence::MAX,
+ witness: Witness::new(),
+ }],
+ output: vec![TxOut {
+ value: Amount::from_sat(1_000),
+ script_pubkey: ScriptBuf::new(),
+ }],
};
+ Psbt::from_unsigned_tx(tx).expect("no input is signed")
}
- macro_rules! impl_psbt_hash_serialize {
- ($hash_type:ty) => {
- impl $crate::psbt::serialize::Serialize for $hash_type {
- fn serialize(&self) -> Vec<u8> {
- self.as_byte_array().to_vec()
- }
- }
- };
- }
-
- /// A trait for serializing a value as raw data for insertion into PSBT
- /// key-value maps.
- pub(crate) trait Serialize {
- /// Serialize a value as raw data.
- fn serialize(&self) -> Vec<u8>;
- }
-
- /// A trait for deserializing a value from raw data in PSBT key-value maps.
- pub(crate) trait Deserialize: Sized {
- /// Deserialize a value from raw data.
- fn deserialize(bytes: &[u8]) -> Result<Self, Error>;
- }
-
- impl_psbt_de_serialize!(Transaction);
- impl_psbt_de_serialize!(TxOut);
- impl_psbt_de_serialize!(Witness);
- impl_psbt_hash_de_serialize!(ripemd160::Hash);
- impl_psbt_hash_de_serialize!(sha256::Hash);
- impl_psbt_hash_de_serialize!(TapLeafHash);
- impl_psbt_hash_de_serialize!(TapNodeHash);
- impl_psbt_hash_de_serialize!(hash160::Hash);
- impl_psbt_hash_de_serialize!(sha256d::Hash);
-
- // taproot
- impl_psbt_de_serialize!(Vec<TapLeafHash>);
-
- impl Serialize for bitcoin::psbt::raw::Key {
- fn serialize(&self) -> Vec<u8> {
- let mut buf = Vec::new();
- UncheckedVarInt((self.key.len() + 1) as u64)
- .consensus_encode(&mut buf)
- .expect("in-memory writers don't error");
-
- self.type_value
- .consensus_encode(&mut buf)
- .expect("in-memory writers don't error");
-
- for key in &self.key {
- key.consensus_encode(&mut buf)
- .expect("in-memory writers don't error");
- }
-
- buf
- }
- }
-
- impl Serialize for ScriptBuf {
- fn serialize(&self) -> Vec<u8> {
- self.to_bytes()
- }
- }
-
- impl Deserialize for ScriptBuf {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- Ok(Self::from(bytes.to_vec()))
- }
- }
-
- impl Serialize for PublicKey {
- fn serialize(&self) -> Vec<u8> {
- let mut buf = Vec::new();
- self.write_into(&mut buf).expect("vecs don't error");
- buf
- }
- }
-
- impl Deserialize for PublicKey {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- PublicKey::from_slice(bytes).map_err(Error::InvalidPublicKey)
- }
- }
-
- impl Serialize for secp256k1::PublicKey {
- fn serialize(&self) -> Vec<u8> {
- self.serialize().to_vec()
- }
- }
-
- impl Deserialize for secp256k1::PublicKey {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- secp256k1::PublicKey::from_slice(bytes).map_err(Error::InvalidSecp256k1PublicKey)
- }
- }
-
- impl Serialize for ecdsa::Signature {
- fn serialize(&self) -> Vec<u8> {
- self.to_vec()
- }
- }
-
- impl Deserialize for ecdsa::Signature {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- // NB: Since BIP-174 says "the signature as would be pushed to the stack from
- // a scriptSig or witness" we should ideally use a consensus deserialization and do
- // not error on a non-standard values. However,
- //
- // 1) the current implementation of from_u32_consensus(`flag`) does not preserve
- // the sighash byte `flag` mapping all unknown values to EcdsaSighashType::All or
- // EcdsaSighashType::AllPlusAnyOneCanPay. Therefore, break the invariant
- // EcdsaSig::from_slice(&sl[..]).to_vec = sl.
- //
- // 2) This would cause to have invalid signatures because the sighash message
- // also has a field sighash_u32 (See BIP141). For example, when signing with non-standard
- // 0x05, the sighash message would have the last field as 0x05u32 while, the verification
- // would use check the signature assuming sighash_u32 as `0x01`.
- ecdsa::Signature::from_slice(bytes).map_err(|e| match e {
- ecdsa::Error::EmptySignature => Error::InvalidEcdsaSignature(e),
- ecdsa::Error::SighashType(flag) => Error::NonStandardSighashType(flag.0),
- ecdsa::Error::Secp256k1(..) => Error::InvalidEcdsaSignature(e),
- ecdsa::Error::Hex(..) => {
- unreachable!("Decoding from slice, not hex")
- }
- _ => Error::InvalidEcdsaSignature(e),
- })
- }
- }
-
- impl Serialize for KeySource {
- fn serialize(&self) -> Vec<u8> {
- let mut rv: Vec<u8> = Vec::with_capacity(key_source_len(self));
-
- rv.append(&mut self.0.to_bytes().to_vec());
-
- for cnum in self.1.into_iter() {
- rv.append(&mut serialize(&u32::from(*cnum)))
- }
-
- rv
- }
- }
-
- impl Deserialize for KeySource {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- if bytes.len() < 4 {
- return Err(Error::ConsensusEncoding(
- bitcoin::consensus::encode::Error::ParseFailed(
- "Not enough bytes for key source",
- ),
- ));
- }
-
- let fprint: Fingerprint = bytes[0..4].try_into().expect("4 is the fingerprint length");
- let mut dpath: Vec<ChildNumber> = Default::default();
-
- let mut d = &bytes[4..];
- while !d.is_empty() {
- match u32::consensus_decode(&mut d) {
- Ok(index) => dpath.push(index.into()),
- Err(e) => return Err(e)?,
- }
- }
-
- Ok((fprint, dpath.into()))
- }
- }
-
- // partial sigs
- impl Serialize for Vec<u8> {
- fn serialize(&self) -> Vec<u8> {
- self.clone()
- }
- }
-
- impl Deserialize for Vec<u8> {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- Ok(bytes.to_vec())
- }
- }
-
- impl Serialize for PsbtSighashType {
- fn serialize(&self) -> Vec<u8> {
- serialize(&self.to_u32())
- }
- }
-
- impl Deserialize for PsbtSighashType {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- let raw: u32 = encode::deserialize(bytes)?;
- Ok(PsbtSighashType::from_u32(raw))
- }
- }
-
- // Taproot related ser/deser
- impl Serialize for XOnlyPublicKey {
- fn serialize(&self) -> Vec<u8> {
- XOnlyPublicKey::serialize(self).to_vec()
- }
- }
-
- impl Deserialize for XOnlyPublicKey {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- XOnlyPublicKey::from_slice(bytes).map_err(|_| Error::InvalidXOnlyPublicKey)
- }
- }
-
- impl Serialize for taproot::Signature {
- fn serialize(&self) -> Vec<u8> {
- self.to_vec()
- }
- }
-
- impl Deserialize for taproot::Signature {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- taproot::Signature::from_slice(bytes).map_err(Error::InvalidTaprootSignature)
- }
- }
-
- impl Serialize for (XOnlyPublicKey, TapLeafHash) {
- fn serialize(&self) -> Vec<u8> {
- let ser_pk = self.0.serialize();
- let mut buf = Vec::with_capacity(ser_pk.len() + self.1.as_byte_array().len());
- buf.extend(ser_pk);
- buf.extend(self.1.as_byte_array());
- buf
- }
- }
-
- impl Deserialize for (XOnlyPublicKey, TapLeafHash) {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- if bytes.len() < 32 {
- return Err(Error::ConsensusEncoding(
- bitcoin::consensus::encode::Error::ParseFailed(
- "Not enough bytes for public key and tapleaf hash",
- ),
- ));
- }
- let a: XOnlyPublicKey = Deserialize::deserialize(&bytes[..32])?;
- let b: TapLeafHash = Deserialize::deserialize(&bytes[32..])?;
- Ok((a, b))
- }
- }
-
- impl Serialize for ControlBlock {
- fn serialize(&self) -> Vec<u8> {
- ControlBlock::serialize(self)
- }
- }
-
- impl Deserialize for ControlBlock {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- Self::decode(bytes).map_err(|_| Error::InvalidControlBlock)
- }
- }
-
- // Versioned ScriptBuf
- impl Serialize for (ScriptBuf, LeafVersion) {
- fn serialize(&self) -> Vec<u8> {
- let mut buf = Vec::with_capacity(self.0.len() + 1);
- buf.extend(self.0.as_bytes());
- buf.push(self.1.to_consensus());
- buf
- }
- }
-
- impl Deserialize for (ScriptBuf, LeafVersion) {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- if bytes.is_empty() {
- return Err(Error::ConsensusEncoding(
- bitcoin::consensus::encode::Error::ParseFailed(
- "Not enough bytes for script buf and leaf version",
- ),
- ));
- }
- // The last byte is LeafVersion.
- let script = ScriptBuf::deserialize(&bytes[..bytes.len() - 1])?;
- let leaf_ver = LeafVersion::from_consensus(bytes[bytes.len() - 1])
- .map_err(|_| Error::InvalidLeafVersion)?;
- Ok((script, leaf_ver))
- }
- }
-
- impl Serialize for (Vec<TapLeafHash>, KeySource) {
- fn serialize(&self) -> Vec<u8> {
- let mut buf = Vec::with_capacity(32 * self.0.len() + key_source_len(&self.1));
- self.0
- .consensus_encode(&mut buf)
- .expect("Vecs don't error allocation");
- // TODO: Add support for writing into a writer for key-source
- buf.extend(self.1.serialize());
- buf
- }
- }
-
- impl Deserialize for (Vec<TapLeafHash>, KeySource) {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- let (leafhash_vec, consumed) = deserialize_partial::<Vec<TapLeafHash>>(bytes)?;
- let key_source = KeySource::deserialize(&bytes[consumed..])?;
- Ok((leafhash_vec, key_source))
- }
- }
-
- impl Serialize for TapTree {
- fn serialize(&self) -> Vec<u8> {
- let capacity = self
- .script_leaves()
- .map(|l| {
- l.script().len() + UncheckedVarInt(l.script().len() as u64).size() // script version
- + 1 // merkle branch
- + 1 // leaf version
- })
- .sum::<usize>();
- let mut buf = Vec::with_capacity(capacity);
- for leaf_info in self.script_leaves() {
- // # Cast Safety:
- //
- // TaprootMerkleBranch can only have len atmost 128(TAPROOT_CONTROL_MAX_NODE_COUNT).
- // safe to cast from usize to u8
- buf.push(leaf_info.merkle_branch().len() as u8);
- buf.push(leaf_info.version().to_consensus());
- leaf_info
- .script()
- .consensus_encode(&mut buf)
- .expect("Vecs dont err");
- }
- buf
- }
- }
-
- impl Deserialize for TapTree {
- fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
- let mut builder = TaprootBuilder::new();
- let mut bytes_iter = bytes.iter();
- while let Some(depth) = bytes_iter.next() {
- let version = bytes_iter
- .next()
- .ok_or(Error::Taproot("Invalid Taproot Builder"))?;
- let (script, consumed) = deserialize_partial::<ScriptBuf>(bytes_iter.as_slice())?;
- if consumed > 0 {
- bytes_iter.nth(consumed - 1);
- }
- let leaf_version =
- LeafVersion::from_consensus(*version).map_err(|_| Error::InvalidLeafVersion)?;
- builder = builder
- .add_leaf_with_ver(*depth, script, leaf_version)
- .map_err(|_| Error::Taproot("Tree not in DFS order"))?;
- }
- TapTree::try_from(builder).map_err(Error::TapTree)
+ fn keyless(key_type: u8) -> raw::Key {
+ raw::Key {
+ type_value: key_type,
+ key: vec![],
}
}
- // Helper function to compute key source len
- fn key_source_len(key_source: &KeySource) -> usize {
- 4 + 4 * (key_source.1).as_ref().len()
+ fn values_for(map: &[(Vec<u8>, Vec<u8>)], key_type: u8) -> Vec<&Vec<u8>> {
+ map.iter()
+ .filter(|(key, _)| key.as_slice() == [key_type])
+ .map(|(_, value)| value)
+ .collect()
+ }
+
+ #[test]
+ fn global_map_is_translated_to_v2() {
+ let maps = get_v2_maps(&unsigned_psbt()).unwrap();
+
+ assert_eq!(maps.inputs.len(), 1);
+ assert_eq!(maps.outputs.len(), 1);
+
+ assert!(
+ values_for(&maps.global, PSBT_GLOBAL_UNSIGNED_TX).is_empty(),
+ "the v0 unsigned transaction must not be forwarded"
+ );
+ for key_type in [
+ PSBT_GLOBAL_TX_VERSION,
+ PSBT_GLOBAL_FALLBACK_LOCKTIME,
+ PSBT_GLOBAL_INPUT_COUNT,
+ PSBT_GLOBAL_OUTPUT_COUNT,
+ PSBT_GLOBAL_VERSION,
+ ]
+ .iter()
+ {
+ assert_eq!(
+ values_for(&maps.global, *key_type).len(),
+ 1,
+ "global key type {:#04x} must be present exactly once",
+ key_type
+ );
+ }
+
+ // with no required locktime of its own, the PSBT's fallback comes from the unsigned tx
+ assert_eq!(
+ values_for(&maps.global, PSBT_GLOBAL_FALLBACK_LOCKTIME)[0],
+ &LOCKTIME.to_le_bytes().to_vec()
+ );
+ assert_eq!(
+ values_for(&maps.global, PSBT_GLOBAL_VERSION)[0],
+ &2_u32.to_le_bytes().to_vec()
+ );
+ }
+
+ #[test]
+ fn input_and_output_maps_get_their_v2_fields() {
+ let maps = get_v2_maps(&unsigned_psbt()).unwrap();
+
+ assert_eq!(
+ values_for(&maps.inputs[0], PSBT_IN_SEQUENCE)[0],
+ &Sequence::MAX.0.to_le_bytes().to_vec()
+ );
+ assert_eq!(values_for(&maps.inputs[0], PSBT_IN_PREVIOUS_TXID).len(), 1);
+ assert_eq!(values_for(&maps.inputs[0], PSBT_IN_OUTPUT_INDEX).len(), 1);
+
+ assert_eq!(
+ values_for(&maps.outputs[0], PSBT_OUT_AMOUNT)[0],
+ &1_000_u64.to_le_bytes().to_vec()
+ );
+ assert_eq!(values_for(&maps.outputs[0], PSBT_OUT_SCRIPT).len(), 1);
+ }
+
+ /// A key type rust-bitcoin does not know must still reach the app: this is what lets it read
+ /// the BIP-370 per-input required locktimes.
+ #[test]
+ fn keys_unknown_to_rust_bitcoin_are_forwarded() {
+ const PSBT_IN_REQUIRED_HEIGHT_LOCKTIME: u8 = 0x12;
+ let height = 10_000_u32.to_le_bytes().to_vec();
+
+ let mut psbt = unsigned_psbt();
+ psbt.inputs[0]
+ .unknown
+ .insert(keyless(PSBT_IN_REQUIRED_HEIGHT_LOCKTIME), height.clone());
+
+ let maps = get_v2_maps(&psbt).unwrap();
+ assert_eq!(
+ values_for(&maps.inputs[0], PSBT_IN_REQUIRED_HEIGHT_LOCKTIME),
+ vec![&height]
+ );
+ }
+
+ /// A v0 PSBT that already re-encodes part of its unsigned transaction as a v2 field is a
+ /// hybrid whose two encodings may disagree. It is rejected rather than resolved: emitting
+ /// both would also duplicate a key, which the app rejects, since a map's keys must be
+ /// strictly increasing.
+ #[test]
+ fn an_existing_v2_key_is_rejected() {
+ // a value that contradicts the unsigned transaction, and one that agrees with it: both
+ // are refused, because the PSBT is malformed either way
+ for fallback in [42_u32, LOCKTIME] {
+ let mut psbt = unsigned_psbt();
+ psbt.unknown.insert(
+ keyless(PSBT_GLOBAL_FALLBACK_LOCKTIME),
+ fallback.to_le_bytes().to_vec(),
+ );
+ assert_eq!(
+ get_v2_maps(&psbt).err(),
+ Some(PsbtV2Error::UnexpectedV2Field(
+ PSBT_GLOBAL_FALLBACK_LOCKTIME
+ ))
+ );
+ }
+
+ let mut psbt = unsigned_psbt();
+ psbt.inputs[0]
+ .unknown
+ .insert(keyless(PSBT_IN_SEQUENCE), 7_u32.to_le_bytes().to_vec());
+ assert_eq!(
+ get_v2_maps(&psbt).err(),
+ Some(PsbtV2Error::UnexpectedV2Field(PSBT_IN_SEQUENCE))
+ );
+
+ let mut psbt = unsigned_psbt();
+ psbt.outputs[0]
+ .unknown
+ .insert(keyless(PSBT_OUT_AMOUNT), 9_u64.to_le_bytes().to_vec());
+ assert_eq!(
+ get_v2_maps(&psbt).err(),
+ Some(PsbtV2Error::UnexpectedV2Field(PSBT_OUT_AMOUNT))
+ );
+ }
+
+ #[test]
+ fn a_truncated_serialization_is_an_error_not_a_panic() {
+ assert!(split_maps(b"psbt", 1).is_none());
+ assert!(split_maps(b"psbt\xff", 1).is_none(), "no map separator");
+ assert!(
+ split_maps(b"psbt\xff\x00", 2).is_none(),
+ "fewer maps than asked for"
+ );
+ // a key length that runs past the end of the buffer
+ assert!(split_maps(b"psbt\xff\x08\x01\x02", 1).is_none());
}
}Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.