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            ProtocolVersion::Signet => Self::signet(),
99            ProtocolVersion::Testnet4 => Self::testnet4(),
100        }
101    }
102
103    /// Mainnet feature activations
104    pub fn mainnet() -> Self {
105        Self {
106            protocol_version: ProtocolVersion::BitcoinV1,
107            features: vec![
108                // SegWit activated via BIP9 at block 481,824 (August 24, 2017)
109                FeatureActivation {
110                    feature_name: "segwit".to_string(),
111                    activation_height: Some(481_824),
112                    activation_timestamp: Some(1503539857), // Aug 24, 2017
113                    activation_method: ActivationMethod::BIP9,
114                    bip_number: Some(141),
115                },
116                // Taproot activated via BIP9 at block 709,632 (November 14, 2021)
117                FeatureActivation {
118                    feature_name: "taproot".to_string(),
119                    activation_height: Some(709_632),
120                    activation_timestamp: Some(1636934400), // Nov 14, 2021
121                    activation_method: ActivationMethod::BIP9,
122                    bip_number: Some(341),
123                },
124                // RBF (BIP125) - Always available (mempool policy)
125                FeatureActivation {
126                    feature_name: "rbf".to_string(),
127                    activation_height: Some(0),
128                    activation_timestamp: None,
129                    activation_method: ActivationMethod::AlwaysActive,
130                    bip_number: Some(125),
131                },
132                // CTV (CheckTemplateVerify) - Not yet activated
133                FeatureActivation {
134                    feature_name: "ctv".to_string(),
135                    activation_height: None,
136                    activation_timestamp: None,
137                    activation_method: ActivationMethod::BIP9,
138                    bip_number: Some(119),
139                },
140                // CSV (CheckSequenceVerify) - Always active
141                FeatureActivation {
142                    feature_name: "csv".to_string(),
143                    activation_height: Some(0),
144                    activation_timestamp: None,
145                    activation_method: ActivationMethod::AlwaysActive,
146                    bip_number: Some(112),
147                },
148                // CLTV (CheckLockTimeVerify) - Always active
149                FeatureActivation {
150                    feature_name: "cltv".to_string(),
151                    activation_height: Some(0),
152                    activation_timestamp: None,
153                    activation_method: ActivationMethod::AlwaysActive,
154                    bip_number: Some(65),
155                },
156            ],
157        }
158    }
159
160    /// Testnet feature activations
161    pub fn testnet() -> Self {
162        Self {
163            protocol_version: ProtocolVersion::Testnet3,
164            features: vec![
165                // SegWit activated earlier on testnet
166                FeatureActivation {
167                    feature_name: "segwit".to_string(),
168                    activation_height: Some(465_600), // Earlier on testnet
169                    activation_timestamp: Some(1493596800), // May 1, 2017
170                    activation_method: ActivationMethod::BIP9,
171                    bip_number: Some(141),
172                },
173                // Taproot activated earlier on testnet
174                FeatureActivation {
175                    feature_name: "taproot".to_string(),
176                    activation_height: Some(2_016_000), // Earlier on testnet
177                    activation_timestamp: Some(1628640000), // Aug 11, 2021
178                    activation_method: ActivationMethod::BIP9,
179                    bip_number: Some(341),
180                },
181                // RBF - Always available
182                FeatureActivation {
183                    feature_name: "rbf".to_string(),
184                    activation_height: Some(0),
185                    activation_timestamp: None,
186                    activation_method: ActivationMethod::AlwaysActive,
187                    bip_number: Some(125),
188                },
189                // CSV - Always active
190                FeatureActivation {
191                    feature_name: "csv".to_string(),
192                    activation_height: Some(0),
193                    activation_timestamp: None,
194                    activation_method: ActivationMethod::AlwaysActive,
195                    bip_number: Some(112),
196                },
197                // CLTV - Always active
198                FeatureActivation {
199                    feature_name: "cltv".to_string(),
200                    activation_height: Some(0),
201                    activation_timestamp: None,
202                    activation_method: ActivationMethod::AlwaysActive,
203                    bip_number: Some(65),
204                },
205            ],
206        }
207    }
208
209    /// Regtest feature activations (all features active from genesis)
210    pub fn regtest() -> Self {
211        Self {
212            protocol_version: ProtocolVersion::Regtest,
213            features: vec![
214                // All features active from genesis on regtest
215                FeatureActivation {
216                    feature_name: "segwit".to_string(),
217                    activation_height: Some(0),
218                    activation_timestamp: None,
219                    activation_method: ActivationMethod::AlwaysActive,
220                    bip_number: Some(141),
221                },
222                FeatureActivation {
223                    feature_name: "taproot".to_string(),
224                    activation_height: Some(0),
225                    activation_timestamp: None,
226                    activation_method: ActivationMethod::AlwaysActive,
227                    bip_number: Some(341),
228                },
229                FeatureActivation {
230                    feature_name: "rbf".to_string(),
231                    activation_height: Some(0),
232                    activation_timestamp: None,
233                    activation_method: ActivationMethod::AlwaysActive,
234                    bip_number: Some(125),
235                },
236                FeatureActivation {
237                    feature_name: "csv".to_string(),
238                    activation_height: Some(0),
239                    activation_timestamp: None,
240                    activation_method: ActivationMethod::AlwaysActive,
241                    bip_number: Some(112),
242                },
243                FeatureActivation {
244                    feature_name: "cltv".to_string(),
245                    activation_height: Some(0),
246                    activation_timestamp: None,
247                    activation_method: ActivationMethod::AlwaysActive,
248                    bip_number: Some(65),
249                },
250                FeatureActivation {
251                    feature_name: "fast_mining".to_string(),
252                    activation_height: Some(0),
253                    activation_timestamp: None,
254                    activation_method: ActivationMethod::AlwaysActive,
255                    bip_number: None,
256                },
257            ],
258        }
259    }
260
261    /// Signet feature activations (Core signet: forks active from height 1)
262    pub fn signet() -> Self {
263        Self {
264            protocol_version: ProtocolVersion::Signet,
265            features: vec![
266                FeatureActivation {
267                    feature_name: "segwit".to_string(),
268                    activation_height: Some(1),
269                    activation_timestamp: None,
270                    activation_method: ActivationMethod::HeightBased,
271                    bip_number: Some(141),
272                },
273                FeatureActivation {
274                    feature_name: "taproot".to_string(),
275                    activation_height: Some(1),
276                    activation_timestamp: None,
277                    activation_method: ActivationMethod::HeightBased,
278                    bip_number: Some(341),
279                },
280                FeatureActivation {
281                    feature_name: "rbf".to_string(),
282                    activation_height: Some(0),
283                    activation_timestamp: None,
284                    activation_method: ActivationMethod::AlwaysActive,
285                    bip_number: Some(125),
286                },
287                FeatureActivation {
288                    feature_name: "csv".to_string(),
289                    activation_height: Some(1),
290                    activation_timestamp: None,
291                    activation_method: ActivationMethod::HeightBased,
292                    bip_number: Some(112),
293                },
294                FeatureActivation {
295                    feature_name: "cltv".to_string(),
296                    activation_height: Some(1),
297                    activation_timestamp: None,
298                    activation_method: ActivationMethod::HeightBased,
299                    bip_number: Some(65),
300                },
301                FeatureActivation {
302                    feature_name: "signet".to_string(),
303                    activation_height: Some(0),
304                    activation_timestamp: None,
305                    activation_method: ActivationMethod::AlwaysActive,
306                    bip_number: Some(325),
307                },
308            ],
309        }
310    }
311
312    /// Testnet4 feature activations (Core: buried forks from height 1, BIP94 always).
313    pub fn testnet4() -> Self {
314        Self {
315            protocol_version: ProtocolVersion::Testnet4,
316            features: vec![
317                FeatureActivation {
318                    feature_name: "segwit".to_string(),
319                    activation_height: Some(1),
320                    activation_timestamp: None,
321                    activation_method: ActivationMethod::HeightBased,
322                    bip_number: Some(141),
323                },
324                FeatureActivation {
325                    feature_name: "taproot".to_string(),
326                    activation_height: Some(1),
327                    activation_timestamp: None,
328                    activation_method: ActivationMethod::HeightBased,
329                    bip_number: Some(341),
330                },
331                FeatureActivation {
332                    feature_name: "rbf".to_string(),
333                    activation_height: Some(0),
334                    activation_timestamp: None,
335                    activation_method: ActivationMethod::AlwaysActive,
336                    bip_number: Some(125),
337                },
338                FeatureActivation {
339                    feature_name: "csv".to_string(),
340                    activation_height: Some(1),
341                    activation_timestamp: None,
342                    activation_method: ActivationMethod::HeightBased,
343                    bip_number: Some(112),
344                },
345                FeatureActivation {
346                    feature_name: "cltv".to_string(),
347                    activation_height: Some(1),
348                    activation_timestamp: None,
349                    activation_method: ActivationMethod::HeightBased,
350                    bip_number: Some(65),
351                },
352            ],
353        }
354    }
355
356    /// Check if a feature is active at a given height and timestamp
357    pub fn is_feature_active(&self, feature_name: &str, height: u64, timestamp: u64) -> bool {
358        self.features
359            .iter()
360            .find(|f| f.feature_name == feature_name)
361            .map(|f| f.is_active_at(height, timestamp))
362            .unwrap_or(false)
363    }
364
365    /// Get feature activation information
366    pub fn get_feature(&self, feature_name: &str) -> Option<&FeatureActivation> {
367        self.features
368            .iter()
369            .find(|f| f.feature_name == feature_name)
370    }
371
372    /// List all features
373    pub fn list_features(&self) -> Vec<String> {
374        self.features
375            .iter()
376            .map(|f| f.feature_name.clone())
377            .collect()
378    }
379
380    /// Create a FeatureContext for a specific height and timestamp
381    /// This consolidates all feature activation checks into a single context
382    pub fn create_context(&self, height: u64, timestamp: u64) -> FeatureContext {
383        FeatureContext {
384            segwit: self.is_feature_active("segwit", height, timestamp),
385            taproot: self.is_feature_active("taproot", height, timestamp),
386            csv: self.is_feature_active("csv", height, timestamp),
387            cltv: self.is_feature_active("cltv", height, timestamp),
388            rbf: self.is_feature_active("rbf", height, timestamp),
389            ctv: self.is_feature_active("ctv", height, timestamp),
390            height,
391            timestamp,
392        }
393    }
394}
395
396/// Feature context consolidating all Bitcoin feature flags at a specific height/timestamp
397/// This provides a single source of truth for feature activation state
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
399pub struct FeatureContext {
400    /// SegWit (BIP141/143) activation state
401    pub segwit: bool,
402    /// Taproot (BIP341/342) activation state
403    pub taproot: bool,
404    /// CSV (BIP112) activation state
405    pub csv: bool,
406    /// CLTV (BIP65) activation state
407    pub cltv: bool,
408    /// RBF (BIP125) activation state (mempool policy)
409    pub rbf: bool,
410    /// CTV (BIP119) activation state
411    pub ctv: bool,
412    /// Block height at which this context is valid
413    pub height: u64,
414    /// Timestamp at which this context is valid
415    pub timestamp: u64,
416}
417
418impl FeatureContext {
419    /// Create a new feature context from a feature registry
420    pub fn from_registry(registry: &FeatureRegistry, height: u64, timestamp: u64) -> Self {
421        registry.create_context(height, timestamp)
422    }
423
424    /// Check if a specific feature is active
425    pub fn is_active(&self, feature: &str) -> bool {
426        match feature {
427            "segwit" => self.segwit,
428            "taproot" => self.taproot,
429            "csv" => self.csv,
430            "cltv" => self.cltv,
431            "rbf" => self.rbf,
432            "ctv" => self.ctv,
433            _ => false,
434        }
435    }
436
437    /// Get list of all active features
438    pub fn active_features(&self) -> Vec<&'static str> {
439        let mut features = Vec::new();
440        if self.segwit {
441            features.push("segwit");
442        }
443        if self.taproot {
444            features.push("taproot");
445        }
446        if self.csv {
447            features.push("csv");
448        }
449        if self.cltv {
450            features.push("cltv");
451        }
452        if self.rbf {
453            features.push("rbf");
454        }
455        if self.ctv {
456            features.push("ctv");
457        }
458        features
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_segwit_activation_mainnet() {
468        let registry = FeatureRegistry::mainnet();
469
470        // Before activation
471        assert!(!registry.is_feature_active("segwit", 481_823, 1503539000));
472
473        // At activation height
474        assert!(registry.is_feature_active("segwit", 481_824, 1503539857));
475
476        // After activation
477        assert!(registry.is_feature_active("segwit", 500_000, 1504000000));
478    }
479
480    #[test]
481    fn test_taproot_activation_mainnet() {
482        let registry = FeatureRegistry::mainnet();
483
484        // Before activation
485        assert!(!registry.is_feature_active("taproot", 709_631, 1636934000));
486
487        // At activation height
488        assert!(registry.is_feature_active("taproot", 709_632, 1636934400));
489
490        // After activation
491        assert!(registry.is_feature_active("taproot", 800_000, 1640000000));
492    }
493
494    #[test]
495    fn test_always_active_features() {
496        let registry = FeatureRegistry::mainnet();
497
498        // RBF, CSV, CLTV should always be active
499        assert!(registry.is_feature_active("rbf", 0, 1231006505));
500        assert!(registry.is_feature_active("csv", 0, 1231006505));
501        assert!(registry.is_feature_active("cltv", 0, 1231006505));
502        assert!(registry.is_feature_active("rbf", 1_000_000, 2000000000));
503    }
504
505    #[test]
506    fn test_regtest_all_features_active() {
507        let registry = FeatureRegistry::regtest();
508
509        // All features should be active from genesis on regtest
510        assert!(registry.is_feature_active("segwit", 0, 1231006505));
511        assert!(registry.is_feature_active("taproot", 0, 1231006505));
512        assert!(registry.is_feature_active("rbf", 0, 1231006505));
513        assert!(registry.is_feature_active("fast_mining", 0, 1231006505));
514    }
515
516    #[test]
517    fn test_testnet_earlier_activations() {
518        let registry = FeatureRegistry::testnet();
519
520        // SegWit activated earlier on testnet
521        assert!(!registry.is_feature_active("segwit", 465_599, 1493596000));
522        assert!(registry.is_feature_active("segwit", 465_600, 1493596800));
523        assert!(registry.is_feature_active("segwit", 500_000, 1500000000));
524    }
525
526    #[test]
527    fn test_feature_not_found() {
528        let registry = FeatureRegistry::mainnet();
529
530        // Non-existent feature should return false
531        assert!(!registry.is_feature_active("nonexistent", 1_000_000, 2000000000));
532    }
533
534    #[test]
535    fn test_get_feature() {
536        let registry = FeatureRegistry::mainnet();
537
538        let segwit = registry.get_feature("segwit").unwrap();
539        assert_eq!(segwit.feature_name, "segwit");
540        assert_eq!(segwit.bip_number, Some(141));
541        assert_eq!(segwit.activation_method, ActivationMethod::BIP9);
542
543        assert!(registry.get_feature("nonexistent").is_none());
544    }
545
546    #[test]
547    fn test_list_features() {
548        let mainnet = FeatureRegistry::mainnet();
549        let features = mainnet.list_features();
550
551        assert!(features.contains(&"segwit".to_string()));
552        assert!(features.contains(&"taproot".to_string()));
553        assert!(features.contains(&"rbf".to_string()));
554        assert!(features.contains(&"csv".to_string()));
555        assert!(features.contains(&"cltv".to_string()));
556    }
557
558    #[test]
559    fn test_activation_methods() {
560        let mainnet = FeatureRegistry::mainnet();
561
562        let segwit = mainnet.get_feature("segwit").unwrap();
563        assert_eq!(segwit.activation_method, ActivationMethod::BIP9);
564
565        let rbf = mainnet.get_feature("rbf").unwrap();
566        assert_eq!(rbf.activation_method, ActivationMethod::AlwaysActive);
567    }
568
569    #[test]
570    fn test_bip9_height_and_timestamp() {
571        let registry = FeatureRegistry::mainnet();
572
573        // BIP9 with activation_height set: height is authoritative.
574        // Height met, timestamp below activation → active (height wins).
575        assert!(registry.is_feature_active("segwit", 481_824, 1500000000));
576
577        // Height NOT met, timestamp at/above activation → NOT active.
578        // This is the corrected behaviour: timestamp alone cannot activate
579        // a BIP9 feature that has an explicit activation_height configured.
580        assert!(!registry.is_feature_active("segwit", 481_000, 1503539857));
581
582        // Both met → active.
583        assert!(registry.is_feature_active("segwit", 481_824, 1503539857));
584    }
585
586    #[test]
587    fn test_feature_context_creation() {
588        let registry = FeatureRegistry::mainnet();
589
590        // Before SegWit activation
591        let ctx_before = registry.create_context(481_823, 1503539000);
592        assert!(!ctx_before.segwit);
593        assert!(!ctx_before.taproot);
594        assert!(ctx_before.csv); // CSV is always active
595        assert!(ctx_before.cltv); // CLTV is always active
596        assert!(ctx_before.rbf); // RBF is always active
597
598        // At SegWit activation
599        let ctx_at_segwit = registry.create_context(481_824, 1503539857);
600        assert!(ctx_at_segwit.segwit);
601        assert!(!ctx_at_segwit.taproot);
602
603        // At Taproot activation
604        let ctx_at_taproot = registry.create_context(709_632, 1636934400);
605        assert!(ctx_at_taproot.segwit);
606        assert!(ctx_at_taproot.taproot);
607
608        // After all activations
609        let ctx_after = registry.create_context(800_000, 1640000000);
610        assert!(ctx_after.segwit);
611        assert!(ctx_after.taproot);
612    }
613
614    #[test]
615    fn test_feature_context_is_active() {
616        let registry = FeatureRegistry::mainnet();
617        let ctx = registry.create_context(800_000, 1640000000);
618
619        assert!(ctx.is_active("segwit"));
620        assert!(ctx.is_active("taproot"));
621        assert!(ctx.is_active("csv"));
622        assert!(ctx.is_active("cltv"));
623        assert!(ctx.is_active("rbf"));
624        assert!(!ctx.is_active("ctv")); // CTV not activated
625        assert!(!ctx.is_active("nonexistent"));
626    }
627
628    #[test]
629    fn test_feature_context_active_features() {
630        let registry = FeatureRegistry::mainnet();
631
632        // Before any activations
633        let ctx_before = registry.create_context(0, 1231006505);
634        let active = ctx_before.active_features();
635        assert!(active.contains(&"csv"));
636        assert!(active.contains(&"cltv"));
637        assert!(active.contains(&"rbf"));
638        assert!(!active.contains(&"segwit"));
639        assert!(!active.contains(&"taproot"));
640
641        // After all activations
642        let ctx_after = registry.create_context(800_000, 1640000000);
643        let active = ctx_after.active_features();
644        assert!(active.contains(&"segwit"));
645        assert!(active.contains(&"taproot"));
646        assert!(active.contains(&"csv"));
647        assert!(active.contains(&"cltv"));
648        assert!(active.contains(&"rbf"));
649    }
650
651    #[test]
652    fn test_feature_context_regtest() {
653        let registry = FeatureRegistry::regtest();
654        let ctx = registry.create_context(0, 1231006505);
655
656        // All features should be active from genesis on regtest
657        assert!(ctx.segwit);
658        assert!(ctx.taproot);
659        assert!(ctx.csv);
660        assert!(ctx.cltv);
661        assert!(ctx.rbf);
662    }
663
664    #[test]
665    fn test_feature_context_from_registry() {
666        let registry = FeatureRegistry::mainnet();
667        let ctx = FeatureContext::from_registry(&registry, 800_000, 1640000000);
668
669        assert!(ctx.segwit);
670        assert!(ctx.taproot);
671        assert_eq!(ctx.height, 800_000);
672        assert_eq!(ctx.timestamp, 1640000000);
673    }
674}