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