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