use crate::ProtocolVersion;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ActivationMethod {
BIP9,
HeightBased,
Timestamp,
HardFork,
AlwaysActive,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureActivation {
pub feature_name: String,
pub activation_height: Option<u64>,
pub activation_timestamp: Option<u64>,
pub activation_method: ActivationMethod,
pub bip_number: Option<u32>,
}
impl FeatureActivation {
pub fn is_active_at(&self, height: u64, timestamp: u64) -> bool {
match self.activation_method {
ActivationMethod::AlwaysActive => true,
ActivationMethod::HardFork => {
true
}
ActivationMethod::HeightBased => {
if let Some(activation_height) = self.activation_height {
height >= activation_height
} else {
false
}
}
ActivationMethod::Timestamp => {
if let Some(activation_timestamp) = self.activation_timestamp {
timestamp >= activation_timestamp
} else {
false
}
}
ActivationMethod::BIP9 => {
let height_active = self.activation_height.is_some_and(|h| height >= h);
let timestamp_active = self.activation_timestamp.is_some_and(|t| timestamp >= t);
height_active || timestamp_active
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureRegistry {
pub protocol_version: ProtocolVersion,
pub features: Vec<FeatureActivation>,
}
impl FeatureRegistry {
pub fn for_protocol(version: ProtocolVersion) -> Self {
match version {
ProtocolVersion::BitcoinV1 => Self::mainnet(),
ProtocolVersion::Testnet3 => Self::testnet(),
ProtocolVersion::Regtest => Self::regtest(),
}
}
pub fn mainnet() -> Self {
Self {
protocol_version: ProtocolVersion::BitcoinV1,
features: vec![
FeatureActivation {
feature_name: "segwit".to_string(),
activation_height: Some(481_824),
activation_timestamp: Some(1503539857), activation_method: ActivationMethod::BIP9,
bip_number: Some(141),
},
FeatureActivation {
feature_name: "taproot".to_string(),
activation_height: Some(709_632),
activation_timestamp: Some(1636934400), activation_method: ActivationMethod::BIP9,
bip_number: Some(341),
},
FeatureActivation {
feature_name: "rbf".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(125),
},
FeatureActivation {
feature_name: "ctv".to_string(),
activation_height: None,
activation_timestamp: None,
activation_method: ActivationMethod::BIP9,
bip_number: Some(119),
},
FeatureActivation {
feature_name: "csv".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(112),
},
FeatureActivation {
feature_name: "cltv".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(65),
},
],
}
}
pub fn testnet() -> Self {
Self {
protocol_version: ProtocolVersion::Testnet3,
features: vec![
FeatureActivation {
feature_name: "segwit".to_string(),
activation_height: Some(465_600), activation_timestamp: Some(1493596800), activation_method: ActivationMethod::BIP9,
bip_number: Some(141),
},
FeatureActivation {
feature_name: "taproot".to_string(),
activation_height: Some(2_016_000), activation_timestamp: Some(1628640000), activation_method: ActivationMethod::BIP9,
bip_number: Some(341),
},
FeatureActivation {
feature_name: "rbf".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(125),
},
FeatureActivation {
feature_name: "csv".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(112),
},
FeatureActivation {
feature_name: "cltv".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(65),
},
],
}
}
pub fn regtest() -> Self {
Self {
protocol_version: ProtocolVersion::Regtest,
features: vec![
FeatureActivation {
feature_name: "segwit".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(141),
},
FeatureActivation {
feature_name: "taproot".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(341),
},
FeatureActivation {
feature_name: "rbf".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(125),
},
FeatureActivation {
feature_name: "csv".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(112),
},
FeatureActivation {
feature_name: "cltv".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: Some(65),
},
FeatureActivation {
feature_name: "fast_mining".to_string(),
activation_height: Some(0),
activation_timestamp: None,
activation_method: ActivationMethod::AlwaysActive,
bip_number: None,
},
],
}
}
pub fn is_feature_active(&self, feature_name: &str, height: u64, timestamp: u64) -> bool {
self.features
.iter()
.find(|f| f.feature_name == feature_name)
.map(|f| f.is_active_at(height, timestamp))
.unwrap_or(false)
}
pub fn get_feature(&self, feature_name: &str) -> Option<&FeatureActivation> {
self.features
.iter()
.find(|f| f.feature_name == feature_name)
}
pub fn list_features(&self) -> Vec<String> {
self.features
.iter()
.map(|f| f.feature_name.clone())
.collect()
}
pub fn create_context(&self, height: u64, timestamp: u64) -> FeatureContext {
FeatureContext {
segwit: self.is_feature_active("segwit", height, timestamp),
taproot: self.is_feature_active("taproot", height, timestamp),
csv: self.is_feature_active("csv", height, timestamp),
cltv: self.is_feature_active("cltv", height, timestamp),
rbf: self.is_feature_active("rbf", height, timestamp),
ctv: self.is_feature_active("ctv", height, timestamp),
height,
timestamp,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureContext {
pub segwit: bool,
pub taproot: bool,
pub csv: bool,
pub cltv: bool,
pub rbf: bool,
pub ctv: bool,
pub height: u64,
pub timestamp: u64,
}
impl FeatureContext {
pub fn from_registry(registry: &FeatureRegistry, height: u64, timestamp: u64) -> Self {
registry.create_context(height, timestamp)
}
pub fn is_active(&self, feature: &str) -> bool {
match feature {
"segwit" => self.segwit,
"taproot" => self.taproot,
"csv" => self.csv,
"cltv" => self.cltv,
"rbf" => self.rbf,
"ctv" => self.ctv,
_ => false,
}
}
pub fn active_features(&self) -> Vec<&'static str> {
let mut features = Vec::new();
if self.segwit {
features.push("segwit");
}
if self.taproot {
features.push("taproot");
}
if self.csv {
features.push("csv");
}
if self.cltv {
features.push("cltv");
}
if self.rbf {
features.push("rbf");
}
if self.ctv {
features.push("ctv");
}
features
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_segwit_activation_mainnet() {
let registry = FeatureRegistry::mainnet();
assert!(!registry.is_feature_active("segwit", 481_823, 1503539000));
assert!(registry.is_feature_active("segwit", 481_824, 1503539857));
assert!(registry.is_feature_active("segwit", 500_000, 1504000000));
}
#[test]
fn test_taproot_activation_mainnet() {
let registry = FeatureRegistry::mainnet();
assert!(!registry.is_feature_active("taproot", 709_631, 1636934000));
assert!(registry.is_feature_active("taproot", 709_632, 1636934400));
assert!(registry.is_feature_active("taproot", 800_000, 1640000000));
}
#[test]
fn test_always_active_features() {
let registry = FeatureRegistry::mainnet();
assert!(registry.is_feature_active("rbf", 0, 1231006505));
assert!(registry.is_feature_active("csv", 0, 1231006505));
assert!(registry.is_feature_active("cltv", 0, 1231006505));
assert!(registry.is_feature_active("rbf", 1_000_000, 2000000000));
}
#[test]
fn test_regtest_all_features_active() {
let registry = FeatureRegistry::regtest();
assert!(registry.is_feature_active("segwit", 0, 1231006505));
assert!(registry.is_feature_active("taproot", 0, 1231006505));
assert!(registry.is_feature_active("rbf", 0, 1231006505));
assert!(registry.is_feature_active("fast_mining", 0, 1231006505));
}
#[test]
fn test_testnet_earlier_activations() {
let registry = FeatureRegistry::testnet();
assert!(!registry.is_feature_active("segwit", 465_599, 1493596000));
assert!(registry.is_feature_active("segwit", 465_600, 1493596800));
assert!(registry.is_feature_active("segwit", 500_000, 1500000000));
}
#[test]
fn test_feature_not_found() {
let registry = FeatureRegistry::mainnet();
assert!(!registry.is_feature_active("nonexistent", 1_000_000, 2000000000));
}
#[test]
fn test_get_feature() {
let registry = FeatureRegistry::mainnet();
let segwit = registry.get_feature("segwit").unwrap();
assert_eq!(segwit.feature_name, "segwit");
assert_eq!(segwit.bip_number, Some(141));
assert_eq!(segwit.activation_method, ActivationMethod::BIP9);
assert!(registry.get_feature("nonexistent").is_none());
}
#[test]
fn test_list_features() {
let mainnet = FeatureRegistry::mainnet();
let features = mainnet.list_features();
assert!(features.contains(&"segwit".to_string()));
assert!(features.contains(&"taproot".to_string()));
assert!(features.contains(&"rbf".to_string()));
assert!(features.contains(&"csv".to_string()));
assert!(features.contains(&"cltv".to_string()));
}
#[test]
fn test_activation_methods() {
let mainnet = FeatureRegistry::mainnet();
let segwit = mainnet.get_feature("segwit").unwrap();
assert_eq!(segwit.activation_method, ActivationMethod::BIP9);
let rbf = mainnet.get_feature("rbf").unwrap();
assert_eq!(rbf.activation_method, ActivationMethod::AlwaysActive);
}
#[test]
fn test_bip9_height_and_timestamp() {
let registry = FeatureRegistry::mainnet();
assert!(registry.is_feature_active("segwit", 481_824, 1500000000));
assert!(registry.is_feature_active("segwit", 481_000, 1503539857));
}
#[test]
fn test_feature_context_creation() {
let registry = FeatureRegistry::mainnet();
let ctx_before = registry.create_context(481_823, 1503539000);
assert!(!ctx_before.segwit);
assert!(!ctx_before.taproot);
assert!(ctx_before.csv); assert!(ctx_before.cltv); assert!(ctx_before.rbf);
let ctx_at_segwit = registry.create_context(481_824, 1503539857);
assert!(ctx_at_segwit.segwit);
assert!(!ctx_at_segwit.taproot);
let ctx_at_taproot = registry.create_context(709_632, 1636934400);
assert!(ctx_at_taproot.segwit);
assert!(ctx_at_taproot.taproot);
let ctx_after = registry.create_context(800_000, 1640000000);
assert!(ctx_after.segwit);
assert!(ctx_after.taproot);
}
#[test]
fn test_feature_context_is_active() {
let registry = FeatureRegistry::mainnet();
let ctx = registry.create_context(800_000, 1640000000);
assert!(ctx.is_active("segwit"));
assert!(ctx.is_active("taproot"));
assert!(ctx.is_active("csv"));
assert!(ctx.is_active("cltv"));
assert!(ctx.is_active("rbf"));
assert!(!ctx.is_active("ctv")); assert!(!ctx.is_active("nonexistent"));
}
#[test]
fn test_feature_context_active_features() {
let registry = FeatureRegistry::mainnet();
let ctx_before = registry.create_context(0, 1231006505);
let active = ctx_before.active_features();
assert!(active.contains(&"csv"));
assert!(active.contains(&"cltv"));
assert!(active.contains(&"rbf"));
assert!(!active.contains(&"segwit"));
assert!(!active.contains(&"taproot"));
let ctx_after = registry.create_context(800_000, 1640000000);
let active = ctx_after.active_features();
assert!(active.contains(&"segwit"));
assert!(active.contains(&"taproot"));
assert!(active.contains(&"csv"));
assert!(active.contains(&"cltv"));
assert!(active.contains(&"rbf"));
}
#[test]
fn test_feature_context_regtest() {
let registry = FeatureRegistry::regtest();
let ctx = registry.create_context(0, 1231006505);
assert!(ctx.segwit);
assert!(ctx.taproot);
assert!(ctx.csv);
assert!(ctx.cltv);
assert!(ctx.rbf);
}
#[test]
fn test_feature_context_from_registry() {
let registry = FeatureRegistry::mainnet();
let ctx = FeatureContext::from_registry(®istry, 800_000, 1640000000);
assert!(ctx.segwit);
assert!(ctx.taproot);
assert_eq!(ctx.height, 800_000);
assert_eq!(ctx.timestamp, 1640000000);
}
}