Skip to main content

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 blvm_spec_lock::spec_locked;
7
8use crate::types::BlockHeader;
9
10/// BIP9 lock-in period (2016 blocks).
11pub const LOCK_IN_PERIOD: u32 = 2016;
12
13/// BIP9 activation threshold (95% of LOCK_IN_PERIOD).
14pub const ACTIVATION_THRESHOLD: u32 = 1916;
15
16/// BIP9 deployment parameters (bit index and time window).
17#[derive(Debug, Clone, Copy)]
18pub struct Bip9Deployment {
19    /// Version bit index (0–28).
20    pub bit: u8,
21    /// Start time (Unix timestamp). Before this, state is Defined.
22    pub start_time: u64,
23    /// Timeout (Unix timestamp). After this, state is Failed.
24    pub timeout: u64,
25}
26
27/// Returns the BIP54 deployment for mainnet when using version-bits activation.
28///
29/// Uses bit 15 and no time bounds so that once 95% of blocks in a 2016-block period
30/// signal the bit, BIP54 is considered active. If the network assigns a different bit
31/// or timeline, pass a custom `Bip9Deployment` to `activation_height_from_headers` instead.
32#[spec_locked("5.4.9", "Bip54DeploymentMainnet")]
33#[blvm_spec_lock::ensures(result.bit == 15)]
34pub fn bip54_deployment_mainnet() -> Bip9Deployment {
35    Bip9Deployment {
36        bit: 15,
37        start_time: 0,
38        timeout: u64::MAX,
39    }
40}
41
42/// Returns the BIP54 deployment for testnet3.
43///
44/// Testnet uses the same bit index but no timeout restriction so IBD can run
45/// on any version-bits signalling period without artificial expiry.
46#[spec_locked("5.4.9", "Bip54DeploymentTestnet")]
47#[blvm_spec_lock::ensures(result.bit == 15)]
48pub fn bip54_deployment_testnet() -> Bip9Deployment {
49    Bip9Deployment {
50        bit: 15,
51        start_time: 0,
52        timeout: u64::MAX,
53    }
54}
55
56/// Returns the BIP54 deployment for regtest.
57///
58/// Regtest activates immediately (bit 15, no time window) so tests using
59/// regtest blocks with the BIP54 bit set pass version-bits scanning.
60#[spec_locked("5.4.9", "Bip54DeploymentRegtest")]
61#[blvm_spec_lock::ensures(result.bit == 15)]
62pub fn bip54_deployment_regtest() -> Bip9Deployment {
63    Bip9Deployment {
64        bit: 15,
65        start_time: 0,
66        timeout: u64::MAX,
67    }
68}
69
70/// Returns the appropriate BIP54 deployment for `network`.
71///
72/// Prefer this over the network-specific variants so that parallel-IBD and block
73/// processing automatically use the right deployment table.
74#[spec_locked("5.4.9", "Bip54DeploymentForNetwork")]
75#[blvm_spec_lock::ensures(result.bit == 15)]
76pub fn bip54_deployment_for_network(network: &crate::types::Network) -> Bip9Deployment {
77    match network {
78        crate::types::Network::Mainnet => bip54_deployment_mainnet(),
79        crate::types::Network::Testnet => bip54_deployment_testnet(),
80        crate::types::Network::Regtest => bip54_deployment_regtest(),
81    }
82}
83
84/// Computes the activation height for a BIP9 deployment from recent block headers.
85///
86/// * `headers` – Last N block headers (oldest first), typically the 2016 blocks before the
87///   block we are validating. Must be the period ending at `current_height - 1`.
88/// * `current_height` – Height of the block we are validating.
89/// * `current_time` – Network time (Unix timestamp) for start/timeout checks.
90/// * `deployment` – BIP9 deployment (bit, start_time, timeout).
91///
92/// Returns `Some(activation_height)` when the last `LOCK_IN_PERIOD` headers (the retarget
93/// window ending at `current_height - 1`) show ≥[`ACTIVATION_THRESHOLD`] signalling for
94/// `deployment.bit`. Then `activation_height = (period_index + 2) * 2016` where
95/// `period_index = (current_height - 1) / 2016` (BIP9: ACTIVE at start of period `period_index + 2`).
96///
97/// This does **not** mean rules are active at `current_height` yet; use
98/// `bip_validation::is_bip54_active_at(height, network, Some(activation_height))` for that.
99///
100/// When scanning the chain sequentially, merge multiple `Some(h)` values with `h.min(...)` so
101/// an earlier period’s lock-in (smaller activation height) is not overwritten by a later window’s
102/// larger computed height (see `merge_bip54_activation_candidate`).
103pub fn activation_height_from_headers<H: AsRef<BlockHeader>>(
104    headers: &[H],
105    current_height: u64,
106    current_time: u64,
107    deployment: &Bip9Deployment,
108) -> Option<u64> {
109    if deployment.start_time >= deployment.timeout {
110        return None;
111    }
112    if current_time < deployment.start_time || current_time >= deployment.timeout {
113        return None;
114    }
115    if headers.len() < LOCK_IN_PERIOD as usize {
116        return None;
117    }
118
119    let mut count = 0u32;
120    for h in headers.iter().take(LOCK_IN_PERIOD as usize) {
121        let v = h.as_ref().version as u32;
122        if ((v >> deployment.bit) & 1) != 0 {
123            count += 1;
124        }
125    }
126    if count < ACTIVATION_THRESHOLD {
127        return None;
128    }
129
130    // Lock-in detected for the period ending at (current_height - 1).
131    // BIP9: ACTIVE for all blocks after the LOCKED_IN retarget period. So if period p
132    // had ≥95%, we are LOCKED_IN at start of period p+1 and ACTIVE at start of period p+2.
133    // period_index p = (current_height - 1) / 2016; activation = (p + 2) * 2016.
134    let period_end = current_height.saturating_sub(1);
135    let period_index = period_end / LOCK_IN_PERIOD as u64;
136    // For `current_height` near u64::MAX, `(period_index + 2) * 2016` can overflow u64.
137    let activation_height = (period_index + 2).checked_mul(LOCK_IN_PERIOD as u64)?;
138
139    Some(activation_height)
140}
141
142/// Combine a running BIP54/version-bits activation height with a new candidate from
143/// [`activation_height_from_headers`]. Keeps the **minimum** (earliest) height.
144#[inline]
145pub fn merge_bip54_activation_candidate(
146    previous: Option<u64>,
147    candidate: Option<u64>,
148) -> Option<u64> {
149    match (previous, candidate) {
150        (Some(a), Some(b)) => Some(a.min(b)),
151        (Some(a), None) => Some(a),
152        (None, Some(b)) => Some(b),
153        (None, None) => None,
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::types::BlockHeader;
161
162    fn header(version: i64) -> BlockHeader {
163        BlockHeader {
164            version,
165            prev_block_hash: [0u8; 32],
166            merkle_root: [0u8; 32],
167            timestamp: 0,
168            bits: 0x1d00ffff,
169            nonce: 0,
170        }
171    }
172
173    #[test]
174    fn disabled_deployment_returns_none() {
175        let dep = Bip9Deployment {
176            bit: 0,
177            start_time: 100,
178            timeout: 100,
179        };
180        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
181        assert!(activation_height_from_headers(&headers, 4032, 150, &dep).is_none());
182    }
183
184    #[test]
185    fn active_after_lockin() {
186        let dep = Bip9Deployment {
187            bit: 0,
188            start_time: 0,
189            timeout: u64::MAX,
190        };
191        // 2016 headers all with bit 0 set (period ending at current_height-1)
192        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
193        // Period 1 ends at 4031: lock-in → ACTIVE from height 6048 onward.
194        assert_eq!(
195            activation_height_from_headers(&headers, 4032, 1, &dep),
196            Some(6048)
197        );
198        // Period 2 window at H=6048: alone this implies activation 8064; IBD merges with min(6048, …).
199        assert_eq!(
200            activation_height_from_headers(&headers, 6048, 1, &dep),
201            Some(8064)
202        );
203        assert_eq!(
204            merge_bip54_activation_candidate(
205                activation_height_from_headers(&headers, 4032, 1, &dep),
206                activation_height_from_headers(&headers, 6048, 1, &dep),
207            ),
208            Some(6048)
209        );
210    }
211
212    #[test]
213    fn not_active_before_activation_height() {
214        let dep = Bip9Deployment {
215            bit: 0,
216            start_time: 0,
217            timeout: u64::MAX,
218        };
219        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
220        let act = activation_height_from_headers(&headers, 4031, 1, &dep);
221        assert_eq!(act, Some(6048));
222        assert!(
223            !crate::bip_validation::is_bip54_active_at(4031, crate::types::Network::Mainnet, act),
224            "override height must not activate BIP54 before that height"
225        );
226    }
227
228    #[test]
229    fn huge_current_height_does_not_panic() {
230        let dep = Bip9Deployment {
231            bit: 0,
232            start_time: 0,
233            timeout: u64::MAX,
234        };
235        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
236        assert!(activation_height_from_headers(&headers, u64::MAX, 1, &dep).is_none());
237    }
238}