Merge remote-tracking branch 'agent/benma-agent/bitcoin-time-locks'
What changed, and why it matters
This commit adds support for timestamp-based Bitcoin nLockTime values (values of 500,000,000 or higher) in the BitBox02 hardware wallet. Previously, such locktimes were rejected as invalid. The change also removes the on-screen RBF (Replace-By-Fee) status display from locktime confirmations and updates the Python library to require firmware version 9.27.0 when using timestamp locktimes. It is a feature addition with associated UI changes, not a security fix for a known vulnerability.
Treat as a normal firmware feature release. Users relying on locktime/RBF warnings should review the new UI behavior. No urgent security patch is indicated by the commit itself.
Security signals we found
Feature expansion: previously rejected timestamp nLockTime values are now accepted and signed
UI change: RBF status is no longer shown during locktime confirmation
Version gating added in Python client for timestamp locktimes (>= v9.27.0)
No evidence of a vulnerability fix, CVE, or vendor security advisory in the commit materials
Evidence from the diff
The patch removes the request.locktime >= 500000000 => Error::InvalidInput check in signtx.rs, adds format_locktime() to display block-height or UTC timestamp locktimes, drops the per-chain rbf_support flag and RBF wording from confirmations, and updates protobuf/Python comments and version gating. Test vectors are updated to expect the new locktime screens and a new policy test with a timestamp locktime (after(1800000000)).
Changed components
src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rssrc/rust/bitbox02-rust/src/hww/api/bitcoin/params.rspy/bitbox02/bitbox02/bitbox02/bitbox02.pymessages/btc.protosrc/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rsBitBox02 Bitcoin transaction signing flowInspect captured patch +496 / −101
### CHANGELOG.md
@@ -10,6 +10,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
- Add support for BitBoxSync
- Display long transaction and swap amounts in full instead of truncating them
- Bitcoin: allow signing messages with keys in the m/45' application namespace
+- Bitcoin: support timestamp-based absolute locktimes and remove the RBF status display
### v9.26.5
- Security improvements
### messages/btc.proto
@@ -103,7 +103,8 @@ message BTCSignInitRequest {
uint32 version = 4; // must be 1 or 2
uint32 num_inputs = 5;
uint32 num_outputs = 6;
- uint32 locktime = 7; // must be <500000000
+ // Consensus nLockTime: values below 500000000 are block heights, otherwise Unix timestamps.
+ uint32 locktime = 7;
enum FormatUnit {
// According to `coin` (BTC, LTC, etc.).
### py/bitbox02/bitbox02/bitbox02/bitbox02.py
@@ -458,17 +458,21 @@ def btc_sign(
if `btc_sign_needs_prevtxs()` returns True.
outputs: transaction outputs. Can be an external output
(BTCOutputExternal) or an internal output for change (BTCOutputInternal).
- version, locktime: reserved for future use.
+ version: transaction version, 1 or 2.
+ locktime: consensus nLockTime, interpreted as a block height or Unix timestamp.
format_unit: defines in which unit amounts will be displayed
output_script_configs: script types for outputs belonging to the same keystore
Returns: list of (input index, signature) tuples.
Raises Bitbox02Exception with ERR_USER_ABORT on user abort.
"""
# pylint: disable=no-member,too-many-branches,too-many-statements
- # Reserved for future use.
assert version in (1, 2)
+ if locktime >= 500_000_000:
+ # Timestamp-based nLockTime supported since v9.27.0.
+ self._require_atleast(semver.VersionInfo(9, 27, 0))
+
if any(map(is_taproot, script_configs)):
self._require_atleast(semver.VersionInfo(9, 10, 0))
### py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
@@ -345,7 +345,7 @@ class BTCSignInitRequest(google.protobuf.message.Message):
num_inputs: builtins.int
num_outputs: builtins.int
locktime: builtins.int
- """must be <500000000"""
+ """Consensus nLockTime: values below 500000000 are block heights, otherwise Unix timestamps."""
format_unit: global___BTCSignInitRequest.FormatUnit.ValueType
contains_silent_payment_outputs: builtins.bool
@property
### src/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rs
@@ -670,7 +670,7 @@ pub struct BtcSignInitRequest {
pub num_inputs: u32,
#[prost(uint32, tag = "6")]
pub num_outputs: u32,
- /// must be <500000000
+ /// Consensus nLockTime: values below 500000000 are block heights, otherwise Unix timestamps.
#[prost(uint32, tag = "7")]
pub locktime: u32,
#[prost(enumeration = "btc_sign_init_request::FormatUnit", tag = "8")]
### src/rust/bitbox-test-vectors/src/btc_transaction/cases/additional_psbt.rs
@@ -561,7 +561,7 @@ fn high_address_index() -> TestVector {
)
}
-fn locktime(block: u32, sequence: u32, rbf: bool) -> TestVector {
+fn locktime(block: u32, sequence: u32, previous_body: &str) -> TestVector {
let spend = simple_spend(
SimpleType::P2wpkh,
"m/84'/1'/0'",
@@ -570,23 +570,24 @@ fn locktime(block: u32, sequence: u32, rbf: bool) -> TestVector {
block,
p2tr_script(TBTC_EXTERNAL_XONLY),
);
- let qualifier = if rbf { "rbf" } else { "non-rbf" };
+ let previous_locktime_screen = Screen::Confirm {
+ title: String::new(),
+ body: previous_body.into(),
+ longtouch: false,
+ };
let locktime_screen = Screen::Confirm {
title: String::new(),
- body: format!(
- "Locktime on block:\n{block}\nTransaction is {}RBF",
- if rbf { "" } else { "not " }
- ),
+ body: format!("Locktime on block:\n{block}"),
longtouch: false,
};
transaction_vector(
- &format!("locktime-{qualifier}"),
- &format!("Displays block locktime {block} and its {qualifier} sequence semantics."),
+ &format!("locktime-sequence-{sequence:08x}"),
+ &format!("Displays block locktime {block} for transaction sequence {sequence:#010x}."),
Coin::Tbtc,
spend.psbt,
PsbtSignOptions::default(),
vec![spend.expected_signature],
- screens::simple_tbtc(&[locktime_screen]),
+ screens::simple_tbtc_locktime(&[previous_locktime_screen], &[locktime_screen]),
)
}
@@ -601,7 +602,7 @@ fn zero_locktime() -> TestVector {
);
transaction_vector(
"locktime-zero",
- "Suppresses the locktime confirmation when locktime is zero even if the sequence signals RBF.",
+ "Suppresses the locktime confirmation when locktime is zero even if the sequence is non-final.",
Coin::Tbtc,
spend.psbt,
PsbtSignOptions::default(),
@@ -670,7 +671,7 @@ fn silent_payment_rejects_owned_output() -> TestVector {
)
}
-fn ltc_locktime(sequence: u32, qualifier: &str) -> TestVector {
+fn ltc_locktime(sequence: u32) -> TestVector {
let spend = simple_spend(
SimpleType::P2wpkh,
"m/84'/2'/0'",
@@ -679,19 +680,24 @@ fn ltc_locktime(sequence: u32, qualifier: &str) -> TestVector {
10,
ltc_external_script(),
);
- let locktime_screen = Screen::Confirm {
+ let previous_locktime_screen = Screen::Confirm {
title: String::new(),
body: "Locktime on block:\n10\n".into(),
longtouch: false,
};
+ let locktime_screen = Screen::Confirm {
+ title: String::new(),
+ body: "Locktime on block:\n10".into(),
+ longtouch: false,
+ };
transaction_vector(
- &format!("locktime-litecoin-{qualifier}"),
- "Displays a Litecoin block locktime without Bitcoin-specific RBF wording.",
+ &format!("locktime-litecoin-sequence-{sequence:08x}"),
+ &format!("Displays a Litecoin block locktime for transaction sequence {sequence:#010x}."),
Coin::Ltc,
spend.psbt,
PsbtSignOptions::default(),
vec![spend.expected_signature],
- screens::simple_ltc(&[locktime_screen]),
+ screens::simple_ltc_locktime(&[previous_locktime_screen], &[locktime_screen]),
)
}
@@ -706,10 +712,18 @@ pub fn all() -> Vec<TestVector> {
p2wpkh_p2sh(),
high_address_index(),
zero_locktime(),
- locktime(10, Sequence::MAX.0 - 1, false),
- locktime(10, Sequence::MAX.0 - 2, true),
- ltc_locktime(Sequence::MAX.0 - 1, "non-rbf-sequence"),
- ltc_locktime(Sequence::MAX.0 - 2, "rbf-sequence"),
+ locktime(
+ 10,
+ Sequence::MAX.0 - 1,
+ "Locktime on block:\n10\nTransaction is not RBF",
+ ),
+ locktime(
+ 10,
+ Sequence::MAX.0 - 2,
+ "Locktime on block:\n10\nTransaction is RBF",
+ ),
+ ltc_locktime(Sequence::MAX.0 - 1),
+ ltc_locktime(Sequence::MAX.0 - 2),
p2tr_output_btc(),
silent_payment_rejects_owned_output(),
op_return_nonascii(),
### src/rust/bitbox-test-vectors/src/btc_transaction/cases/descriptor_psbt.rs
@@ -8,7 +8,7 @@ use super::common::{
};
use super::screens;
use crate::btc_transaction::{
- Coin, KeyOriginInfo, MultisigScriptType, PsbtSignOptions, Registration, ScriptConfig,
+ Coin, KeyOriginInfo, MultisigScriptType, PsbtSignOptions, Registration, Screen, ScriptConfig,
ScriptConfigWithKeypath, TestVector,
};
use bitcoin::bip32::{ChainCode, ChildNumber, DerivationPath, Fingerprint, KeySource, Xpriv, Xpub};
@@ -243,6 +243,8 @@ fn policy_spend(policy: &str, change_index: u32, sign_cosigner: bool) -> PolicyS
let our_xpub = simulator_xpub_at(&secp, &account);
let cosigner_root = seeded_xpriv(100);
let cosigner_xpub = account_xpub(&cosigner_root, &account);
+ let has_cosigner = policy.contains("@1/");
+ assert!(!sign_cosigner || has_cosigner);
let descriptor = policy
.replace("/**", "/<0;1>/*")
.replace(
@@ -265,20 +267,23 @@ fn policy_spend(policy: &str, change_index: u32, sign_cosigner: bool) -> PolicyS
if sign_cosigner {
add_cosigner_signature(&mut psbt, &cosigner_root);
}
+ let mut keys = vec![KeyOriginInfo {
+ root_fingerprint: Some(simulator_xprv().fingerprint(&secp).to_string()),
+ keypath: Some(keypath(&account)),
+ xpub: our_xpub.to_string(),
+ }];
+ let mut display_keys = vec![format!("This device: {}", screens::DEVICE_POLICY_XPUB)];
+ if has_cosigner {
+ keys.push(KeyOriginInfo {
+ root_fingerprint: None,
+ keypath: None,
+ xpub: cosigner_xpub.to_string(),
+ });
+ display_keys.push(cosigner_xpub.to_string());
+ }
let config = ScriptConfig::Policy {
policy: policy.into(),
- keys: vec![
- KeyOriginInfo {
- root_fingerprint: Some(simulator_xprv().fingerprint(&secp).to_string()),
- keypath: Some(keypath(&account)),
- xpub: our_xpub.to_string(),
- },
- KeyOriginInfo {
- root_fingerprint: None,
- keypath: None,
- xpub: cosigner_xpub.to_string(),
- },
- ],
+ keys,
};
let input_branch = if policy.contains("<10;11>") { 10 } else { 0 };
let input_path = path(&format!("{account}/{input_branch}/0"));
@@ -287,10 +292,7 @@ fn policy_spend(policy: &str, change_index: u32, sign_cosigner: bool) -> PolicyS
config,
account,
input_pubkey: simulator_xpub_at(&secp, &input_path).public_key,
- display_keys: vec![
- format!("This device: {}", screens::DEVICE_POLICY_XPUB),
- cosigner_xpub.to_string(),
- ],
+ display_keys,
}
}
@@ -570,6 +572,42 @@ fn policy_tr_unspendable_internal_key_complex() -> TestVector {
})
}
+fn policy_timestamp_locktime() -> TestVector {
+ const LOCKTIME: u32 = 1_800_000_000;
+
+ let policy = "wsh(and_v(v:pk(@0/**),after(1800000000)))";
+ let name = "test timestamp locktime";
+ let mut spend = policy_spend(policy, 0, false);
+ spend.psbt.unsigned_tx.lock_time = bitcoin::absolute::LockTime::from_consensus(LOCKTIME);
+ spend.psbt.unsigned_tx.input[0].sequence = Sequence(Sequence::MAX.0 - 1);
+ let locktime_screen = Screen::Confirm {
+ title: String::new(),
+ body: "Locktime (UTC):\n2027-01-15\n08:00:00".into(),
+ longtouch: false,
+ };
+ let mut vector = transaction_vector(
+ "policy-timestamp-locktime",
+ "Signs and finalizes a registered WSH policy whose absolute locktime is a Unix timestamp.",
+ Coin::Tbtc,
+ spend.psbt,
+ PsbtSignOptions {
+ force_script_config: Some(ScriptConfigWithKeypath {
+ script_config: spend.config.clone(),
+ keypath: keypath(&spend.account),
+ }),
+ ..Default::default()
+ },
+ vec![ecdsa_signature(0, spend.input_pubkey)],
+ screens::policy_timestamp_locktime(policy, name, &spend.display_keys, locktime_screen),
+ );
+ vector.registrations.push(Registration {
+ script_config: spend.config,
+ keypath: None,
+ name: name.into(),
+ });
+ vector
+}
+
pub fn all() -> Vec<TestVector> {
let multipath_policy = "wsh(multi(2,@0/<10;11>/*,@1/<20;21>/*))";
let standard_policy = "wsh(multi(2,@0/**,@1/**))";
@@ -604,6 +642,7 @@ pub fn all() -> Vec<TestVector> {
policy_tr_keyspend_with_script_tree(),
policy_tr_unspendable_internal_key(),
policy_tr_unspendable_internal_key_complex(),
+ policy_timestamp_locktime(),
policy_vector(
"policy-different-multipath-derivations",
"Signs and finalizes a registered WSH policy whose two keys use different receive and change branches.",
### src/rust/bitbox-test-vectors/src/btc_transaction/cases/screens.rs
@@ -24,6 +24,7 @@ const PSBT_OTHER_ACCOUNT_ADDRESS_GROUPED: &str =
// only earlier difference would be such an incomplete capture start at v9.20, so their screen lists
// cannot be mistaken for the screens shown by the device.
const COMPLETE_TRANSACTION_SCREEN_CAPTURE_VERSION: &str = "9.20.0";
+const LOCKTIME_CONFIRMATION_UPDATE_VERSION: &str = "9.27.0";
pub const DEVICE_POLICY_XPUB: &str = "[4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv";
fn success(
@@ -195,6 +196,42 @@ fn simple_expectations(
]
}
+fn simple_locktime_expectations(
+ previous_middle: &[Screen],
+ current_middle: &[Screen],
+ external_address: &str,
+ grouped_external_address: &str,
+ unit: &str,
+) -> Vec<VersionExpectation> {
+ let amount = format!("0.20000000 {unit}");
+ let total = format!("0.30000000 {unit}");
+ let fee_amount = format!("0.10000000 {unit}");
+ let suffix = |address_value: &str, middle: &[Screen]| {
+ let mut screens = vec![address(&amount, address_value)];
+ screens.extend_from_slice(middle);
+ screens.extend([warning_fee(&total, &fee_amount), high_fee(50), status()]);
+ screens
+ };
+
+ vec![
+ success(
+ Some(COMPLETE_TRANSACTION_SCREEN_CAPTURE_VERSION),
+ Some("9.26.0"),
+ suffix(external_address, previous_middle),
+ ),
+ success(
+ Some("9.26.0"),
+ Some(LOCKTIME_CONFIRMATION_UPDATE_VERSION),
+ suffix(grouped_external_address, previous_middle),
+ ),
+ success(
+ Some(LOCKTIME_CONFIRMATION_UPDATE_VERSION),
+ None,
+ suffix(grouped_external_address, current_middle),
+ ),
+ ]
+}
+
fn taproot_policy_expectations(prefix: Vec<Screen>) -> Vec<VersionExpectation> {
let suffix_920 = vec![
address("0.20000000 TBTC", TBTC_EXTERNAL_ADDRESS),
@@ -237,9 +274,26 @@ pub fn simple_tbtc(middle: &[Screen]) -> Vec<VersionExpectation> {
)
}
-pub fn simple_ltc(middle: &[Screen]) -> Vec<VersionExpectation> {
- simple_expectations(
- middle,
+pub fn simple_tbtc_locktime(
+ previous_middle: &[Screen],
+ current_middle: &[Screen],
+) -> Vec<VersionExpectation> {
+ simple_locktime_expectations(
+ previous_middle,
+ current_middle,
+ TBTC_EXTERNAL_ADDRESS,
+ TBTC_EXTERNAL_ADDRESS_GROUPED,
+ "TBTC",
+ )
+}
+
+pub fn simple_ltc_locktime(
+ previous_middle: &[Screen],
+ current_middle: &[Screen],
+) -> Vec<VersionExpectation> {
+ simple_locktime_expectations(
+ previous_middle,
+ current_middle,
LTC_EXTERNAL_ADDRESS,
LTC_EXTERNAL_ADDRESS_GROUPED,
"LTC",
@@ -566,6 +620,26 @@ pub fn policy(policy: &str, name: &str, keys: &[String], taproot: bool) -> Vec<V
}
}
+pub fn policy_timestamp_locktime(
+ policy: &str,
+ name: &str,
+ keys: &[String],
+ locktime_screen: Screen,
+) -> Vec<VersionExpectation> {
+ let mut screens = policy_prefix(policy, name, keys);
+ screens.extend([
+ address("0.20000000 TBTC", TBTC_EXTERNAL_ADDRESS_GROUPED),
+ locktime_screen,
+ warning_fee("0.30000000 TBTC", "0.10000000 TBTC"),
+ high_fee(50),
+ status(),
+ ]);
+ vec![
+ invalid_input_before(LOCKTIME_CONFIRMATION_UPDATE_VERSION),
+ success(Some(LOCKTIME_CONFIRMATION_UPDATE_VERSION), None, screens),
+ ]
+}
+
pub fn policy_wsh() -> Vec<VersionExpectation> {
policy(
"wsh(or_b(pk(@0/<0;1>/*),s:pk(@1/<0;1>/*)))",
### src/rust/bitbox-test-vectors/testdata/btc-transaction-test-vectors.json
@@ -2227,7 +2227,7 @@
},
{
"id": "locktime-zero",
- "description": "Suppresses the locktime confirmation when locktime is zero even if the sequence signals RBF.",
+ "description": "Suppresses the locktime confirmation when locktime is zero even if the sequence is non-final.",
"coin": "tbtc",
"psbt": {
"transaction": "70736274ff01007d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000fdffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000"
@@ -2302,8 +2302,8 @@
]
},
{
- "id": "locktime-non-rbf",
- "description": "Displays block locktime 10 and its non-rbf sequence semantics.",
+ "id": "locktime-sequence-fffffffe",
+ "description": "Displays block locktime 10 for transaction sequence 0xfffffffe.",
"coin": "tbtc",
"psbt": {
"transaction": "70736274ff01007d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000feffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef140a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000"
@@ -2347,6 +2347,7 @@
},
{
"min_version": "9.26.0",
+ "max_version_exclusive": "9.27.0",
"outcome": "success",
"screens": [
{
@@ -2378,6 +2379,40 @@
"body": "confirmed"
}
]
+ },
+ {
+ "min_version": "9.27.0",
+ "outcome": "success",
+ "screens": [
+ {
+ "type": "transaction_address",
+ "amount": "0.20000000 TBTC",
+ "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d"
+ },
+ {
+ "type": "confirm",
+ "title": "",
+ "body": "Locktime on block:\n10",
+ "longtouch": false
+ },
+ {
+ "type": "transaction_fee",
+ "amount": "0.30000000 TBTC",
+ "fee": "0.10000000 TBTC",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "High fee",
+ "body": "The fee is 50.0%\nthe send amount.\nProceed?",
+ "longtouch": true
+ },
+ {
+ "type": "status",
+ "title": "Transaction",
+ "body": "confirmed"
+ }
+ ]
}
],
"expected_signatures": [
@@ -2390,8 +2425,8 @@
]
},
{
- "id": "locktime-rbf",
- "description": "Displays block locktime 10 and its rbf sequence semantics.",
+ "id": "locktime-sequence-fffffffd",
+ "description": "Displays block locktime 10 for transaction sequence 0xfffffffd.",
"coin": "tbtc",
"psbt": {
"transaction": "70736274ff01007d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000fdffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef140a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000"
@@ -2435,6 +2470,7 @@
},
{
"min_version": "9.26.0",
+ "max_version_exclusive": "9.27.0",
"outcome": "success",
"screens": [
{
@@ -2466,6 +2502,40 @@
"body": "confirmed"
}
]
+ },
+ {
+ "min_version": "9.27.0",
+ "outcome": "success",
+ "screens": [
+ {
+ "type": "transaction_address",
+ "amount": "0.20000000 TBTC",
+ "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d"
+ },
+ {
+ "type": "confirm",
+ "title": "",
+ "body": "Locktime on block:\n10",
+ "longtouch": false
+ },
+ {
+ "type": "transaction_fee",
+ "amount": "0.30000000 TBTC",
+ "fee": "0.10000000 TBTC",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "High fee",
+ "body": "The fee is 50.0%\nthe send amount.\nProceed?",
+ "longtouch": true
+ },
+ {
+ "type": "status",
+ "title": "Transaction",
+ "body": "confirmed"
+ }
+ ]
}
],
"expected_signatures": [
@@ -2478,8 +2548,8 @@
]
},
{
- "id": "locktime-litecoin-non-rbf-sequence",
- "description": "Displays a Litecoin block locktime without Bitcoin-specific RBF wording.",
+ "id": "locktime-litecoin-sequence-fffffffe",
+ "description": "Displays a Litecoin block locktime for transaction sequence 0xfffffffe.",
"coin": "ltc",
"psbt": {
"transaction": "70736274ff01007102000000018660b164e26bff1622e4172600f4011bd824211a4ef4201d92c0b0fb290f9b730000000000feffffff02801d2c04000000001600146514c2779c7ee0f8e09ae4f2999c334e5254c70e002d310100000000160014751e76e8199196d454941c45d1b3a323f1433bd60a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f160000000001011f00e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f162206032780e5412e19ac486f2be57004bc34d0f9957468bfa2b0fa1c8e689819d97df2184c00739d5400008002000080000000800000000000000000002202039537a5fa4fa947baa7b215407914d5fcb3709c554ff5750bf0ba931b3cda24ae184c00739d54000080020000800000008001000000000000000000"
@@ -2523,6 +2593,7 @@
},
{
"min_version": "9.26.0",
+ "max_version_exclusive": "9.27.0",
"outcome": "success",
"screens": [
{
@@ -2554,6 +2625,40 @@
"body": "confirmed"
}
]
+ },
+ {
+ "min_version": "9.27.0",
+ "outcome": "success",
+ "screens": [
+ {
+ "type": "transaction_address",
+ "amount": "0.20000000 LTC",
+ "address": "ltc1 qw50 8d6q ejxt dg4y 5r3z arva ry0c 5xw7 kgmn 4n9"
+ },
+ {
+ "type": "confirm",
+ "title": "",
+ "body": "Locktime on block:\n10",
+ "longtouch": false
+ },
+ {
+ "type": "transaction_fee",
+ "amount": "0.30000000 LTC",
+ "fee": "0.10000000 LTC",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "High fee",
+ "body": "The fee is 50.0%\nthe send amount.\nProceed?",
+ "longtouch": true
+ },
+ {
+ "type": "status",
+ "title": "Transaction",
+ "body": "confirmed"
+ }
+ ]
}
],
"expected_signatures": [
@@ -2566,8 +2671,8 @@
]
},
{
- "id": "locktime-litecoin-rbf-sequence",
- "description": "Displays a Litecoin block locktime without Bitcoin-specific RBF wording.",
+ "id": "locktime-litecoin-sequence-fffffffd",
+ "description": "Displays a Litecoin block locktime for transaction sequence 0xfffffffd.",
"coin": "ltc",
"psbt": {
"transaction": "70736274ff01007102000000018660b164e26bff1622e4172600f4011bd824211a4ef4201d92c0b0fb290f9b730000000000fdffffff02801d2c04000000001600146514c2779c7ee0f8e09ae4f2999c334e5254c70e002d310100000000160014751e76e8199196d454941c45d1b3a323f1433bd60a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f160000000001011f00e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f162206032780e5412e19ac486f2be57004bc34d0f9957468bfa2b0fa1c8e689819d97df2184c00739d5400008002000080000000800000000000000000002202039537a5fa4fa947baa7b215407914d5fcb3709c554ff5750bf0ba931b3cda24ae184c00739d54000080020000800000008001000000000000000000"
@@ -2611,6 +2716,7 @@
},
{
"min_version": "9.26.0",
+ "max_version_exclusive": "9.27.0",
"outcome": "success",
"screens": [
{
@@ -2642,6 +2748,40 @@
"body": "confirmed"
}
]
+ },
+ {
+ "min_version": "9.27.0",
+ "outcome": "success",
+ "screens": [
+ {
+ "type": "transaction_address",
+ "amount": "0.20000000 LTC",
+ "address": "ltc1 qw50 8d6q ejxt dg4y 5r3z arva ry0c 5xw7 kgmn 4n9"
+ },
+ {
+ "type": "confirm",
+ "title": "",
+ "body": "Locktime on block:\n10",
+ "longtouch": false
+ },
+ {
+ "type": "transaction_fee",
+ "amount": "0.30000000 LTC",
+ "fee": "0.10000000 LTC",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "High fee",
+ "body": "The fee is 50.0%\nthe send amount.\nProceed?",
+ "longtouch": true
+ },
+ {
+ "type": "status",
+ "title": "Transaction",
+ "body": "confirmed"
+ }
+ ]
}
],
"expected_signatures": [
@@ -4150,6 +4290,126 @@
}
]
},
+ {
+ "id": "policy-timestamp-locktime",
+ "description": "Signs and finalizes a registered WSH policy whose absolute locktime is a Unix timestamp.",
+ "coin": "tbtc",
+ "psbt": {
+ "transaction": "70736274ff01008902000000014c02333380544b73235a544a4cd8e1f9342fb18d6be6d836b2d2af267dc012830000000000feffffff02801d2c0400000000220020924e71bdb1a95ebe4ef83a1f0ac8eff1d958dc339353f0dfe70069f5eeb6c205002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef1400d2496b0001005e02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f5050000000022002077f8b061c8bfd7bc8ee3aac678b20cbdfbc4aa0ac90339b54f38c99ca911208b0000000001012b00e1f5050000000022002077f8b061c8bfd7bc8ee3aac678b20cbdfbc4aa0ac90339b54f38c99ca911208b01052921033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2ad0400d2496bb12206033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b21c4c00739d3000008001000080000000800300008000000000000000000001012921028c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44aad0400d2496bb12202028c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a1c4c00739d3000008001000080000000800300008001000000000000000000",
+ "options": {
+ "force_script_config": {
+ "script_config": {
+ "type": "policy",
+ "policy": "wsh(and_v(v:pk(@0/**),after(1800000000)))",
+ "keys": [
+ {
+ "root_fingerprint": "4c00739d",
+ "keypath": "m/48'/1'/0'/3'",
+ "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA"
+ }
+ ]
+ },
+ "keypath": "m/48'/1'/0'/3'"
+ }
+ }
+ },
+ "expected_needs_prevtxs": true,
+ "expectations": [
+ {
+ "max_version_exclusive": "9.27.0",
+ "outcome": "invalid_input",
+ "screens": []
+ },
+ {
+ "min_version": "9.27.0",
+ "outcome": "success",
+ "screens": [
+ {
+ "type": "confirm",
+ "title": "Spend from",
+ "body": "BTC Testnet\npolicy with\n1 keys",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "Name",
+ "body": "test timestamp locktime",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "",
+ "body": "Show policy\ndetails?",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "Policy",
+ "body": "wsh(and_v(v:pk(@0/**),after(1800000000)))",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "Key 1/1",
+ "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv",
+ "longtouch": false
+ },
+ {
+ "type": "transaction_address",
+ "amount": "0.20000000 TBTC",
+ "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d"
+ },
+ {
+ "type": "confirm",
+ "title": "",
+ "body": "Locktime (UTC):\n2027-01-15\n08:00:00",
+ "longtouch": false
+ },
+ {
+ "type": "transaction_fee",
+ "amount": "0.30000000 TBTC",
+ "fee": "0.10000000 TBTC",
+ "longtouch": false
+ },
+ {
+ "type": "confirm",
+ "title": "High fee",
+ "body": "The fee is 50.0%\nthe send amount.\nProceed?",
+ "longtouch": true
+ },
+ {
+ "type": "status",
+ "title": "Transaction",
+ "body": "confirmed"
+ }
+ ]
+ }
+ ],
+ "registrations": [
+ {
+ "script_config": {
+ "type": "policy",
+ "policy": "wsh(and_v(v:pk(@0/**),after(1800000000)))",
+ "keys": [
+ {
+ "root_fingerprint": "4c00739d",
+ "keypath": "m/48'/1'/0'/3'",
+ "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA"
+ }
+ ]
+ },
+ "name": "test timestamp locktime"
+ }
+ ],
+ "expected_signatures": [
+ {
+ "input_index": 0,
+ "kind": "ecdsa",
+ "pubkey": "033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2",
+ "sighash": "all"
+ }
+ ]
+ },
{
"id": "policy-different-multipath-derivations",
"description": "Signs and finalizes a registered WSH policy whose two keys use different receive and change branches.",
### src/rust/bitbox02-rust/src/hww/api/bitcoin/params.rs
@@ -15,7 +15,6 @@ pub struct Params {
pub base58_version_p2sh: u8,
pub bech32_hrp: &'static str,
pub name: &'static str,
- pub rbf_support: bool,
pub taproot_support: bool,
}
@@ -34,7 +33,6 @@ const PARAMS_BTC: Params = Params {
base58_version_p2sh: 0x05, // starts with 3
bech32_hrp: "bc",
name: "Bitcoin",
- rbf_support: true,
taproot_support: true,
};
@@ -45,7 +43,6 @@ const PARAMS_TBTC: Params = Params {
base58_version_p2sh: 0xc4, // starts with 2
bech32_hrp: "tb",
name: "BTC Testnet",
- rbf_support: true,
taproot_support: true,
};
const PARAMS_RBTC: Params = Params {
@@ -55,7 +52,6 @@ const PARAMS_RBTC: Params = Params {
base58_version_p2sh: 0xc4, // starts with 2
bech32_hrp: "bcrt",
name: "BTC Regtest",
- rbf_support: true,
taproot_support: true,
};
@@ -66,7 +62,6 @@ const PARAMS_LTC: Params = Params {
base58_version_p2sh: 0x32, // starts with M
bech32_hrp: "ltc",
name: "Litecoin",
- rbf_support: false,
taproot_support: false,
};
@@ -77,7 +72,6 @@ const PARAMS_TLTC: Params = Params {
base58_version_p2sh: 0xc4, // starts with 2
bech32_hrp: "tltc",
name: "LTC Testnet",
- rbf_support: false,
taproot_support: false,
};
### src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
@@ -660,7 +660,31 @@ impl<'a> TryFrom<&'a ValidatedScriptConfigWithKeypath<'a>>
}
}
-/// Singing flow:
+/// Formats the transaction's consensus nLockTime for confirmation.
+///
+/// Timestamp locktimes are shown in UTC. Whether a locktime has passed is determined using chain
+/// state, which the device does not have, so only the exact signed value is displayed.
+fn format_locktime(locktime: u32) -> Result<String, Error> {
+ let locktime = bitcoin::absolute::LockTime::from_consensus(locktime);
+ if locktime.is_block_height() {
+ return Ok(format!(
+ "Locktime on block:\n{}",
+ locktime.to_consensus_u32()
+ ));
+ }
+
+ let tm = util::datetime::get_datetime(locktime.to_consensus_u32())
+ .map_err(|_| Error::InvalidInput)?;
+ Ok(format!(
+ "Locktime (UTC):\n{}\n{}:{}:{}",
+ tm.date(),
+ tm.hour(),
+ tm.minute(),
+ tm.second(),
+ ))
+}
+
+/// Signing flow:
///
/// init
/// for each input:
@@ -689,7 +713,7 @@ impl<'a> TryFrom<&'a ValidatedScriptConfigWithKeypath<'a>>
/// scripts), but we only skip streaming previous transactions if all inputs are taproot, for
/// simplicity.
///
-/// For each output, the recipient is confirmed. At the last output, the total out, fee, locktime/RBF
+/// For each output, the recipient is confirmed. At the last output, the total out, fee and locktime
/// are confirmed.
///
/// The inputs are signed in inputs_pass2.
@@ -715,10 +739,6 @@ async fn _process(
let coin_params = super::params::get(coin);
// Validate the format_unit.
let format_unit = FormatUnit::try_from(request.format_unit)?;
- // Currently we do not support time-based nlocktime
- if request.locktime >= 500000000 {
- return Err(Error::InvalidInput);
- }
// Currently only support version 1 or version 2 tx.
// Version 2: https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki
if request.version != 1 && request.version != 2 {
@@ -752,8 +772,6 @@ async fn _process(
let mut inputs_sum_pass1: u64 = 0;
let mut locktime_applies: bool = false;
- let mut rbf: bool = false;
-
let mut hasher_prevouts = Sha256::new();
let mut hasher_sequence = Sha256::new();
let mut hasher_amounts = Sha256::new();
@@ -783,9 +801,6 @@ async fn _process(
.get(tx_input.script_config_index as usize)
.ok_or(Error::InvalidInput)?;
validate_input(&tx_input, coin_params, script_config_account)?;
- if tx_input.sequence < 0xffffffff - 1 {
- rbf = true;
- }
if tx_input.sequence < 0xffffffff {
locktime_applies = true;
}
@@ -1127,31 +1142,13 @@ async fn _process(
.await?;
}
- // Verify locktime/rbf.
- // A locktime of 0 will also not be verified, as it's certainly in the past and can't do any
- // harm.
- //
- // This is not a security feature, the extra locktime/RBF user confirmation is skipped if the tx
- // is not rbf or has a locktime of 0.
+ // Confirm an effective non-zero nLockTime because it can defer when the signed transaction
+ // becomes valid. nLockTime is ignored if every input uses the final sequence number.
if request.locktime > 0 && locktime_applies {
- // The RBF nsequence bytes are often set in conjunction with a locktime,
- // so verify both simultaneously.
+ let body = format_locktime(request.locktime)?;
hal.ui()
.confirm(&ConfirmParams {
- body: &format!(
- "Locktime on block:\n{}\n{}",
- request.locktime,
- if coin_params.rbf_support {
- if rbf {
- "Transaction is RBF"
- } else {
- "Transaction is not RBF"
- }
- } else {
- // There is no RBF in Litecoin.
- ""
- }
- ),
+ body: &body,
accept_is_nextarrow: true,
..Default::default()
})
@@ -1387,6 +1384,26 @@ mod tests {
use pb::btc_payment_request_request::{Memo, memo};
use util::bip32::HARDENED;
+ #[test]
+ fn test_format_locktime() {
+ assert_eq!(
+ format_locktime(499_999_999).unwrap(),
+ "Locktime on block:\n499999999"
+ );
+ assert_eq!(
+ format_locktime(500_000_000).unwrap(),
+ "Locktime (UTC):\n1985-11-05\n00:53:20"
+ );
+ assert_eq!(
+ format_locktime(1_800_000_000).unwrap(),
+ "Locktime (UTC):\n2027-01-15\n08:00:00"
+ );
+ assert_eq!(
+ format_locktime(u32::MAX).unwrap(),
+ "Locktime (UTC):\n2106-02-07\n06:28:15"
+ );
+ }
+
fn extract_next(response: &Response) -> &pb::BtcSignNextResponse {
match response {
Response::BtcSignNext(next) => next,
@@ -2646,15 +2663,6 @@ mod tests {
);
}
}
- {
- // test invalid locktime
- let mut init_req_invalid = init_req_valid.clone();
- init_req_invalid.locktime = 500000000;
- assert_eq!(
- process(&mut TestingHal::new(), &init_req_invalid).await,
- Err(Error::InvalidInput)
- );
- }
{
// test invalid inputs
let mut init_req_invalid = init_req_valid.clone();Why this scored 32/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.