blvm_consensus/version_bits.rs
1//! BIP9-style version bits activation.
2//!
3//! Computes soft-fork activation height from block header version bits so the node
4//! can enforce a fork when miners signal (e.g. BIP54) without a fixed activation height.
5
6use crate::types::BlockHeader;
7
8/// BIP9 lock-in period (2016 blocks).
9pub const LOCK_IN_PERIOD: u32 = 2016;
10
11/// BIP9 activation threshold (95% of LOCK_IN_PERIOD).
12pub const ACTIVATION_THRESHOLD: u32 = 1916;
13
14/// BIP9 deployment parameters (bit index and time window).
15#[derive(Debug, Clone, Copy)]
16pub struct Bip9Deployment {
17 /// Version bit index (0–28).
18 pub bit: u8,
19 /// Start time (Unix timestamp). Before this, state is Defined.
20 pub start_time: u64,
21 /// Timeout (Unix timestamp). After this, state is Failed.
22 pub timeout: u64,
23}
24
25/// Returns the BIP54 deployment for mainnet when using version-bits activation.
26///
27/// Uses bit 15 and no time bounds so that once 95% of blocks in a 2016-block period
28/// signal the bit, BIP54 is considered active. If the network assigns a different bit
29/// or timeline, pass a custom `Bip9Deployment` to `activation_height_from_headers` instead.
30pub fn bip54_deployment_mainnet() -> Bip9Deployment {
31 Bip9Deployment {
32 bit: 15,
33 start_time: 0,
34 timeout: u64::MAX,
35 }
36}
37
38/// Computes the activation height for a BIP9 deployment from recent block headers.
39///
40/// * `headers` – Last N block headers (oldest first), typically the 2016 blocks before the
41/// block we are validating. Must be the period ending at `current_height - 1`.
42/// * `current_height` – Height of the block we are validating.
43/// * `current_time` – Network time (Unix timestamp) for start/timeout checks.
44/// * `deployment` – BIP9 deployment (bit, start_time, timeout).
45///
46/// Returns `Some(activation_height)` when the last `LOCK_IN_PERIOD` headers (the retarget
47/// window ending at `current_height - 1`) show ≥[`ACTIVATION_THRESHOLD`] signalling for
48/// `deployment.bit`. Then `activation_height = (period_index + 2) * 2016` where
49/// `period_index = (current_height - 1) / 2016` (BIP9: ACTIVE at start of period `period_index + 2`).
50///
51/// This does **not** mean rules are active at `current_height` yet; use
52/// `bip_validation::is_bip54_active_at(height, network, Some(activation_height))` for that.
53///
54/// When scanning the chain sequentially, merge multiple `Some(h)` values with `h.min(...)` so
55/// an earlier period’s lock-in (smaller activation height) is not overwritten by a later window’s
56/// larger computed height (see `merge_bip54_activation_candidate`).
57pub fn activation_height_from_headers<H: AsRef<BlockHeader>>(
58 headers: &[H],
59 current_height: u64,
60 current_time: u64,
61 deployment: &Bip9Deployment,
62) -> Option<u64> {
63 if deployment.start_time >= deployment.timeout {
64 return None;
65 }
66 if current_time < deployment.start_time || current_time >= deployment.timeout {
67 return None;
68 }
69 if headers.len() < LOCK_IN_PERIOD as usize {
70 return None;
71 }
72
73 let mut count = 0u32;
74 for h in headers.iter().take(LOCK_IN_PERIOD as usize) {
75 let v = h.as_ref().version as u32;
76 if ((v >> deployment.bit) & 1) != 0 {
77 count += 1;
78 }
79 }
80 if count < ACTIVATION_THRESHOLD {
81 return None;
82 }
83
84 // Lock-in detected for the period ending at (current_height - 1).
85 // BIP9: ACTIVE for all blocks after the LOCKED_IN retarget period. So if period p
86 // had ≥95%, we are LOCKED_IN at start of period p+1 and ACTIVE at start of period p+2.
87 // period_index p = (current_height - 1) / 2016; activation = (p + 2) * 2016.
88 let period_end = current_height.saturating_sub(1);
89 let period_index = period_end / LOCK_IN_PERIOD as u64;
90 let activation_height = (period_index + 2) * LOCK_IN_PERIOD as u64;
91
92 Some(activation_height)
93}
94
95/// Combine a running BIP54/version-bits activation height with a new candidate from
96/// [`activation_height_from_headers`]. Keeps the **minimum** (earliest) height.
97#[inline]
98pub fn merge_bip54_activation_candidate(
99 previous: Option<u64>,
100 candidate: Option<u64>,
101) -> Option<u64> {
102 match (previous, candidate) {
103 (Some(a), Some(b)) => Some(a.min(b)),
104 (Some(a), None) => Some(a),
105 (None, Some(b)) => Some(b),
106 (None, None) => None,
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::types::BlockHeader;
114
115 fn header(version: i64) -> BlockHeader {
116 BlockHeader {
117 version,
118 prev_block_hash: [0u8; 32],
119 merkle_root: [0u8; 32],
120 timestamp: 0,
121 bits: 0x1d00ffff,
122 nonce: 0,
123 }
124 }
125
126 #[test]
127 fn disabled_deployment_returns_none() {
128 let dep = Bip9Deployment {
129 bit: 0,
130 start_time: 100,
131 timeout: 100,
132 };
133 let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
134 assert!(activation_height_from_headers(&headers, 4032, 150, &dep).is_none());
135 }
136
137 #[test]
138 fn active_after_lockin() {
139 let dep = Bip9Deployment {
140 bit: 0,
141 start_time: 0,
142 timeout: u64::MAX,
143 };
144 // 2016 headers all with bit 0 set (period ending at current_height-1)
145 let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
146 // Period 1 ends at 4031: lock-in → ACTIVE from height 6048 onward.
147 assert_eq!(
148 activation_height_from_headers(&headers, 4032, 1, &dep),
149 Some(6048)
150 );
151 // Period 2 window at H=6048: alone this implies activation 8064; IBD merges with min(6048, …).
152 assert_eq!(
153 activation_height_from_headers(&headers, 6048, 1, &dep),
154 Some(8064)
155 );
156 assert_eq!(
157 merge_bip54_activation_candidate(
158 activation_height_from_headers(&headers, 4032, 1, &dep),
159 activation_height_from_headers(&headers, 6048, 1, &dep),
160 ),
161 Some(6048)
162 );
163 }
164
165 #[test]
166 fn not_active_before_activation_height() {
167 let dep = Bip9Deployment {
168 bit: 0,
169 start_time: 0,
170 timeout: u64::MAX,
171 };
172 let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
173 let act = activation_height_from_headers(&headers, 4031, 1, &dep);
174 assert_eq!(act, Some(6048));
175 assert!(
176 !crate::bip_validation::is_bip54_active_at(4031, crate::types::Network::Mainnet, act),
177 "override height must not activate BIP54 before that height"
178 );
179 }
180}