Skip to content

Commit 136cd21

Browse files
committed
btc: fix requesting previous transactions when not needed
The previous transaction of every input in a Bitcoin transaction needs to be streamed to the BitBox, unless all inputs are Taproot inputs. They are needed for the BitBox to verify the input amount. Since Taproot, that is not necessary because all input amounts are part of the sighash. The firmware currently inspects the "scriptConfigs" to determine if all inputs are taproot. Bug: the scriptConfigs is also used to indicate outputs belonging to the same account (either change outputs or outputs verified as "This BitBox (same account): …". As a result, if all inputs are Taproot, but the change output is not Taproot (or coins are sent to a non-Taproot address of the same account), then previous transactions are requested/streamed even if they don't need to be. Fix: we let the client libraries set the flag if prevtxs are required or not, and reject the transaction if they set it to "not required" even if there is a non-Taproot input present. The previous way is preserved for backwards compatibility with client libraries that do not set this new flag.
1 parent 25c3a2b commit 136cd21

File tree

7 files changed

+244
-54
lines changed

7 files changed

+244
-54
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
1414
- simulator: simulate a Nova device
1515
- Add API call to fetch multiple xpubs at once
1616
- Add the option for the simulator to write its memory to file.
17+
- Bitcoin: do not request previous transactions if all inputs are Taproot, even if an output paying
18+
to the same account is not Taproot
1719

1820
### 9.23.1
1921
- EVM: add HyperEVM (HYPE) and SONIC (S) to known networks

messages/btc.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ message BTCSignInitRequest {
129129
// used script configs for outputs that send to an address of the same keystore, but not
130130
// necessarily the same account (as defined by `script_configs` above).
131131
repeated BTCScriptConfigWithKeypath output_script_configs = 10;
132+
133+
enum PrevTxs {
134+
PREV_TXS_DEFAULT = 0;
135+
PREV_TXS_REQUIRED = 1;
136+
PREV_TXS_NOT_REQUIRED = 2;
137+
}
138+
// Set this to:
139+
// - `PREV_TXS_DEFAULT` for compatibility mode, the value will be determined using `script_configs`.
140+
// This is not robust, as non-Taproot change outputs are included there and falsely leads to
141+
// previous transactions being required.
142+
// - `PREV_TXS_REQUIRED` if not all transaction inputs are Taproot.604
143+
// - `PREV_TXS_NOT_REQUIRED` if all transaction inputs are Taproot.
144+
PrevTxs prev_txs = 11;
132145
}
133146

134147
message BTCSignNextResponse {

py/bitbox02/bitbox02/bitbox02/bitbox02.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,9 @@ def btc_sign(
471471

472472
sigs: List[Tuple[int, bytes]] = []
473473

474+
all_inputs_are_taproot = all(
475+
is_taproot(script_configs[inp["script_config_index"]]) for inp in inputs
476+
)
474477
# Init request
475478
request = hww.Request()
476479
request.btc_sign_init.CopyFrom(
@@ -483,6 +486,11 @@ def btc_sign(
483486
locktime=locktime,
484487
format_unit=format_unit,
485488
output_script_configs=output_script_configs,
489+
prev_txs=(
490+
btc.BTCSignInitRequest.PrevTxs.PREV_TXS_NOT_REQUIRED
491+
if all_inputs_are_taproot
492+
else btc.BTCSignInitRequest.PrevTxs.PREV_TXS_REQUIRED
493+
),
486494
)
487495
)
488496
next_response = self._msg_query(request, expected_response="btc_sign_next").btc_sign_next

py/bitbox02/bitbox02/communication/generated/btc_pb2.py

Lines changed: 52 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,21 @@ class BTCSignInitRequest(google.protobuf.message.Message):
342342
SAT: BTCSignInitRequest.FormatUnit.ValueType # 1
343343
"""Only valid for BTC/TBTC, formats as "sat"/"tsat"."""
344344

345+
class _PrevTxs:
346+
ValueType = typing.NewType("ValueType", builtins.int)
347+
V: typing_extensions.TypeAlias = ValueType
348+
349+
class _PrevTxsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCSignInitRequest._PrevTxs.ValueType], builtins.type):
350+
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
351+
PREV_TXS_DEFAULT: BTCSignInitRequest._PrevTxs.ValueType # 0
352+
PREV_TXS_REQUIRED: BTCSignInitRequest._PrevTxs.ValueType # 1
353+
PREV_TXS_NOT_REQUIRED: BTCSignInitRequest._PrevTxs.ValueType # 2
354+
355+
class PrevTxs(_PrevTxs, metaclass=_PrevTxsEnumTypeWrapper): ...
356+
PREV_TXS_DEFAULT: BTCSignInitRequest.PrevTxs.ValueType # 0
357+
PREV_TXS_REQUIRED: BTCSignInitRequest.PrevTxs.ValueType # 1
358+
PREV_TXS_NOT_REQUIRED: BTCSignInitRequest.PrevTxs.ValueType # 2
359+
345360
COIN_FIELD_NUMBER: builtins.int
346361
SCRIPT_CONFIGS_FIELD_NUMBER: builtins.int
347362
VERSION_FIELD_NUMBER: builtins.int
@@ -351,6 +366,7 @@ class BTCSignInitRequest(google.protobuf.message.Message):
351366
FORMAT_UNIT_FIELD_NUMBER: builtins.int
352367
CONTAINS_SILENT_PAYMENT_OUTPUTS_FIELD_NUMBER: builtins.int
353368
OUTPUT_SCRIPT_CONFIGS_FIELD_NUMBER: builtins.int
369+
PREV_TXS_FIELD_NUMBER: builtins.int
354370
coin: global___BTCCoin.ValueType
355371
version: builtins.int
356372
"""must be 1 or 2"""
@@ -360,6 +376,14 @@ class BTCSignInitRequest(google.protobuf.message.Message):
360376
"""must be <500000000"""
361377
format_unit: global___BTCSignInitRequest.FormatUnit.ValueType
362378
contains_silent_payment_outputs: builtins.bool
379+
prev_txs: global___BTCSignInitRequest.PrevTxs.ValueType
380+
"""Set this to:
381+
- `PREV_TXS_DEFAULT` for compatibility mode, the value will be determined using `script_configs`.
382+
This is not robust, as non-Taproot change outputs are included there and falsely leads to
383+
previous transactions being required.
384+
- `PREV_TXS_REQUIRED` if not all transaction inputs are Taproot.604
385+
- `PREV_TXS_NOT_REQUIRED` if all transaction inputs are Taproot.
386+
"""
363387
@property
364388
def script_configs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BTCScriptConfigWithKeypath]:
365389
"""used script configs in inputs and changes"""
@@ -382,8 +406,9 @@ class BTCSignInitRequest(google.protobuf.message.Message):
382406
format_unit: global___BTCSignInitRequest.FormatUnit.ValueType = ...,
383407
contains_silent_payment_outputs: builtins.bool = ...,
384408
output_script_configs: collections.abc.Iterable[global___BTCScriptConfigWithKeypath] | None = ...,
409+
prev_txs: global___BTCSignInitRequest.PrevTxs.ValueType = ...,
385410
) -> None: ...
386-
def ClearField(self, field_name: typing.Literal["coin", b"coin", "contains_silent_payment_outputs", b"contains_silent_payment_outputs", "format_unit", b"format_unit", "locktime", b"locktime", "num_inputs", b"num_inputs", "num_outputs", b"num_outputs", "output_script_configs", b"output_script_configs", "script_configs", b"script_configs", "version", b"version"]) -> None: ...
411+
def ClearField(self, field_name: typing.Literal["coin", b"coin", "contains_silent_payment_outputs", b"contains_silent_payment_outputs", "format_unit", b"format_unit", "locktime", b"locktime", "num_inputs", b"num_inputs", "num_outputs", b"num_outputs", "output_script_configs", b"output_script_configs", "prev_txs", b"prev_txs", "script_configs", b"script_configs", "version", b"version"]) -> None: ...
387412

