Skip to content

Commit 01eec40

Browse files
committed
feat(node): apply_block enforces BIP68 height-based sequence locks (post-CSV activation)
Enforce the height-based subset of BIP68 sequence locks in apply_block after transaction verification and coinbase-maturity checks, gated by CSV activation. Inputs with the disable flag set remain exempt, and pre-v2 transactions retain legacy semantics. Time-based BIP68 sequence locks remain deferred because the block tree does not yet expose prevout median-time-past lookup for the target height. Those inputs are warned and accepted rather than falsely rejected. No end-to-end import regression test was added: regtest CSV activates at height 432, while existing node import tests exercise heights 0-1, so constructing the scenario would require an impractical chain setup for this strand. Op: extend
1 parent b613c2f commit 01eec40

1 file changed

Lines changed: 65 additions & 0 deletions

File tree

crates/node/src/apply.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ use crate::state::ApplyError;
1717
/// Number of blocks after a coinbase that its outputs become spendable.
1818
/// Consensus rule since Bitcoin v0.3.1; universal across networks.
1919
const COINBASE_MATURITY: u32 = 100;
20+
/// BIP68 sequence-bit masks.
21+
const BIP68_DISABLE_FLAG: u32 = 0x8000_0000;
22+
const BIP68_TYPE_FLAG: u32 = 0x0040_0000;
23+
const BIP68_MASK: u32 = 0x0000_ffff;
2024

2125
/// Owned shared handle set needed by `apply_block` to perform a block apply.
2226
pub struct ApplyHandles {
@@ -103,6 +107,7 @@ pub fn apply_block(
103107
verify_block_transactions(handles, block, height, prev_tip_state.median_time_past)?;
104108

105109
check_coinbase_maturity(handles, block, height)?;
110+
check_bip68_sequence_locks(handles, block, height)?;
106111

107112
let changes = build_utxo_changes(block, height)?;
108113
handles
@@ -226,6 +231,66 @@ pub(crate) fn check_coinbase_maturity(
226231
Ok(())
227232
}
228233

234+
fn check_bip68_sequence_locks(
235+
handles: &ApplyHandles,
236+
block: &bitcoin::Block,
237+
height: u32,
238+
) -> core::result::Result<(), ApplyError> {
239+
use bitcoin::hashes::Hash as _;
240+
241+
if !handles.network.is_csv_active(height) {
242+
return Ok(());
243+
}
244+
245+
for tx in &block.txdata {
246+
if tx.is_coinbase() {
247+
continue;
248+
}
249+
if tx.version.0 < 2 {
250+
continue;
251+
}
252+
for tx_input in &tx.input {
253+
let sequence = tx_input.sequence.to_consensus_u32();
254+
if sequence & BIP68_DISABLE_FLAG != 0 {
255+
continue;
256+
}
257+
let is_time_based = sequence & BIP68_TYPE_FLAG != 0;
258+
if is_time_based {
259+
tracing::warn!(
260+
sequence = %format_args!("0x{sequence:08x}"),
261+
"BIP68 time-based sequence lock observed; enforcement deferred"
262+
);
263+
continue;
264+
}
265+
266+
let relative_blocks = sequence & BIP68_MASK;
267+
let prev_outpoint = OutPoint::new(
268+
bitcoin_rs_primitives::Hash256::from_le_bytes(
269+
tx_input.previous_output.txid.as_byte_array(),
270+
),
271+
tx_input.previous_output.vout,
272+
);
273+
let Some(entry) = handles.utxo.get_entry(&prev_outpoint) else {
274+
continue;
275+
};
276+
let earliest_height = entry.height.saturating_add(relative_blocks);
277+
if height < earliest_height {
278+
return Err(ApplyError::Consensus(
279+
bitcoin_rs_consensus::ConsensusError::Bip {
280+
bip: "BIP68",
281+
reason: format!(
282+
"input sequence height-based lock unmet: prevout at height {} + {} blocks > current {}",
283+
entry.height, relative_blocks, height
284+
),
285+
},
286+
));
287+
}
288+
}
289+
}
290+
291+
Ok(())
292+
}
293+
229294
fn check_bip30_and_bip34(
230295
handles: &ApplyHandles,
231296
block: &bitcoin::Block,

0 commit comments

Comments
 (0)