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