Skip to main content

rialo_s_program_runtime/
active_features.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Per-block view of currently active features.
5//!
6//! `ActiveFeatures` is the immutable snapshot the bank installs once per block
7//! after the subdag-fed clock update, and from which every hot-path gating site
8//! reads. The map value is the **current bank slot** at the moment the
9//! snapshot was computed; it is informational (debugging, metrics) and is not
10//! persisted — historical activation slots would require on-chain state the
11//! design intentionally avoids.
12//!
13//! This type lives in `rialo-s-program-runtime` so it can be threaded
14//! through `EnvironmentConfig` → `InvokeContext`, reachable by
15//! every builtin processor. The on-chain wire crate
16//! `feature-management-program-interface` stays `#![no_std]` and free of
17//! the `ahash` dependency.
18
19use ahash::AHashMap;
20use rialo_feature_management_interface::state::FeaturesState;
21
22/// Slot number alias used by the runtime view.
23///
24/// Aliased locally rather than imported from `rialo-s-clock` because the
25/// inherited Solana SDK is not the canonical source for this concept in
26/// Rialo's runtime.
27pub type Slot = u64;
28
29/// Immutable per-block snapshot of active features.
30///
31/// Built once per bank inside `execute_blob`, immediately after
32/// `update_clock`, and held under `Arc` so every consumer in the block sees
33/// the same view. There is no mid-block mutation, no lazy refresh, and no
34/// cross-block caching.
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
36pub struct ActiveFeatures {
37    /// Active feature name → bank slot at snapshot time.
38    active: AHashMap<String, Slot>,
39}
40
41impl ActiveFeatures {
42    /// Empty snapshot — useful at genesis and in tests.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Build the snapshot for a bank by evaluating `FeaturesState` against
48    /// the current clock. Every entry whose window currently includes
49    /// `current_time_ms` is added to the snapshot, keyed by `snapshot_slot`
50    /// (the current bank slot — informational, not the historical activation
51    /// slot, which would require on-chain state the design intentionally
52    /// avoids).
53    ///
54    /// This is the canonical producer the bank calls inside
55    /// `recompute_active_features`. Construction lives here (not in the
56    /// caller) so the producer surface stays in this crate; `from_map` can
57    /// remain `pub(crate)`.
58    pub fn from_features_state(
59        state: &FeaturesState,
60        current_time_ms: u64,
61        snapshot_slot: Slot,
62    ) -> Self {
63        let mut active: AHashMap<String, Slot> = AHashMap::new();
64        // Use `FeaturesState::is_active` rather than inlining the window check
65        // so the activation semantics stay defined in exactly one place.
66        for name in state.entries().keys() {
67            if state.is_active(name, current_time_ms) {
68                active.insert(name.clone(), snapshot_slot);
69            }
70        }
71        Self::from_map(active)
72    }
73
74    /// Snapshot from a pre-built map of the **complete** active set.
75    ///
76    /// This is the canonical producer used by `Bank::recompute_active_features`:
77    /// the bank evaluates `FeaturesState` against `Clock::unix_timestamp`,
78    /// collects every feature whose window currently includes the clock, and
79    /// hands the full map (name → current bank slot) here. A feature absent
80    /// from the map is inactive — there is no "missing-means-default" fallback.
81    /// Tests that want sugar for "mark these names as active in an otherwise
82    /// empty snapshot" should use `with_overrides` instead.
83    ///
84    /// The strict `KNOWN_FEATURES` check lives in
85    /// `Bank::can_process_block` — running the same check
86    /// here would only fire on test fixtures that legitimately use
87    /// fabricated names. The producer paths
88    /// (`Self::from_features_state`, `with_overrides`) supply the names;
89    /// the readiness check is what halts a release node on an unknown one.
90    ///
91    /// `pub(crate)` so the only producer is `from_features_state` /
92    /// `with_overrides` inside this crate, matching the NORTHSTAR contract
93    /// "the only producer of `Arc<ActiveFeatures>` is the per-block
94    /// recomputation inside `execute_blob`." Cross-crate construction goes
95    /// through `from_features_state`, which takes already-validated
96    /// `FeaturesState` bytes.
97    pub(crate) fn from_map(active: AHashMap<String, Slot>) -> Self {
98        Self { active }
99    }
100
101    /// Test affordance — marks the named features as active in an
102    /// otherwise-empty snapshot.
103    ///
104    /// All overrides share the same snapshot slot `0`. Tests that need
105    /// distinct slots per feature should build the `AHashMap` themselves
106    /// and use `from_map`. Semantics intentionally differ from `from_map`:
107    /// `with_overrides` is "start from empty, add these"; `from_map` is
108    /// "this *is* the complete active set."
109    ///
110    /// Gated on `cfg(test)` or the `dev-context-only-utils` feature so the
111    /// affordance never reaches a release binary.
112    #[cfg(any(test, feature = "dev-context-only-utils"))]
113    pub fn with_overrides(features: &[&str]) -> Self {
114        let active: AHashMap<String, Slot> = features
115            .iter()
116            .map(|name| ((*name).to_string(), 0))
117            .collect();
118        Self::from_map(active)
119    }
120
121    /// Returns `true` if `feature` is active in this snapshot.
122    pub fn is_active(&self, feature: &str) -> bool {
123        self.active.contains_key(feature)
124    }
125
126    /// Returns the recorded snapshot slot for `feature`, if active.
127    ///
128    /// This is the bank slot at the moment the snapshot was computed, not
129    /// the historical activation slot.
130    pub fn snapshot_slot(&self, feature: &str) -> Option<Slot> {
131        self.active.get(feature).copied()
132    }
133
134    /// Number of active features in the snapshot.
135    pub fn len(&self) -> usize {
136        self.active.len()
137    }
138
139    /// `true` iff no features are active.
140    pub fn is_empty(&self) -> bool {
141        self.active.is_empty()
142    }
143
144    /// Iterator over `(feature_name, snapshot_slot)`.
145    pub fn iter(&self) -> impl Iterator<Item = (&str, Slot)> + '_ {
146        self.active.iter().map(|(k, v)| (k.as_str(), *v))
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    fn make(entries: &[(&str, Slot)]) -> ActiveFeatures {
155        let mut m = AHashMap::new();
156        for (k, v) in entries {
157            m.insert((*k).to_string(), *v);
158        }
159        ActiveFeatures::from_map(m)
160    }
161
162    #[test]
163    fn test_empty_is_empty() {
164        let a = ActiveFeatures::new();
165        assert!(a.is_empty());
166        assert_eq!(a.len(), 0);
167        assert!(!a.is_active("anything"));
168        assert_eq!(a.snapshot_slot("anything"), None);
169    }
170
171    #[test]
172    fn test_is_active_reflects_membership() {
173        let a = make(&[("foo", 42), ("bar", 100)]);
174        assert!(a.is_active("foo"));
175        assert!(a.is_active("bar"));
176        assert!(!a.is_active("baz"));
177    }
178
179    #[test]
180    fn test_snapshot_slot_returns_recorded_value() {
181        let a = make(&[("foo", 42)]);
182        assert_eq!(a.snapshot_slot("foo"), Some(42));
183        assert_eq!(a.snapshot_slot("missing"), None);
184    }
185
186    #[test]
187    fn test_iter_covers_all_entries() {
188        let a = make(&[("foo", 1), ("bar", 2)]);
189        let mut seen: Vec<(&str, Slot)> = a.iter().collect();
190        seen.sort_by_key(|(k, _)| *k);
191        assert_eq!(seen, vec![("bar", 2), ("foo", 1)]);
192    }
193
194    #[test]
195    fn test_with_overrides_marks_listed_features_active() {
196        let a = ActiveFeatures::with_overrides(&["foo", "bar"]);
197        assert!(a.is_active("foo"));
198        assert!(a.is_active("bar"));
199        assert!(!a.is_active("baz"));
200        assert_eq!(a.snapshot_slot("foo"), Some(0));
201    }
202
203    #[test]
204    fn test_with_overrides_empty_yields_empty() {
205        let a = ActiveFeatures::with_overrides(&[]);
206        assert!(a.is_empty());
207    }
208}