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")]
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}