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