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-enforcement-interface` stays `#![no_std]` and free
17//! of the `ahash` dependency.
18//!
19//! `iter()` below is a public, iterated view consumed by a per-block
20//! readiness check and an RPC handler. Neither depends on iteration order:
21//! the RPC handler explicitly sorts its collected output by name before
22//! returning it, and the readiness check only reports the first unsupported
23//! feature name found in a local error surfaced to the operator (the
24//! execution loop halts and retries the same block; nothing about which
25//! name gets reported reaches another validator).
26
27use ahash::AHashMap;
28use rialo_feature_management_enforcement_interface::state::EnforcementState;
29
30/// Slot number alias used by the runtime view.
31///
32/// Aliased locally rather than imported from `rialo-s-clock` because the
33/// inherited Solana SDK is not the canonical source for this concept in
34/// Rialo's runtime.
35pub type Slot = u64;
36
37/// Immutable per-block snapshot of active features.
38///
39/// Built once per bank inside `execute_blob`, immediately after
40/// `update_clock`, and held under `Arc` so every consumer in the block sees
41/// the same view. There is no mid-block mutation, no lazy refresh, and no
42/// cross-block caching.
43#[derive(Clone, Debug, Default, PartialEq, Eq)]
44pub struct ActiveFeatures {
45    /// Active feature name → bank slot at snapshot time.
46    #[allow(
47        clippy::disallowed_types,
48        reason = "the only persisted AHashMap state in this file; every access is either a keyed lookup (is_active, snapshot_slot) or the iter() view, whose only two consumers do not depend on order — an RPC handler that sorts its collected output by name, and a per-block readiness check whose only output is a local diagnostic naming the first unsupported feature"
49    )]
50    active: AHashMap<String, Slot>,
51}
52
53impl ActiveFeatures {
54    /// Empty snapshot — useful at genesis and in tests.
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Build the snapshot for a bank from `EnforcementState`. Every entry
60    /// name in the state is added to the snapshot, keyed by `snapshot_slot`
61    /// (the current bank slot — informational, not the historical
62    /// activation slot, which would require on-chain state the design
63    /// intentionally avoids).
64    ///
65    /// Activation is presence-based: a feature is active iff its name is
66    /// in `state.entries()`. No clock consult.
67    ///
68    /// This is the canonical producer the bank calls inside
69    /// `recompute_active_features`. Construction lives here (not in the
70    /// caller) so the producer surface stays in this crate; `from_map` can
71    /// remain `pub(crate)`.
72    #[allow(
73        clippy::disallowed_types,
74        reason = "transient construction site: builds the map from EnforcementState::entries() and immediately hands it to from_map, which stores it as ActiveFeatures::active; see that field's reason for why order does not matter"
75    )]
76    pub fn from_enforcement_state(state: &EnforcementState, snapshot_slot: Slot) -> Self {
77        let active: AHashMap<String, Slot> = state
78            .entries()
79            .iter()
80            .map(|name| (name.clone(), snapshot_slot))
81            .collect();
82        Self::from_map(active)
83    }
84
85    /// Snapshot from a pre-built map of the **complete** active set.
86    ///
87    /// This is the canonical producer used by `Bank::recompute_active_features`:
88    /// the bank reads `EnforcementState.entries` and hands the full map
89    /// (name → current bank slot) here. A feature absent from the map is
90    /// inactive — there is no "missing-means-default" fallback. Tests that
91    /// want sugar for "mark these names as active in an otherwise empty
92    /// snapshot" should use `with_overrides` instead.
93    ///
94    /// The strict `KNOWN_FEATURES` check lives in
95    /// `Bank::can_process_block` — running the same check
96    /// here would only fire on test fixtures that legitimately use
97    /// fabricated names. The producer paths
98    /// (`Self::from_enforcement_state`, `with_overrides`) supply the names;
99    /// the readiness check is what halts a release node on an unknown one.
100    ///
101    /// `pub(crate)` so the only producer is `from_enforcement_state` /
102    /// `with_overrides` inside this crate, matching the NORTHSTAR contract
103    /// "the only producer of `Arc<ActiveFeatures>` is the per-block
104    /// recomputation inside `execute_blob`." Cross-crate construction goes
105    /// through `from_enforcement_state`, which takes already-validated
106    /// `EnforcementState` bytes.
107    #[allow(
108        clippy::disallowed_types,
109        reason = "receives the already-built map and stores it verbatim as ActiveFeatures::active; see that field's reason for why order does not matter"
110    )]
111    pub(crate) fn from_map(active: AHashMap<String, Slot>) -> Self {
112        Self { active }
113    }
114
115    /// Test affordance — marks the named features as active in an
116    /// otherwise-empty snapshot.
117    ///
118    /// All overrides share the same snapshot slot `0`. Tests that need
119    /// distinct slots per feature should build the `AHashMap` themselves
120    /// and use `from_map`. Semantics intentionally differ from `from_map`:
121    /// `with_overrides` is "start from empty, add these"; `from_map` is
122    /// "this *is* the complete active set."
123    ///
124    /// Gated on `cfg(test)` or the `dev-context-only-utils` feature so the
125    /// affordance never reaches a release binary.
126    #[cfg(any(test, feature = "dev-context-only-utils"))]
127    #[allow(
128        clippy::disallowed_types,
129        reason = "test/dev-only construction site: every override shares snapshot slot 0, so the collect() order is discarded before the map reaches from_map"
130    )]
131    pub fn with_overrides(features: &[&str]) -> Self {
132        let active: AHashMap<String, Slot> = features
133            .iter()
134            .map(|name| ((*name).to_string(), 0))
135            .collect();
136        Self::from_map(active)
137    }
138
139    /// Returns `true` if `feature` is active in this snapshot.
140    pub fn is_active(&self, feature: &str) -> bool {
141        self.active.contains_key(feature)
142    }
143
144    /// Returns the recorded snapshot slot for `feature`, if active.
145    ///
146    /// This is the bank slot at the moment the snapshot was computed, not
147    /// the historical activation slot.
148    pub fn snapshot_slot(&self, feature: &str) -> Option<Slot> {
149        self.active.get(feature).copied()
150    }
151
152    /// Number of active features in the snapshot.
153    pub fn len(&self) -> usize {
154        self.active.len()
155    }
156
157    /// `true` iff no features are active.
158    pub fn is_empty(&self) -> bool {
159        self.active.is_empty()
160    }
161
162    /// Iterator over `(feature_name, snapshot_slot)`.
163    pub fn iter(&self) -> impl Iterator<Item = (&str, Slot)> + '_ {
164        self.active.iter().map(|(k, v)| (k.as_str(), *v))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[allow(
173        clippy::disallowed_types,
174        reason = "test helper that builds a fixture map by inserting keys one at a time before handing it to from_map; order is irrelevant to the fixture, and tests that need a specific order sort explicitly (test_iter_covers_all_entries)"
175    )]
176    fn make(entries: &[(&str, Slot)]) -> ActiveFeatures {
177        let mut m = AHashMap::new();
178        for (k, v) in entries {
179            m.insert((*k).to_string(), *v);
180        }
181        ActiveFeatures::from_map(m)
182    }
183
184    #[test]
185    fn test_empty_is_empty() {
186        let a = ActiveFeatures::new();
187        assert!(a.is_empty());
188        assert_eq!(a.len(), 0);
189        assert!(!a.is_active("anything"));
190        assert_eq!(a.snapshot_slot("anything"), None);
191    }
192
193    #[test]
194    fn test_is_active_reflects_membership() {
195        let a = make(&[("foo", 42), ("bar", 100)]);
196        assert!(a.is_active("foo"));
197        assert!(a.is_active("bar"));
198        assert!(!a.is_active("baz"));
199    }
200
201    #[test]
202    fn test_snapshot_slot_returns_recorded_value() {
203        let a = make(&[("foo", 42)]);
204        assert_eq!(a.snapshot_slot("foo"), Some(42));
205        assert_eq!(a.snapshot_slot("missing"), None);
206    }
207
208    #[test]
209    fn test_iter_covers_all_entries() {
210        let a = make(&[("foo", 1), ("bar", 2)]);
211        let mut seen: Vec<(&str, Slot)> = a.iter().collect();
212        seen.sort_by_key(|(k, _)| *k);
213        assert_eq!(seen, vec![("bar", 2), ("foo", 1)]);
214    }
215
216    #[test]
217    fn test_with_overrides_marks_listed_features_active() {
218        let a = ActiveFeatures::with_overrides(&["foo", "bar"]);
219        assert!(a.is_active("foo"));
220        assert!(a.is_active("bar"));
221        assert!(!a.is_active("baz"));
222        assert_eq!(a.snapshot_slot("foo"), Some(0));
223    }
224
225    #[test]
226    fn test_with_overrides_empty_yields_empty() {
227        let a = ActiveFeatures::with_overrides(&[]);
228        assert!(a.is_empty());
229    }
230}