Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,11 @@ impl Wallet {
// If they didn't tell us the current height, we assume it's the latest sync height.
None => {
let tip_height = self.chain.tip().height();
absolute::LockTime::from_height(tip_height).expect("invalid height")
// If the local chain tip height is not a valid block height for a locktime,
// that's an invariant violation in `LocalChain` (block heights are
// always below the locktime threshold) and should be addressed upstream.
absolute::LockTime::from_height(tip_height)
.expect("LocalChain tip height should be a valid block height")
}
Comment on lines +1308 to 1313

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invariant described here is not enforced by LocalChain.

CheckPoint::new accepts a BlockId with any u32 height, CheckPoint::push only checks ordering, and LocalChain::from_changeset/apply_update do not check the locktime threshold.

The update below succeeds, after which transaction creation reaches this expect:

let tip = wallet.latest_checkpoint().push(BlockId {
    height: 500_000_000,
    hash: BlockHash::all_zeros(),
}).unwrap();

wallet.apply_update(Update {
    chain: Some(tip),
    ..Default::default()
}).unwrap();

let mut builder = wallet.build_tx();
builder.add_recipient(script, amount);
let _ = builder.finish();

I reproduced the same panic after taking the staged ChangeSet and loading it with Wallet::load().load_wallet_no_persist(...).

I'd keep absolute::Height for the explicit setter and make the fallback conversion return CreateTxError::InvalidCurrentHeight instead of relying on this invariant.

Please also add regression tests for the update and reload paths. The current tests only wrap existing valid constants in Height::from_consensus(...).unwrap() and do not exercise the audit finding.

Some(h) => h,
};
Expand Down
9 changes: 6 additions & 3 deletions src/wallet/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,12 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
/// them using [`TxBuilder::add_utxos`].
///
/// In both cases, if you don't provide a current height, we use the last sync height.
pub fn current_height(&mut self, height: u32) -> &mut Self {
self.params.current_height =
Some(absolute::LockTime::from_height(height).expect("Invalid height"));
///
/// This method takes an [`absolute::Height`], which is guaranteed by the type system to
/// be a valid block height for a locktime (below 500_000_000). Callers can construct one
/// with [`absolute::Height::from_consensus`].
pub fn current_height(&mut self, height: absolute::Height) -> &mut Self {
self.params.current_height = Some(absolute::LockTime::from(height));
self
}

Expand Down
8 changes: 4 additions & 4 deletions tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ fn test_create_tx_custom_locktime() {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000))
.current_height(630_001)
.current_height(absolute::Height::from_consensus(630_001).unwrap())
.nlocktime(absolute::LockTime::from_height(630_000).unwrap());
let psbt = builder.finish().unwrap();

Expand Down Expand Up @@ -2537,7 +2537,7 @@ fn test_spend_coinbase() {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance.immature / 2)
.current_height(confirmation_height);
.current_height(absolute::Height::from_consensus(confirmation_height).unwrap());
assert!(matches!(
builder.finish(),
Err(CreateTxError::CoinSelection(
Expand All @@ -2552,7 +2552,7 @@ fn test_spend_coinbase() {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance.immature / 2)
.current_height(not_yet_mature_time);
.current_height(absolute::Height::from_consensus(not_yet_mature_time).unwrap());
assert_matches!(
builder.finish(),
Err(CreateTxError::CoinSelection(
Expand Down Expand Up @@ -2583,7 +2583,7 @@ fn test_spend_coinbase() {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance.confirmed / 2)
.current_height(maturity_time);
.current_height(absolute::Height::from_consensus(maturity_time).unwrap());
builder.finish().unwrap();
}

Expand Down