blvm_consensus/sequence_locks.rs
1//! Sequence lock calculation functions (BIP68)
2//!
3//! Implements consensus's sequence lock calculation for relative locktime.
4//! Sequence locks are used to enforce relative locktime constraints using
5//! transaction input sequence numbers.
6//!
7//! Reference: consensus `tx_verify.cpp` CalculateSequenceLocks and EvaluateSequenceLocks
8
9use crate::bip113::get_median_time_past;
10use crate::error::Result;
11use crate::locktime::{
12 extract_sequence_locktime_value, extract_sequence_type_flag, is_sequence_disabled,
13};
14use crate::types::*;
15use blvm_spec_lock::spec_locked;
16
17/// Sequence locktime disable flag (bit 31)
18/// When set, the sequence number is not treated as a relative locktime
19#[allow(dead_code)]
20const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 0x80000000;
21
22/// Sequence locktime type flag (bit 22)
23/// When set, locktime is time-based; otherwise block-based
24#[allow(dead_code)]
25const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 0x00400000;
26
27/// Sequence locktime mask (bits 0-15)
28/// Extracts the locktime value from sequence number
29#[allow(dead_code)]
30const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000ffff;
31
32/// Sequence locktime granularity (for time-based locks)
33/// Time-based locks are measured in 512-second intervals
34const SEQUENCE_LOCKTIME_GRANULARITY: u32 = 9; // 2^9 = 512 seconds
35
36/// Locktime verify sequence flag
37/// Must be set to enable BIP68 sequence lock enforcement
38const LOCKTIME_VERIFY_SEQUENCE: u32 = 0x01;
39
40/// Calculate sequence locks for a transaction (BIP68)
41///
42/// Computes the minimum block height and time that must be reached
43/// before the transaction can be included in a block.
44///
45/// Matches consensus's CalculateSequenceLocks() exactly.
46///
47/// # Arguments
48/// * `tx` - Transaction to calculate locks for
49/// * `flags` - Script verification flags (must include LOCKTIME_VERIFY_SEQUENCE)
50/// * `prev_heights` - Block heights at which each input confirmed
51/// * `recent_headers` - Recent block headers for median time-past calculation
52///
53/// # Returns
54/// Pair (min_height, min_time) that must be satisfied:
55/// - min_height: Minimum block height (or -1 if no height constraint)
56/// - min_time: Minimum block time (or -1 if no time constraint)
57#[spec_locked("5.5")]
58pub fn calculate_sequence_locks(
59 tx: &Transaction,
60 flags: u32,
61 prev_heights: &[u64],
62 recent_headers: Option<&[BlockHeader]>,
63) -> Result<(i64, i64)> {
64 // Ensure prev_heights matches input count
65 if prev_heights.len() != tx.inputs.len() {
66 return Err(crate::error::ConsensusError::ConsensusRuleViolation(
67 format!(
68 "prev_heights length {} does not match input count {}",
69 prev_heights.len(),
70 tx.inputs.len()
71 )
72 .into(),
73 ));
74 }
75
76 // Initialize to -1 (no constraint)
77 let mut min_height: i64 = -1;
78 let mut min_time: i64 = -1;
79
80 // BIP68 is only enforced for version 2+ transactions and when flag is set
81 let enforce_bip68 = tx.version >= 2 && (flags & LOCKTIME_VERIFY_SEQUENCE) != 0;
82
83 if !enforce_bip68 {
84 return Ok((min_height, min_time));
85 }
86
87 // Process each input
88 for (i, input) in tx.inputs.iter().enumerate() {
89 // Check if sequence is disabled (bit 31 set)
90 if is_sequence_disabled(input.sequence as u32) {
91 // This input doesn't contribute to sequence locks
92 continue;
93 }
94
95 // Must not use `as i64`: large u64 values wrap to negative i64 and break invariants.
96 let coin_height = i64::try_from(prev_heights[i]).map_err(|_| {
97 crate::error::ConsensusError::ConsensusRuleViolation(
98 "prev_height does not fit in i64 for sequence lock calculation".into(),
99 )
100 })?;
101
102 // Check locktime type (bit 22)
103 if extract_sequence_type_flag(input.sequence as u32) {
104 // Time-based relative locktime
105 // Need median time-past of the block prior to the coin's block
106 let coin_time = if let Some(headers) = recent_headers {
107 // Calculate median time-past for the block prior to coin_height
108 // For simplicity, we'll use the most recent header's median time-past
109 // In a full implementation, we'd need to look up the actual block
110 i64::try_from(get_median_time_past(headers)).map_err(|_| {
111 crate::error::ConsensusError::ConsensusRuleViolation(
112 "median time-past does not fit in i64 for sequence lock calculation".into(),
113 )
114 })?
115 } else {
116 // No headers available - can't calculate time-based lock
117 // This is acceptable for some contexts (e.g., mempool validation)
118 continue;
119 };
120
121 // Extract locktime value and multiply by granularity (512 seconds)
122 let locktime_value = extract_sequence_locktime_value(input.sequence as u32) as i64;
123
124 // Runtime assertion: Locktime value must be non-negative
125 debug_assert!(
126 locktime_value >= 0,
127 "Locktime value ({locktime_value}) must be non-negative"
128 );
129
130 let locktime_seconds = locktime_value << SEQUENCE_LOCKTIME_GRANULARITY;
131
132 // Runtime assertion: Shift operation must not overflow
133 // locktime_value is u16 (max 65535), so locktime_seconds = 65535 * 512 = 33,553,920
134 // This fits in i64, so no overflow possible
135 debug_assert!(
136 locktime_seconds >= 0,
137 "Locktime seconds ({locktime_seconds}) must be non-negative (locktime_value: {locktime_value}, granularity: {SEQUENCE_LOCKTIME_GRANULARITY})"
138 );
139
140 // Calculate minimum time: coin_time + locktime_seconds - 1
141 // The -1 is to maintain nLockTime semantics (last invalid time)
142 let required_time = coin_time
143 .checked_add(locktime_seconds)
144 .and_then(|sum| sum.checked_sub(1))
145 .ok_or_else(|| {
146 crate::error::ConsensusError::ConsensusRuleViolation(
147 "Sequence lock time calculation overflow".into(),
148 )
149 })?;
150
151 // Runtime assertion: Required time must be >= coin_time (or close to it after -1)
152 debug_assert!(
153 required_time >= coin_time.saturating_sub(1),
154 "Required time ({required_time}) must be >= coin_time - 1 ({coin_time})"
155 );
156
157 min_time = min_time.max(required_time);
158 } else {
159 // Block-based relative locktime
160 // Extract locktime value (number of blocks)
161 let locktime_value = extract_sequence_locktime_value(input.sequence as u32) as i64;
162
163 // Runtime assertion: Locktime value must be non-negative
164 debug_assert!(
165 locktime_value >= 0,
166 "Locktime value ({locktime_value}) must be non-negative"
167 );
168
169 // Calculate minimum height: coin_height + locktime_value - 1
170 // The -1 is to maintain nLockTime semantics (last invalid height)
171 let required_height = coin_height
172 .checked_add(locktime_value)
173 .and_then(|sum| sum.checked_sub(1))
174 .ok_or_else(|| {
175 crate::error::ConsensusError::ConsensusRuleViolation(
176 "Sequence lock height calculation overflow".into(),
177 )
178 })?;
179
180 // Runtime assertion: Required height must be >= coin_height - 1
181 debug_assert!(
182 required_height >= coin_height.saturating_sub(1),
183 "Required height ({required_height}) must be >= coin_height - 1 ({coin_height})"
184 );
185
186 min_height = min_height.max(required_height);
187 }
188 }
189
190 Ok((min_height, min_time))
191}
192
193/// Evaluate if sequence locks are satisfied
194///
195/// Checks if the current block height and time satisfy the sequence lock constraints.
196///
197/// Matches consensus's EvaluateSequenceLocks() exactly.
198///
199/// # Arguments
200/// * `block_height` - Current block height
201/// * `block_time` - Current block's median time-past
202/// * `lock_pair` - (min_height, min_time) from calculate_sequence_locks
203///
204/// # Returns
205/// true if locks are satisfied, false otherwise
206#[spec_locked("5.5")]
207pub fn evaluate_sequence_locks(block_height: u64, block_time: u64, lock_pair: (i64, i64)) -> bool {
208 let (min_height, min_time) = lock_pair;
209
210 // u64 is always non-negative - no assertion needed
211
212 // Check height constraint
213 if min_height >= 0 && block_height <= min_height as u64 {
214 // Runtime assertion: Cast must be valid (min_height >= 0)
215 debug_assert!(
216 min_height >= 0,
217 "min_height ({min_height}) must be non-negative for cast to u64"
218 );
219 return false;
220 }
221
222 // Check time constraint
223 if min_time >= 0 && block_time <= min_time as u64 {
224 // Runtime assertion: Cast must be valid (min_time >= 0)
225 debug_assert!(
226 min_time >= 0,
227 "min_time ({min_time}) must be non-negative for cast to u64"
228 );
229 return false;
230 }
231
232 true
233}
234
235/// Check if transaction sequence locks are satisfied
236///
237/// Convenience function that combines CalculateSequenceLocks and EvaluateSequenceLocks.
238///
239/// # Arguments
240/// * `tx` - Transaction to check
241/// * `flags` - Script verification flags
242/// * `prev_heights` - Block heights at which each input confirmed
243/// * `block_height` - Current block height
244/// * `block_time` - Current block's median time-past
245/// * `recent_headers` - Recent headers for median time-past calculation
246///
247/// # Returns
248/// true if sequence locks are satisfied, false otherwise
249#[spec_locked("5.5")]
250pub fn sequence_locks(
251 tx: &Transaction,
252 flags: u32,
253 prev_heights: &[u64],
254 block_height: u64,
255 block_time: u64,
256 recent_headers: Option<&[BlockHeader]>,
257) -> Result<bool> {
258 let lock_pair = calculate_sequence_locks(tx, flags, prev_heights, recent_headers)?;
259 Ok(evaluate_sequence_locks(block_height, block_time, lock_pair))
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_calculate_sequence_locks_disabled() {
268 let tx = Transaction {
269 version: 2,
270 inputs: vec![TransactionInput {
271 prevout: OutPoint {
272 hash: [0; 32].into(),
273 index: 0,
274 },
275 script_sig: vec![],
276 sequence: SEQUENCE_LOCKTIME_DISABLE_FLAG as u64, // Disabled
277 }]
278 .into(),
279 outputs: vec![].into(),
280 lock_time: 0,
281 };
282
283 let prev_heights = vec![100];
284 let result =
285 calculate_sequence_locks(&tx, LOCKTIME_VERIFY_SEQUENCE, &prev_heights, None).unwrap();
286
287 // Disabled sequence should not create locks
288 assert_eq!(result, (-1, -1));
289 }
290
291 #[test]
292 fn test_calculate_sequence_locks_block_based() {
293 let tx = Transaction {
294 version: 2,
295 inputs: vec![TransactionInput {
296 prevout: OutPoint {
297 hash: [0; 32].into(),
298 index: 0,
299 },
300 script_sig: vec![],
301 sequence: 100, // 100 blocks relative locktime
302 }]
303 .into(),
304 outputs: vec![].into(),
305 lock_time: 0,
306 };
307
308 let prev_heights = vec![1000]; // Input confirmed at height 1000
309 let result =
310 calculate_sequence_locks(&tx, LOCKTIME_VERIFY_SEQUENCE, &prev_heights, None).unwrap();
311
312 // Should require height 1000 + 100 - 1 = 1099
313 assert_eq!(result.0, 1099);
314 assert_eq!(result.1, -1); // No time constraint
315 }
316
317 #[test]
318 fn test_prev_height_overflowing_i64_returns_err() {
319 let tx = Transaction {
320 version: 2,
321 inputs: vec![TransactionInput {
322 prevout: OutPoint {
323 hash: [0; 32].into(),
324 index: 0,
325 },
326 script_sig: vec![],
327 sequence: 10,
328 }]
329 .into(),
330 outputs: vec![].into(),
331 lock_time: 0,
332 };
333 let prev_heights = vec![(i64::MAX as u64).saturating_add(1)];
334 let err = calculate_sequence_locks(&tx, LOCKTIME_VERIFY_SEQUENCE, &prev_heights, None)
335 .unwrap_err();
336 assert!(matches!(
337 err,
338 crate::error::ConsensusError::ConsensusRuleViolation(_)
339 ));
340 }
341
342 #[test]
343 fn test_evaluate_sequence_locks() {
344 // Lock requires height 1099
345 let lock_pair = (1099, -1);
346
347 // Block height 1100 satisfies the lock
348 assert!(evaluate_sequence_locks(1100, 0, lock_pair));
349
350 // Block height 1099 does not satisfy (must be > 1099)
351 assert!(!evaluate_sequence_locks(1099, 0, lock_pair));
352
353 // Block height 1098 does not satisfy
354 assert!(!evaluate_sequence_locks(1098, 0, lock_pair));
355 }
356}