rust client: build PSBTv2 maps from all pairs, not an enumerated list
What changed, and why it matters
This commit refactors how the Ledger Bitcoin app's Rust client converts a PSBT (Partially Signed Bitcoin Transaction) from version 0 to version 2. Previously, the code manually listed every PSBT field it knew how to serialize, which risked silently dropping newer or unknown fields. The new code lets the rust-bitcoin library do most of the serialization, then only adds, removes, or changes the specific fields that differ between v0 and v2. It also adds checks that reject malformed PSBTs where v2 fields already exist and conflict with the v0 transaction data. The change is a defensive cleanup that reduces the chance of an incomplete or inconsistent PSBT being sent to the hardware wallet.
Review the new `split_maps` and `read_prefixed` parsing logic for off-by-one or length-prefix edge cases, and ensure the test suite exercises PSBTs with proprietary, unknown, and all standard input/output fields. Consider fuzzing `get_v2_maps` against arbitrary PSBT byte streams. No immediate incident response is indicated by the diff alone.
Security signals we found
Refactor of PSBT serialization path used before signing on hardware wallet
Removal of hand-maintained field enumeration that could omit or mis-serialize PSBT fields
Addition of explicit error handling for PSBTs containing pre-existing v2 keys that conflict with v0 transaction data
Delegation of known-field serialization to upstream rust-bitcoin library
Addition of unit tests covering malformed/truncated serialization and conflicting v2 fields
Evidence from the diff
The change replaces hand-rolled get_v2_global_pairs, get_v2_input_pairs, and get_v2_output_pairs with a single get_v2_maps that: (1) serializes the whole PSBT using rust-bitcoin’s v0 serializer, (2) splits the serialized byte stream into global/input/output maps, (3) drops PSBT_GLOBAL_UNSIGNED_TX, (4) injects the required v2 global/input/output fields derived from unsigned_tx, and (5) returns an error if any of those synthesized v2 keys already exist in the source PSBT. The client and async_client now use this unified function and no longer look up unsigned_tx.input/output by index themselves. A new PsbtV2Error enum and unit tests are added, including tests that unknown keys are forwarded and that conflicting v2 keys are rejected.
Changed components
bitcoin_client_rs/src/psbt.rsbitcoin_client_rs/src/client.rsbitcoin_client_rs/src/async_client.rsbitcoin_client_rs/src/protocol.rsInspect captured patch +344 / −848
### 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 35/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.