Skip to main content

blvm_protocol/
features.rs

1//! Feature Activation Tracking
2//!
3//! Tracks when protocol features activate by block height or timestamp.
4//! This allows the protocol engine to determine if features are active
5//! at a specific block height, not just whether they're supported.
6
7use crate::ProtocolVersion;
8use serde::{Deserialize, Serialize};
9
10/// Feature activation method
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ActivationMethod {
13    /// BIP9 version bits activation
14    BIP9,
15    /// Height-based activation (e.g., BIP34 blocks version)
16    HeightBased,
17    /// Timestamp-based activation
18    Timestamp,
19    /// Hard fork - immediate activation at genesis
20    HardFork,
21    /// Always active from genesis
22    AlwaysActive,
23}
24
25/// Feature activation information
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct FeatureActivation {
28    /// Feature name
29    pub feature_name: String,
30    /// Activation block height (if height-based)
31    pub activation_height: Option<u64>,
32    /// Activation timestamp (Unix timestamp, if time-based)
33    pub activation_timestamp: Option<u64>,
34    /// Activation method
35    pub activation_method: ActivationMethod,
36    /// BIP number (if applicable)
37    pub bip_number: Option<u32>,
38}
39
40impl FeatureActivation {
41    /// Check if feature is active at given height and timestamp
42    pub fn is_active_at(&self, height: u64, timestamp: u64) -> bool {
43        match self.activation_method {
44            ActivationMethod::AlwaysActive => true,
45            ActivationMethod::HardFork => {
46                // Hard forks activate immediately at genesis
47                true
48            }
49            ActivationMethod::HeightBased => {
50                if let Some(activation_height) = self.activation_height {
51                    height >= activation_height
52                } else {
53                    false
54                }
55            }
56            ActivationMethod::Timestamp => {
57                if let Some(activation_timestamp) = self.activation_timestamp {
58                    timestamp >= activation_timestamp
59                } else {
60                    false
61                }
62            }
63            ActivationMethod::BIP9 => {
64                // BIP9 activation is determined by height (block height at which the deployment
65                // is considered ACTIVE after lock-in). Timestamp alone must NOT activate a
66                // feature — that was the bug: `height_active || timestamp_active` allowed
67                // timestamp to short-circuit the height gate.
68                //
69                // If activation_height is set, it is the authoritative gate.
70                // Timestamp is only used as the gate when no activation_height is configured
71                // (timestamp-only BIP9 pre-height-based locking).
72                if self.activation_height.is_some() {
73                    self.activation_height.is_some_and(|h| height >= h)
74                } else {
75                    self.activation_timestamp.is_some_and(|t| timestamp >= t)
76                }
77            }
78        }
79    }
80}
81
82/// Feature activation registry for a protocol version
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct FeatureRegistry {
85    /// Protocol version
86    pub protocol_version: ProtocolVersion,
87    /// Feature activations
88    pub features: Vec<FeatureActivation>,
89}
90
91impl FeatureRegistry {
92    /// Get feature activations for a protocol version
93    pub fn for_protocol(version: ProtocolVersion) -> Self {
94        match version {
95            ProtocolVersion::BitcoinV1 => Self::mainnet(),
96            ProtocolVersion::Testnet3 => Self::testnet(),
97            ProtocolVersion::Regtest => Self::regtest(),
98        }
99    }
100
101    /// Mainnet feature activations
102    pub fn mainnet() -> Self {
103        Self {
104            protocol_version: ProtocolVersion::BitcoinV1,
105            features: vec![
106                // SegWit activated via BIP9 at block 481,824 (August 24, 2017)
107                FeatureActivation {
108                    feature_name: "segwit".to_string(),
109                    activation_height: Some(481_824),
110                    activation_timestamp: Some(1503539857), // Aug 24, 2017
111                    activation_method: ActivationMethod::BIP9,
112                    bip_number: Some(141),
113                },
114                // Taproot activated via BIP9 at block 709,632 (November 14, 2021)
115                FeatureActivation {
116                    feature_name: "taproot".to_string(),
117                    activation_height: Some(709_632),
118                    activation_timestamp: Some(1636934400), // Nov 14, 2021
119                    activation_method: ActivationMethod::BIP9,
120                    bip_number: Some(341),
121                },
122                // RBF (BIP125) - Always available (mempool policy)
123                FeatureActivation {
124                    feature_name: "rbf".to_string(),
125                    activation_height: Some(0),
126                    activation_timestamp: None,
127                    activation_method: ActivationMethod::AlwaysActive,
128                    bip_number: Some(125),
129                },
130                // CTV (CheckTemplateVerify) - Not yet activated
131                FeatureActivation {
132                    feature_name: "ctv".to_string(),
133                    activation_height: None,
134                    activation_timestamp: None,
135                    activation_method: ActivationMethod::BIP9,
136                    bip_number: Some(119),
137                },
138                // CSV (CheckSequenceVerify) - Always active
139                FeatureActivation {
140                    feature_name: "csv".to_string(),
141                    activation_height: Some(0),
142                    activation_timestamp: None,
143                    activation_method: ActivationMethod::AlwaysActive,
144                    bip_number: Some(112),
145                },
146                // CLTV (CheckLockTimeVerify) - Always active
147                FeatureActivation {
148                    feature_name: "cltv".to_string(),
149                    activation_height: Some(0),
150                    activation_timestamp: None,
151                    activation_method: ActivationMethod::AlwaysActive,
152                    bip_number: Some(65),
153                },
154            ],
155        }
156    }
157
158    /// Testnet feature activations
159    pub fn testnet() -> Self {
160        Self {
161            protocol_version: ProtocolVersion::Testnet3,
162            features: vec![
163                // SegWit activated earlier on testnet
164                FeatureActivation {
165                    feature_name: "segwit".to_string(),
166                    activation_height: Some(465_600), // Earlier on testnet
167                    activation_timestamp: Some(1493596800), // May 1, 2017
168                    activation_method: ActivationMethod::BIP9,
169                    bip_number: Some(141),
170                },
171                // Taproot activated earlier on testnet
172                FeatureActivation {
173                    feature_name: "taproot".to_string(),
174                    activation_height: Some(2_016_000), // Earlier on testnet
175                    activation_timestamp: Some(1628640000), // Aug 11, 2021
176                    activation_method: ActivationMethod::BIP9,
177                    bip_number: Some(341),
178                },
179                // RBF - Always available
180                FeatureActivation {
181                    feature_name: "rbf".to_string(),
182                    activation_height: Some(0),
183                    activation_timestamp: None,
184                    activation_method: ActivationMethod::AlwaysActive,
185                    bip_number: Some(125),
186                },
187                // CSV - Always active
188                FeatureActivation {
189                    feature_name: "csv".to_string(),
190                    activation_height: Some(0),
191                    activation_timestamp: None,
192                    activation_method: ActivationMethod::AlwaysActive,
193                    bip_number: Some(112),
194                },
195                // CLTV - Always active
196                FeatureActivation {
197                    feature_name: "cltv".to_string(),
198                    activation_height: Some(0),
199                    activation_timestamp: None,
200                    activation_method: ActivationMethod::AlwaysActive,
201                    bip_number: Some(65),
202                },
203            ],
204        }
205    }
206
207    /// Regtest feature activations (all features active from genesis)
208    pub fn regtest() -> Self {
209        Self {
210            protocol_version: ProtocolVersion::Regtest,
211            features: vec![
212                // All features active from genesis on regtest
213                FeatureActivation {
214                    feature_name: "segwit".to_string(),
215                    activation_height: Some(0),
216                    activation_timestamp: None,
217                    activation_method: ActivationMethod::AlwaysActive,
218                    bip_number: Some(141),
219                },
220                FeatureActivation {
221                    feature_name: "taproot".to_string(),
222                    activation_height: Some(0),
223                    activation_timestamp: None,
224                    activation_method: ActivationMethod::AlwaysActive,
225                    bip_number: Some(341),
226                },
227                FeatureActivation {
228                    feature_name: "rbf".to_string(),
229                    activation_height: Some(0),
230                    activation_timestamp: None,
231                    activation_method: ActivationMethod::AlwaysActive,
232                    bip_number: Some(125),
233                },
234                FeatureActivation {
235                    feature_name: "csv".to_string(),
236                    activation_height: Some(0),
237                    activation_timestamp: None,
238                    activation_method: ActivationMethod::AlwaysActive,
239                    bip_number: Some(112),
240                },
241                FeatureActivation {
242                    feature_name: "cltv".to_string(),
243                    activation_height: Some(0),
244                    activation_timestamp: None,
245                    activation_method: ActivationMethod::AlwaysActive,
246                    bip_number: Some(65),
247                },
248                FeatureActivation {
249                    feature_name: "fast_mining".to_string(),
250                    activation_height: Some(0),
251                    activation_timestamp: None,
252                    activation_method: ActivationMethod::AlwaysActive,
253                    bip_number: None,
254                },
255            ],
256        }
257    }
258
259    /// Check if a feature is active at a given height and timestamp
260    pub fn is_feature_active(&self, feature_name: &str, height: u64, timestamp: u64) -> bool {
261        self.features
262            .iter()
263            .find(|f| f.feature_name == feature_name)
264            .map(|f| f.is_active_at(height, timestamp))
265            .unwrap_or(false)
266    }
267
268    /// Get feature activation information
269    pub fn get_feature(&self, feature_name: &str) -> Option<&FeatureActivation> {
270        self.features
271            .iter()
272            .find(|f| f.feature_name == feature_name)
273    }
274
275    /// List all features
276    pub fn list_features(&self) -> Vec<String> {
277        self.features
278            .iter()
279            .map(|f| f.feature_name.clone())
280            .collect()
281    }
282
283    /// Create a FeatureContext for a specific height and timestamp
284    /// This consolidates all feature activation checks into a single context
285    pub fn create_context(&self, height: u64, timestamp: u64) -> FeatureContext {
286        FeatureContext {
287            segwit: self.is_feature_active("segwit", height, timestamp),
288            taproot: self.is_feature_active("taproot", height, timestamp),
289            csv: self.is_feature_active("csv", height, timestamp),
290            cltv: self.is_feature_active("cltv", height, timestamp),
291            rbf: self.is_feature_active("rbf", height, timestamp),
292            ctv: self.is_feature_active("ctv", height, timestamp),
293            height,
294            timestamp,
295        }
296    }
297}
298
299/// Feature context consolidating all Bitcoin feature flags at a specific height/timestamp
300/// This provides a single source of truth for feature activation state
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
302pub struct FeatureContext {
303    /// SegWit (BIP141/143) activation state
304    pub segwit: bool,
305    /// Taproot (BIP341/342) activation state
306    pub taproot: bool,
307    /// CSV (BIP112) activation state
308    pub csv: bool,
309    /// CLTV (BIP65) activation state
310    pub cltv: bool,
311    /// RBF (BIP125) activation state (mempool policy)
312    pub rbf: bool,
313    /// CTV (BIP119) activation state
314    pub ctv: bool,
315    /// Block height at which this context is valid
316    pub height: u64,
317    /// Timestamp at which this context is valid
318    pub timestamp: u64,
319}
320
321impl FeatureContext {
322    /// Create a new feature context from a feature registry
323    pub fn from_registry(registry: &FeatureRegistry, height: u64, timestamp: u64) -> Self {
324        registry.create_context(height, timestamp)
325    }
326
327    /// Check if a specific feature is active
328    pub fn is_active(&self, feature: &str) -> bool {
329        match feature {
330            "segwit" => self.segwit,
331            "taproot" => self.taproot,
332            "csv" => self.csv,
333            "cltv" => self.cltv,
334            "rbf" => self.rbf,
335            "ctv" => self.ctv,
336            _ => false,
337        }
338    }
339
340    /// Get list of all active features
341    pub fn active_features(&self) -> Vec<&'static str> {
342        let mut features = Vec::new();
343        if self.segwit {
344            features.push("segwit");
345        }
346        if self.taproot {
347            features.push("taproot");
348        }
349        if self.csv {
350            features.push("csv");
351        }
352        if self.cltv {
353            features.push("cltv");
354        }
355        if self.rbf {
356            features.push("rbf");
357        }
358        if self.ctv {
359            features.push("ctv");
360        }
361        features
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_segwit_activation_mainnet() {
371        let registry = FeatureRegistry::mainnet();
372
373        // Before activation
374        assert!(!registry.is_feature_active("segwit", 481_823, 1503539000));
375
376        // At activation height
377        assert!(registry.is_feature_active("segwit", 481_824, 1503539857));
378
379        // After activation
380        assert!(registry.is_feature_active("segwit", 500_000, 1504000000));
381    }
382
383    #[test]
384    fn test_taproot_activation_mainnet() {
385        let registry = FeatureRegistry::mainnet();
386
387        // Before activation
388        assert!(!registry.is_feature_active("taproot", 709_631, 1636934000));
389
390        // At activation height
391        assert!(registry.is_feature_active("taproot", 709_632, 1636934400));
392
393        // After activation
394        assert!(registry.is_feature_active("taproot", 800_000, 1640000000));
395    }
396
397    #[test]
398    fn test_always_active_features() {
399        let registry = FeatureRegistry::mainnet();
400
401        // RBF, CSV, CLTV should always be active
402        assert!(registry.is_feature_active("rbf", 0, 1231006505));
403        assert!(registry.is_feature_active("csv", 0, 1231006505));
404        assert!(registry.is_feature_active("cltv", 0, 1231006505));
405        assert!(registry.is_feature_active("rbf", 1_000_000, 2000000000));
406    }
407
408    #[test]
409    fn test_regtest_all_features_active() {
410        let registry = FeatureRegistry::regtest();
411
412        // All features should be active from genesis on regtest
413        assert!(registry.is_feature_active("segwit", 0, 1231006505));
414        assert!(registry.is_feature_active("taproot", 0, 1231006505));
415        assert!(registry.is_feature_active("rbf", 0, 1231006505));
416        assert!(registry.is_feature_active("fast_mining", 0, 1231006505));
417    }
418
419    #[test]
420    fn test_testnet_earlier_activations() {
421        let registry = FeatureRegistry::testnet();
422
423        // SegWit activated earlier on testnet
424        assert!(!registry.is_feature_active("segwit", 465_599, 1493596000));
425        assert!(registry.is_feature_active("segwit", 465_600, 1493596800));
426        assert!(registry.is_feature_active("segwit", 500_000, 1500000000));
427    }
428
429    #[test]
430    fn test_feature_not_found() {
431        let registry = FeatureRegistry::mainnet();
432
433        // Non-existent feature should return false
434        assert!(!registry.is_feature_active("nonexistent", 1_000_000, 2000000000));
435    }
436
437    #[test]
438    fn test_get_feature() {
439        let registry = FeatureRegistry::mainnet();
440
441        let segwit = registry.get_feature("segwit").unwrap();
442        assert_eq!(segwit.feature_name, "segwit");
443        assert_eq!(segwit.bip_number, Some(141));
444        assert_eq!(segwit.activation_method, ActivationMethod::BIP9);
445
446        assert!(registry.get_feature("nonexistent").is_none());
447    }
448
449    #[test]
450    fn test_list_features() {
451        let mainnet = FeatureRegistry::mainnet();
452        let features = mainnet.list_features();
453
454        assert!(features.contains(&"segwit".to_string()));
455        assert!(features.contains(&"taproot".to_string()));
456        assert!(features.contains(&"rbf".to_string()));
457        assert!(features.contains(&"csv".to_string()));
458        assert!(features.contains(&"cltv".to_string()));
459    }
460
461    #[test]
462    fn test_activation_methods() {
463        let mainnet = FeatureRegistry::mainnet();
464
465        let segwit = mainnet.get_feature("segwit").unwrap();
466        assert_eq!(segwit.activation_method, ActivationMethod::BIP9);
467
468        let rbf = mainnet.get_feature("rbf").unwrap();
469        assert_eq!(rbf.activation_method, ActivationMethod::AlwaysActive);
470    }
471
472    #[test]
473    fn test_bip9_height_and_timestamp() {
474        let registry = FeatureRegistry::mainnet();
475
476        // BIP9 with activation_height set: height is authoritative.
477        // Height met, timestamp below activation → active (height wins).
478        assert!(registry.is_feature_active("segwit", 481_824, 1500000000));
479
480        // Height NOT met, timestamp at/above activation → NOT active.
481        // This is the corrected behaviour: timestamp alone cannot activate
482        // a BIP9 feature that has an explicit activation_height configured.
483        assert!(!registry.is_feature_active("segwit", 481_000, 1503539857));
484
485        // Both met → active.
486        assert!(registry.is_feature_active("segwit", 481_824, 1503539857));
487    }
488
489    #[test]
490    fn test_feature_context_creation() {
491        let registry = FeatureRegistry::mainnet();
492
493        // Before SegWit activation
494        let ctx_before = registry.create_context(481_823, 1503539000);
495        assert!(!ctx_before.segwit);
496        assert!(!ctx_before.taproot);
497        assert!(ctx_before.csv); // CSV is always active
498        assert!(ctx_before.cltv); // CLTV is always active
499        assert!(ctx_before.rbf); // RBF is always active
500
501        // At SegWit activation
502        let ctx_at_segwit = registry.create_context(481_824, 1503539857);
503        assert!(ctx_at_segwit.segwit);
504        assert!(!ctx_at_segwit.taproot);
505
506        // At Taproot activation
507        let ctx_at_taproot = registry.create_context(709_632, 1636934400);
508        assert!(ctx_at_taproot.segwit);
509        assert!(ctx_at_taproot.taproot);
510
511        // After all activations
512        let ctx_after = registry.create_context(800_000, 1640000000);
513        assert!(ctx_after.segwit);
514        assert!(ctx_after.taproot);
515    }
516
517    #[test]
518    fn test_feature_context_is_active() {
519        let registry = FeatureRegistry::mainnet();
520        let ctx = registry.create_context(800_000, 1640000000);
521
522        assert!(ctx.is_active("segwit"));
523        assert!(ctx.is_active("taproot"));
524        assert!(ctx.is_active("csv"));
525        assert!(ctx.is_active("cltv"));
526        assert!(ctx.is_active("rbf"));
527        assert!(!ctx.is_active("ctv")); // CTV not activated
528        assert!(!ctx.is_active("nonexistent"));
529    }
530
531    #[test]
532    fn test_feature_context_active_features() {
533        let registry = FeatureRegistry::mainnet();
534
535        // Before any activations
536        let ctx_before = registry.create_context(0, 1231006505);
537        let active = ctx_before.active_features();
538        assert!(active.contains(&"csv"));
539        assert!(active.contains(&"cltv"));
540        assert!(active.contains(&"rbf"));
541        assert!(!active.contains(&"segwit"));
542        assert!(!active.contains(&"taproot"));
543
544        // After all activations
545        let ctx_after = registry.create_context(800_000, 1640000000);
546        let active = ctx_after.active_features();
547        assert!(active.contains(&"segwit"));
548        assert!(active.contains(&"taproot"));
549        assert!(active.contains(&"csv"));
550        assert!(active.contains(&"cltv"));
551        assert!(active.contains(&"rbf"));
552    }
553
554    #[test]
555    fn test_feature_context_regtest() {
556        let registry = FeatureRegistry::regtest();
557        let ctx = registry.create_context(0, 1231006505);
558
559        // All features should be active from genesis on regtest
560        assert!(ctx.segwit);
561        assert!(ctx.taproot);
562        assert!(ctx.csv);
563        assert!(ctx.cltv);
564        assert!(ctx.rbf);
565    }
566
567    #[test]
568    fn test_feature_context_from_registry() {
569        let registry = FeatureRegistry::mainnet();
570        let ctx = FeatureContext::from_registry(&registry, 800_000, 1640000000);
571
572        assert!(ctx.segwit);
573        assert!(ctx.taproot);
574        assert_eq!(ctx.height, 800_000);
575        assert_eq!(ctx.timestamp, 1640000000);
576    }
577}