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