388413
global___BTCSignInitRequest = BTCSignInitRequest
389414

src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,20 @@ async fn _process(
717717
let mut hasher_amounts = Sha256::new();
718718
let mut hasher_scriptpubkeys = Sha256::new();
719719

720-
// Are all inputs taproot?
721-
let taproot_only = validated_script_configs.iter().all(is_taproot);
720+
let prev_txs = pb::btc_sign_init_request::PrevTxs::try_from(request.prev_txs)?;
721+
722+
// We will request to stream previous transactions if not all inputs are Taproot.
723+
let prevtxs_required: bool = match prev_txs {
724+
// For backwards compatibility for client's that don't provide the `prev_txs` field: handle
725+
// it like before `prev_txs` was introduced by inspecting the script configs and seeing if
726+
// all of them are taproot. This is not robust, as non-Taproot change outputs are included
727+
// there and falsely leads to previous transactions being required.
728+
pb::btc_sign_init_request::PrevTxs::Default => {
729+
!validated_script_configs.iter().all(is_taproot)
730+
}
731+
pb::btc_sign_init_request::PrevTxs::Required => true,
732+
pb::btc_sign_init_request::PrevTxs::NotRequired => false,
733+
};
722734

723735
let mut silent_payment = if request.contains_silent_payment_outputs {
724736
Some(SilentPayment::new(SECP256K1, coin.try_into()?))
@@ -775,7 +787,7 @@ async fn _process(
775787
hasher_scriptpubkeys.update(serialize_varint(pk_script.len() as u64).as_slice());
776788
hasher_scriptpubkeys.update(pk_script.as_slice());
777789

778-
if !taproot_only {
790+
if prevtxs_required {
779791
handle_prevtx(
780792
input_index,
781793
&tx_input,
@@ -784,6 +796,8 @@ async fn _process(
784796
&mut next_response,
785797
)
786798
.await?;
799+
} else if !is_taproot(script_config_account) {
800+
return Err(Error::InvalidInput);
787801
}
788802

789803
if let Some(ref mut silent_payment) = silent_payment {
@@ -1572,6 +1586,7 @@ mod tests {
15721586
.outputs
15731587
.iter()
15741588
.any(|output| output.silent_payment.is_some()),
1589+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
15751590
}
15761591
}
15771592

@@ -1595,6 +1610,7 @@ mod tests {
15951610
locktime: self.locktime,
15961611
format_unit: FormatUnit::Default as _,
15971612
contains_silent_payment_outputs: false,
1613+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
15981614
}
15991615
}
16001616

@@ -1679,6 +1695,7 @@ mod tests {
16791695
locktime: 0,
16801696
format_unit: FormatUnit::Default as _,
16811697
contains_silent_payment_outputs: false,
1698+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
16821699
};
16831700

16841701
{
@@ -1871,6 +1888,7 @@ mod tests {
18711888
locktime: 0,
18721889
format_unit: FormatUnit::Default as _,
18731890
contains_silent_payment_outputs: false,
1891+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
18741892
}
18751893
)),
18761894
Err(Error::InvalidInput)
@@ -2147,6 +2165,57 @@ mod tests {
21472165
assert!(unsafe { !PREVTX_REQUESTED });
21482166
}
21492167

2168+
/// Test signing if all inputs are of type P2TR, but change is not of type P2TR.
2169+
/// Previous transactions are requested for backwards compatibility when
2170+
#[test]
2171+
fn test_script_type_p2tr_different_change() {
2172+
let transaction =
2173+
alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
2174+
for input in transaction.borrow_mut().inputs.iter_mut() {
2175+
input.input.keypath[0] = 86 + HARDENED;
2176+
input.input.script_config_index = 1;
2177+
}
2178+
2179+
let tx = transaction.clone();
2180+
// Check that previous transactions are not streamed, as all inputs are taproot.
2181+
static mut PREVTX_REQUESTED: bool = false;
2182+
*crate::hww::MOCK_NEXT_REQUEST.0.borrow_mut() =
2183+
Some(Box::new(move |response: Response| {
2184+
let next = extract_next(&response);
2185+
if NextType::try_from(next.r#type).unwrap() == NextType::PrevtxInit {
2186+
unsafe { PREVTX_REQUESTED = true }
2187+
}
2188+
Ok(tx.borrow().make_host_request(response))
2189+
}));
2190+
2191+
mock_unlocked();
2192+
bitbox02::random::fake_reset();
2193+
let mut init_request = transaction.borrow().init_request();
2194+
init_request
2195+
.script_configs
2196+
.push(pb::BtcScriptConfigWithKeypath {
2197+
script_config: Some(pb::BtcScriptConfig {
2198+
config: Some(pb::btc_script_config::Config::SimpleType(
2199+
SimpleType::P2tr as _,
2200+
)),
2201+
}),
2202+
keypath: vec![86 + HARDENED, 0 + HARDENED, 10 + HARDENED],
2203+
});
2204+
2205+
init_request.prev_txs = pb::btc_sign_init_request::PrevTxs::NotRequired as _;
2206+
assert!(block_on(process(&mut TestingHal::new(), &init_request)).is_ok());
2207+
assert!(unsafe { !PREVTX_REQUESTED });
2208+
2209+
// Also test compatibility mode - before the introduction of `prev_txs`, the previous
2210+
// transactions were wrongly requested in this case.
2211+
unsafe {
2212+
PREVTX_REQUESTED = false;
2213+
}
2214+
init_request.prev_txs = pb::btc_sign_init_request::PrevTxs::Default as _;
2215+
assert!(block_on(process(&mut TestingHal::new(), &init_request)).is_ok());
2216+
assert!(unsafe { PREVTX_REQUESTED });
2217+
}
2218+
21502219
/// Test signing if with mixed inputs, one of them being taproot. Previous transactions of all
21512220
/// inputs should be streamed in this case.
21522221
#[test]
@@ -2180,11 +2249,31 @@ mod tests {
21802249
}),
21812250
keypath: vec![86 + HARDENED, 0 + HARDENED, 10 + HARDENED],
21822251
});
2252+
2253+
// In compatibility mode, prevtxs are correctly requested.
2254+
init_request.prev_txs = pb::btc_sign_init_request::PrevTxs::Default as _;
2255+
unsafe { PREVTX_REQUESTED = 0 };
21832256
assert!(block_on(process(&mut TestingHal::new(), &init_request)).is_ok());
21842257
assert_eq!(
21852258
unsafe { PREVTX_REQUESTED },
21862259
transaction.borrow().inputs.len() as _
21872260
);
2261+
2262+
// Same as when the client explicitly states it.
2263+
init_request.prev_txs = pb::btc_sign_init_request::PrevTxs::Required as _;
2264+
unsafe { PREVTX_REQUESTED = 0 };
2265+
assert!(block_on(process(&mut TestingHal::new(), &init_request)).is_ok());
2266+
assert_eq!(
2267+
unsafe { PREVTX_REQUESTED },
2268+
transaction.borrow().inputs.len() as _
2269+
);
2270+
2271+
// Client can't lie about it.
2272+
init_request.prev_txs = pb::btc_sign_init_request::PrevTxs::NotRequired as _;
2273+
assert_eq!(
2274+
block_on(process(&mut TestingHal::new(), &init_request)),
2275+
Err(Error::InvalidInput)
2276+
);
21882277
}
21892278

