Skip to main content

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