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    // if/else (not enum match) so spec-lock Z3 can model control flow with Signet.
78    if *network == crate::types::Network::Mainnet {
79        bip54_deployment_mainnet()
80    } else if *network == crate::types::Network::Testnet {
81        bip54_deployment_testnet()
82    } else {
83        bip54_deployment_regtest()
84    }
85}
86
87/// Computes the activation height for a BIP9 deployment from recent block headers.
88///
89/// * `headers` – Last N block headers (oldest first), typically the 2016 blocks before the
90///   block we are validating. Must be the period ending at `current_height - 1`.
91/// * `current_height` – Height of the block we are validating.
92/// * `current_time` – Network time (Unix timestamp) for start/timeout checks.
93/// * `deployment` – BIP9 deployment (bit, start_time, timeout).
94///
95/// Returns `Some(activation_height)` when the last `LOCK_IN_PERIOD` headers (the retarget
96/// window ending at `current_height - 1`) show ≥[`ACTIVATION_THRESHOLD`] signalling for
97/// `deployment.bit`. Then `activation_height = (period_index + 2) * 2016` where
98/// `period_index = (current_height - 1) / 2016` (BIP9: ACTIVE at start of period `period_index + 2`).
99///
100/// This does **not** mean rules are active at `current_height` yet; use
101/// `bip_validation::is_bip54_active_at(height, network, Some(activation_height))` for that.
102///
103/// When scanning the chain sequentially, merge multiple `Some(h)` values with `h.min(...)` so
104/// an earlier period’s lock-in (smaller activation height) is not overwritten by a later window’s
105/// larger computed height (see `merge_bip54_activation_candidate`).
106#[spec_locked("5.4.9", "ActivationHeightFromVersionBits")]
107pub fn activation_height_from_headers<H: AsRef<BlockHeader>>(
108    headers: &[H],
109    current_height: u64,
110    current_time: u64,
111    deployment: &Bip9Deployment,
112) -> Option<u64> {
113    if deployment.start_time >= deployment.timeout {
114        return None;
115    }
116    if current_time < deployment.start_time || current_time >= deployment.timeout {
117        return None;
118    }
119    if headers.len() < LOCK_IN_PERIOD as usize {
120        return None;
121    }
122
123    let mut count = 0u32;
124    for h in headers.iter().take(LOCK_IN_PERIOD as usize) {
125        let v = h.as_ref().version as u32;
126        if ((v >> deployment.bit) & 1) != 0 {
127            count += 1;
128        }
129    }
130    if count < ACTIVATION_THRESHOLD {
131        return None;
132    }
133
134    // Lock-in detected for the period ending at (current_height - 1).
135    // BIP9: ACTIVE for all blocks after the LOCKED_IN retarget period. So if period p
136    // had ≥95%, we are LOCKED_IN at start of period p+1 and ACTIVE at start of period p+2.
137    // period_index p = (current_height - 1) / 2016; activation = (p + 2) * 2016.
138    let period_end = current_height.saturating_sub(1);
139    let period_index = period_end / LOCK_IN_PERIOD as u64;
140    // For `current_height` near u64::MAX, `(period_index + 2) * 2016` can overflow u64.
141    let activation_height = (period_index + 2).checked_mul(LOCK_IN_PERIOD as u64)?;
142
143    Some(activation_height)
144}
145
146/// Combine a running BIP54/version-bits activation height with a new candidate from
147/// [`activation_height_from_headers`]. Keeps the **minimum** (earliest) height.
148#[inline]
149pub fn merge_bip54_activation_candidate(
150    previous: Option<u64>,
151    candidate: Option<u64>,
152) -> Option<u64> {
153    match (previous, candidate) {
154        (Some(a), Some(b)) => Some(a.min(b)),
155        (Some(a), None) => Some(a),
156        (None, Some(b)) => Some(b),
157        (None, None) => None,
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::types::BlockHeader;
165
166    fn header(version: i64) -> BlockHeader {
167        BlockHeader {
168            version,
169            prev_block_hash: [0u8; 32],
170            merkle_root: [0u8; 32],
171            timestamp: 0,
172            bits: 0x1d00ffff,
173            nonce: 0,
174        }
175    }
176
177    #[test]
178    fn disabled_deployment_returns_none() {
179        let dep = Bip9Deployment {
180            bit: 0,
181            start_time: 100,
182            timeout: 100,
183        };
184        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
185        assert!(activation_height_from_headers(&headers, 4032, 150, &dep).is_none());
186    }
187
188    #[test]
189    fn active_after_lockin() {
190        let dep = Bip9Deployment {
191            bit: 0,
192            start_time: 0,
193            timeout: u64::MAX,
194        };
195        // 2016 headers all with bit 0 set (period ending at current_height-1)
196        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
197        // Period 1 ends at 4031: lock-in → ACTIVE from height 6048 onward.
198        assert_eq!(
199            activation_height_from_headers(&headers, 4032, 1, &dep),
200            Some(6048)
201        );
202        // Period 2 window at H=6048: alone this implies activation 8064; IBD merges with min(6048, …).
203        assert_eq!(
204            activation_height_from_headers(&headers, 6048, 1, &dep),
205            Some(8064)
206        );
207        assert_eq!(
208            merge_bip54_activation_candidate(
209                activation_height_from_headers(&headers, 4032, 1, &dep),
210                activation_height_from_headers(&headers, 6048, 1, &dep),
211            ),
212            Some(6048)
213        );
214    }
215
216    #[test]
217    fn not_active_before_activation_height() {
218        let dep = Bip9Deployment {
219            bit: 0,
220            start_time: 0,
221            timeout: u64::MAX,
222        };
223        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
224        let act = activation_height_from_headers(&headers, 4031, 1, &dep);
225        assert_eq!(act, Some(6048));
226        assert!(
227            !crate::bip_validation::is_bip54_active_at(4031, crate::types::Network::Mainnet, act),
228            "override height must not activate BIP54 before that height"
229        );
230    }
231
232    #[test]
233    fn huge_current_height_does_not_panic() {
234        let dep = Bip9Deployment {
235            bit: 0,
236            start_time: 0,
237            timeout: u64::MAX,
238        };
239        let headers: Vec<BlockHeader> = (0..2016).map(|_| header(1)).collect();
240        assert!(activation_height_from_headers(&headers, u64::MAX, 1, &dep).is_none());
241    }
242}