21902279
/// Test signing UTXOs with high keypath address indices. Even though we don't support verifying
@@ -2881,6 +2970,7 @@ mod tests {
28812970
locktime: tx.locktime,
28822971
format_unit: FormatUnit::Default as _,
28832972
contains_silent_payment_outputs: false,
2973+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
28842974
}
28852975
};
28862976

@@ -2976,6 +3066,7 @@ mod tests {
29763066
locktime: tx.locktime,
29773067
format_unit: FormatUnit::Default as _,
29783068
contains_silent_payment_outputs: false,
3069+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
29793070
}
29803071
};
29813072
assert_eq!(
@@ -3047,6 +3138,7 @@ mod tests {
30473138
locktime: tx.locktime,
30483139
format_unit: FormatUnit::Default as _,
30493140
contains_silent_payment_outputs: false,
3141+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
30503142
}
30513143
};
30523144
let result = block_on(process(&mut TestingHal::new(), &init_request));
@@ -3124,6 +3216,7 @@ mod tests {
31243216
locktime: tx.locktime,
31253217
format_unit: FormatUnit::Default as _,
31263218
contains_silent_payment_outputs: false,
3219+
prev_txs: pb::btc_sign_init_request::PrevTxs::Default as _,
31273220
}
31283221
};
31293222
let result = block_on(process(&mut TestingHal::new(), &init_request));

