use crate::bip113::get_median_time_past;
use crate::error::Result;
use crate::locktime::{
extract_sequence_locktime_value, extract_sequence_type_flag, is_sequence_disabled,
};
use crate::types::*;
use blvm_spec_lock::spec_locked;
#[allow(dead_code)]
const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 0x80000000;
#[allow(dead_code)]
const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 0x00400000;
#[allow(dead_code)]
const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000ffff;
const SEQUENCE_LOCKTIME_GRANULARITY: u32 = 9;
const LOCKTIME_VERIFY_SEQUENCE: u32 = 0x01;
#[spec_locked("5.5")]
pub fn calculate_sequence_locks(
tx: &Transaction,
flags: u32,
prev_heights: &[u64],
recent_headers: Option<&[BlockHeader]>,
) -> Result<(i64, i64)> {
if prev_heights.len() != tx.inputs.len() {
return Err(crate::error::ConsensusError::ConsensusRuleViolation(
format!(
"prev_heights length {} does not match input count {}",
prev_heights.len(),
tx.inputs.len()
)
.into(),
));
}
let mut min_height: i64 = -1;
let mut min_time: i64 = -1;
let enforce_bip68 = tx.version >= 2 && (flags & LOCKTIME_VERIFY_SEQUENCE) != 0;
if !enforce_bip68 {
return Ok((min_height, min_time));
}
for (i, input) in tx.inputs.iter().enumerate() {
if is_sequence_disabled(input.sequence as u32) {
continue;
}
let coin_height = i64::try_from(prev_heights[i]).map_err(|_| {
crate::error::ConsensusError::ConsensusRuleViolation(
"prev_height does not fit in i64 for sequence lock calculation".into(),
)
})?;
if extract_sequence_type_flag(input.sequence as u32) {
let coin_time = if let Some(headers) = recent_headers {
i64::try_from(get_median_time_past(headers)).map_err(|_| {
crate::error::ConsensusError::ConsensusRuleViolation(
"median time-past does not fit in i64 for sequence lock calculation".into(),
)
})?
} else {
continue;
};
let locktime_value = extract_sequence_locktime_value(input.sequence as u32) as i64;
debug_assert!(
locktime_value >= 0,
"Locktime value ({locktime_value}) must be non-negative"
);
let locktime_seconds = locktime_value << SEQUENCE_LOCKTIME_GRANULARITY;
debug_assert!(
locktime_seconds >= 0,
"Locktime seconds ({locktime_seconds}) must be non-negative (locktime_value: {locktime_value}, granularity: {SEQUENCE_LOCKTIME_GRANULARITY})"
);
let required_time = coin_time
.checked_add(locktime_seconds)
.and_then(|sum| sum.checked_sub(1))
.ok_or_else(|| {
crate::error::ConsensusError::ConsensusRuleViolation(
"Sequence lock time calculation overflow".into(),
)
})?;
debug_assert!(
required_time >= coin_time.saturating_sub(1),
"Required time ({required_time}) must be >= coin_time - 1 ({coin_time})"
);
min_time = min_time.max(required_time);
} else {
let locktime_value = extract_sequence_locktime_value(input.sequence as u32) as i64;
debug_assert!(
locktime_value >= 0,
"Locktime value ({locktime_value}) must be non-negative"
);
let required_height = coin_height
.checked_add(locktime_value)
.and_then(|sum| sum.checked_sub(1))
.ok_or_else(|| {
crate::error::ConsensusError::ConsensusRuleViolation(
"Sequence lock height calculation overflow".into(),
)
})?;
debug_assert!(
required_height >= coin_height.saturating_sub(1),
"Required height ({required_height}) must be >= coin_height - 1 ({coin_height})"
);
min_height = min_height.max(required_height);
}
}
Ok((min_height, min_time))
}
#[spec_locked("5.5")]
pub fn evaluate_sequence_locks(block_height: u64, block_time: u64, lock_pair: (i64, i64)) -> bool {
let (min_height, min_time) = lock_pair;
if min_height >= 0 && block_height <= min_height as u64 {
debug_assert!(
min_height >= 0,
"min_height ({min_height}) must be non-negative for cast to u64"
);
return false;
}
if min_time >= 0 && block_time <= min_time as u64 {
debug_assert!(
min_time >= 0,
"min_time ({min_time}) must be non-negative for cast to u64"
);
return false;
}
true
}
#[spec_locked("5.5")]
pub fn sequence_locks(
tx: &Transaction,
flags: u32,
prev_heights: &[u64],
block_height: u64,
block_time: u64,
recent_headers: Option<&[BlockHeader]>,
) -> Result<bool> {
let lock_pair = calculate_sequence_locks(tx, flags, prev_heights, recent_headers)?;
Ok(evaluate_sequence_locks(block_height, block_time, lock_pair))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_sequence_locks_disabled() {
let tx = Transaction {
version: 2,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: SEQUENCE_LOCKTIME_DISABLE_FLAG as u64, }]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
let prev_heights = vec![100];
let result =
calculate_sequence_locks(&tx, LOCKTIME_VERIFY_SEQUENCE, &prev_heights, None).unwrap();
assert_eq!(result, (-1, -1));
}
#[test]
fn test_calculate_sequence_locks_block_based() {
let tx = Transaction {
version: 2,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 100, }]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
let prev_heights = vec![1000]; let result =
calculate_sequence_locks(&tx, LOCKTIME_VERIFY_SEQUENCE, &prev_heights, None).unwrap();
assert_eq!(result.0, 1099);
assert_eq!(result.1, -1); }
#[test]
fn test_prev_height_overflowing_i64_returns_err() {
let tx = Transaction {
version: 2,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 10,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
let prev_heights = vec![(i64::MAX as u64).saturating_add(1)];
let err = calculate_sequence_locks(&tx, LOCKTIME_VERIFY_SEQUENCE, &prev_heights, None)
.unwrap_err();
assert!(matches!(
err,
crate::error::ConsensusError::ConsensusRuleViolation(_)
));
}
#[test]
fn test_evaluate_sequence_locks() {
let lock_pair = (1099, -1);
assert!(evaluate_sequence_locks(1100, 0, lock_pair));
assert!(!evaluate_sequence_locks(1099, 0, lock_pair));
assert!(!evaluate_sequence_locks(1098, 0, lock_pair));
}
}