src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,14 @@ pub struct BtcSignInitRequest {
599599
/// necessarily the same account (as defined by `script_configs` above).
600600
#[prost(message, repeated, tag = "10")]
601601
pub output_script_configs: ::prost::alloc::vec::Vec<BtcScriptConfigWithKeypath>,
602+
/// Set this to:
603+
/// - `PREV_TXS_DEFAULT` for compatibility mode, the value will be determined using `script_configs`.
604+
/// This is not robust, as non-Taproot change outputs are included there and falsely leads to
605+
/// previous transactions being required.
606+
/// - `PREV_TXS_REQUIRED` if not all transaction inputs are Taproot.604
607+
/// - `PREV_TXS_NOT_REQUIRED` if all transaction inputs are Taproot.
608+
#[prost(enumeration = "btc_sign_init_request::PrevTxs", tag = "11")]
609+
pub prev_txs: i32,
602610
}
603611
/// Nested message and enum types in `BTCSignInitRequest`.
604612
pub mod btc_sign_init_request {
@@ -640,6 +648,45 @@ pub mod btc_sign_init_request {
640648
}
641649
}
642650
}
651+
#[derive(
652+
Clone,
653+
Copy,
654+
Debug,
655+
PartialEq,
656+
Eq,
657+
Hash,
658+
PartialOrd,
659+
Ord,
660+
::prost::Enumeration
661+
)]
662+
#[repr(i32)]
663+
pub enum PrevTxs {
664+
Default = 0,
665+
Required = 1,
666+
NotRequired = 2,
667+
}
668+
impl PrevTxs {
669+
/// String value of the enum field names used in the ProtoBuf definition.
670+
///
671+
/// The values are not transformed in any way and thus are considered stable
672+
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
673+
pub fn as_str_name(&self) -> &'static str {
674+
match self {
675+
PrevTxs::Default => "PREV_TXS_DEFAULT",
676+
PrevTxs::Required => "PREV_TXS_REQUIRED",
677+
PrevTxs::NotRequired => "PREV_TXS_NOT_REQUIRED",
678+
}
679+
}
680+
/// Creates an enum from field names used in the ProtoBuf definition.
681+
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
682+
match value {
683+
"PREV_TXS_DEFAULT" => Some(Self::Default),
684+
"PREV_TXS_REQUIRED" => Some(Self::Required),
685+
"PREV_TXS_NOT_REQUIRED" => Some(Self::NotRequired),
686+
_ => None,
687+
}
688+
}
689+
}
643690
}
644691
#[allow(clippy::derive_partial_eq_without_eq)]
645692
#[derive(Clone, PartialEq, ::prost::Message)]

0 commit comments

Comments
 (0)