Skip to main content

hpsvm/
precompile_registry.rs

1//! Per-instance precompile dispatch: which program ids execute as precompiles
2//! and what runs for each of them.
3//!
4//! Agave owns precompile *execution*: [`InvokeContextCallback`] still drives
5//! the call, but instead of consulting agave's global table it consults the
6//! provider installed on the VM. A VM therefore runs the standard native set
7//! by default, nothing at all with [`NoPrecompiles`], or a custom set that
8//! shadows natives id by id.
9//!
10//! [`InvokeContextCallback`]: solana_svm_callback::InvokeContextCallback
11
12use std::{collections::HashMap, fmt, marker::PhantomData, sync::Arc};
13
14use agave_feature_set::FeatureSet;
15use solana_address::Address;
16use solana_precompile_error::PrecompileError;
17
18use crate::SvmTypesHost;
19
20/// A single precompile entry: the program id it handles plus its verifier.
21///
22/// The signature binds agave's real verify function
23/// (`fn(&[u8], &[&[u8]], &FeatureSet) -> Result<(), PrecompileError>`).
24/// There is deliberately no compute-unit or invocation-context parameter:
25/// agave hands verifiers no context either, so a CU-aware precompile is
26/// impossible on this path and the registry stays honest about it.
27pub trait Precompile<T: SvmTypesHost>: Send + Sync {
28    /// The program id this entry handles.
29    fn program_id(&self) -> Address;
30
31    /// Whether this entry is live under `feature_set`.
32    ///
33    /// The default is always-on. The native adapter overrides it with the
34    /// entry's optional agave feature gate, so gating is enforced at lookup
35    /// time exactly like agave's `get_precompile` feature check.
36    fn is_enabled(&self, _feature_set: &FeatureSet) -> bool {
37        true
38    }
39
40    /// Runs the precompile over `data` plus every instruction's data.
41    fn verify(
42        &self,
43        data: &[u8],
44        slices: &[&[u8]],
45        feature_set: &FeatureSet,
46    ) -> Result<(), PrecompileError>;
47}
48
49/// The active precompile set: program id to handler.
50///
51/// Inserting under an existing program id replaces (shadows) the previous
52/// entry. Handlers are reference-counted, so cloning a set shares them.
53pub struct PrecompileSet<T: SvmTypesHost> {
54    entries: HashMap<Address, Arc<dyn Precompile<T>>>,
55    // `fn() -> T` keeps the set `Send + Sync` for every family instead of
56    // inheriting the auto traits of `T`.
57    _marker: PhantomData<fn() -> T>,
58}
59
60impl<T: SvmTypesHost> PrecompileSet<T> {
61    /// An empty active set: nothing dispatches as a precompile.
62    pub fn new() -> Self {
63        Self { entries: HashMap::new(), _marker: PhantomData }
64    }
65
66    /// Inserts `precompile` under its program id, returning the entry it
67    /// shadows, if any.
68    pub fn insert(&mut self, precompile: Arc<dyn Precompile<T>>) -> Option<Arc<dyn Precompile<T>>> {
69        self.entries.insert(precompile.program_id(), precompile)
70    }
71
72    /// Returns the handler for `program_id`, if the set holds one.
73    pub fn get(&self, program_id: &Address) -> Option<Arc<dyn Precompile<T>>> {
74        self.entries.get(program_id).cloned()
75    }
76
77    /// Whether the set holds a handler for `program_id`.
78    pub fn contains(&self, program_id: &Address) -> bool {
79        self.entries.contains_key(program_id)
80    }
81
82    /// The number of handlers in the set.
83    pub fn len(&self) -> usize {
84        self.entries.len()
85    }
86
87    /// Whether the set holds no handlers.
88    pub fn is_empty(&self) -> bool {
89        self.entries.is_empty()
90    }
91
92    /// Every program id in the set, in no particular order.
93    pub fn program_ids(&self) -> Vec<Address> {
94        self.entries.keys().cloned().collect()
95    }
96}
97
98impl<T: SvmTypesHost> Clone for PrecompileSet<T> {
99    fn clone(&self) -> Self {
100        Self { entries: self.entries.clone(), _marker: PhantomData }
101    }
102}
103
104impl<T: SvmTypesHost> Default for PrecompileSet<T> {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl<T: SvmTypesHost> fmt::Debug for PrecompileSet<T> {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.debug_struct("PrecompileSet").field("program_ids", &self.program_ids()).finish()
113    }
114}
115
116/// Per-instance precompile dispatch, queried by program id.
117///
118/// The provider answers which program ids are live precompiles ([`get`](PrecompileProvider::get))
119/// and enumerates them ([`program_ids`](PrecompileProvider::program_ids),
120/// used for account installation). Every [`PrecompileSet`] is its own
121/// provider; [`NoPrecompiles`] and (with the `precompiles` feature)
122/// `NativePrecompiles` cover the standard shapes, and anything fancier
123/// implements this trait directly.
124pub trait PrecompileProvider<T: SvmTypesHost>: Send + Sync {
125    /// Returns the handler for `program_id`, if this provider enables one.
126    fn get(&self, program_id: &Address) -> Option<Arc<dyn Precompile<T>>>;
127
128    /// Every program id this provider enables, in no particular order.
129    fn program_ids(&self) -> Vec<Address>;
130}
131
132impl<T: SvmTypesHost> PrecompileProvider<T> for PrecompileSet<T> {
133    fn get(&self, program_id: &Address) -> Option<Arc<dyn Precompile<T>>> {
134        self.entries.get(program_id).cloned()
135    }
136
137    fn program_ids(&self) -> Vec<Address> {
138        self.entries.keys().cloned().collect()
139    }
140}
141
142/// The empty provider: no program id dispatches as a precompile.
143///
144/// This is the VM default when the `precompiles` feature is off, and the
145/// explicit opt-out when it is on.
146#[derive(Clone, Copy, Debug, Default)]
147pub struct NoPrecompiles;
148
149impl<T: SvmTypesHost> PrecompileProvider<T> for NoPrecompiles {
150    fn get(&self, _program_id: &Address) -> Option<Arc<dyn Precompile<T>>> {
151        None
152    }
153
154    fn program_ids(&self) -> Vec<Address> {
155        Vec::new()
156    }
157}
158
159/// The agave-backed default set: today's precompile behaviour as a provider.
160///
161/// Built once from agave's precompile table. Each entry keeps its optional
162/// feature gate, enforced at lookup through [`Precompile::is_enabled`], so a
163/// gated native behaves exactly as it did under agave's `get_precompile`
164/// feature check. Call [`into_set`](NativePrecompiles::into_set) to get an
165/// editable set for adding or shadowing entries.
166#[cfg(feature = "precompiles")]
167pub struct NativePrecompiles<T: SvmTypesHost> {
168    set: PrecompileSet<T>,
169}
170
171#[cfg(feature = "precompiles")]
172impl<T: SvmTypesHost> NativePrecompiles<T> {
173    /// Builds the default set from agave's precompile table.
174    pub fn new() -> Self {
175        let mut set = PrecompileSet::new();
176        for precompile in agave_precompiles::get_precompiles() {
177            set.insert(Arc::new(NativePrecompile {
178                program_id: precompile.program_id,
179                feature: precompile.feature,
180                verify_fn: precompile.verify_fn,
181            }));
182        }
183        Self { set }
184    }
185
186    /// Unpacks the default entries into an editable set.
187    ///
188    /// Insert under an existing program id to shadow that native.
189    pub fn into_set(self) -> PrecompileSet<T> {
190        self.set
191    }
192}
193
194#[cfg(feature = "precompiles")]
195impl<T: SvmTypesHost> Clone for NativePrecompiles<T> {
196    fn clone(&self) -> Self {
197        Self { set: self.set.clone() }
198    }
199}
200
201#[cfg(feature = "precompiles")]
202impl<T: SvmTypesHost> Default for NativePrecompiles<T> {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208#[cfg(feature = "precompiles")]
209impl<T: SvmTypesHost> fmt::Debug for NativePrecompiles<T> {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        f.debug_struct("NativePrecompiles").field("program_ids", &self.set.program_ids()).finish()
212    }
213}
214
215#[cfg(feature = "precompiles")]
216impl<T: SvmTypesHost> PrecompileProvider<T> for NativePrecompiles<T> {
217    fn get(&self, program_id: &Address) -> Option<Arc<dyn Precompile<T>>> {
218        self.set.entries.get(program_id).cloned()
219    }
220
221    fn program_ids(&self) -> Vec<Address> {
222        self.set.program_ids()
223    }
224}
225
226/// One agave table row as a [`Precompile`]: program id, optional feature
227/// gate, and the table's verify function.
228#[cfg(feature = "precompiles")]
229#[derive(Clone, Copy, Debug)]
230struct NativePrecompile {
231    program_id: Address,
232    feature: Option<Address>,
233    verify_fn: agave_precompiles::Verify,
234}
235
236#[cfg(feature = "precompiles")]
237impl NativePrecompile {
238    /// Whether this entry is live under `feature_set`.
239    fn enabled(&self, feature_set: &FeatureSet) -> bool {
240        // Dynamic id: each precompile carries its own optional feature, which
241        // cannot be enumerated into the fixed `TxFeatureFlags` set. Checked
242        // at lookup, mirroring agave's `get_precompile` feature check.
243        self.feature.is_none_or(|feature| feature_set.is_active(&feature))
244    }
245}
246
247#[cfg(feature = "precompiles")]
248impl<T: SvmTypesHost> Precompile<T> for NativePrecompile {
249    fn program_id(&self) -> Address {
250        self.program_id
251    }
252
253    fn is_enabled(&self, feature_set: &FeatureSet) -> bool {
254        self.enabled(feature_set)
255    }
256
257    fn verify(
258        &self,
259        data: &[u8],
260        slices: &[&[u8]],
261        feature_set: &FeatureSet,
262    ) -> Result<(), PrecompileError> {
263        // Dispatch filters through `is_enabled` first; the gate is re-checked
264        // here so a direct `verify` cannot run a feature-disabled native.
265        // A disabled entry reports the same `InvalidPublicKey` a lookup miss
266        // produces.
267        if self.enabled(feature_set) {
268            (self.verify_fn)(data, slices, feature_set)
269        } else {
270            Err(PrecompileError::InvalidPublicKey)
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::BaseSvmTypes;
279
280    #[derive(Debug)]
281    struct StubPrecompile {
282        program_id: Address,
283    }
284
285    impl<T: SvmTypesHost> Precompile<T> for StubPrecompile {
286        fn program_id(&self) -> Address {
287            self.program_id
288        }
289
290        fn verify(
291            &self,
292            _data: &[u8],
293            _slices: &[&[u8]],
294            _feature_set: &FeatureSet,
295        ) -> Result<(), PrecompileError> {
296            Ok(())
297        }
298    }
299
300    fn stub() -> Arc<dyn Precompile<BaseSvmTypes>> {
301        Arc::new(StubPrecompile { program_id: Address::new_unique() })
302    }
303
304    #[test]
305    fn new_set_is_empty() {
306        let set = PrecompileSet::<BaseSvmTypes>::new();
307        assert!(set.is_empty());
308        assert_eq!(set.len(), 0);
309        assert!(set.program_ids().is_empty());
310    }
311
312    #[test]
313    fn insert_get_contains_and_ids() {
314        let first = stub();
315        let second = stub();
316        let mut set = PrecompileSet::<BaseSvmTypes>::new();
317        assert!(set.insert(Arc::clone(&first)).is_none());
318        assert!(set.insert(Arc::clone(&second)).is_none());
319
320        assert_eq!(set.len(), 2);
321        assert!(!set.is_empty());
322        assert!(set.contains(&first.program_id()));
323        assert!(set.contains(&second.program_id()));
324        assert!(set.get(&first.program_id()).is_some());
325        assert!(set.get(&Address::new_unique()).is_none());
326
327        let mut ids = set.program_ids();
328        ids.sort();
329        let mut want = [first.program_id(), second.program_id()];
330        want.sort();
331        assert_eq!(ids, want);
332    }
333
334    #[test]
335    fn insert_under_same_id_shadows_previous_entry() {
336        let program_id = Address::new_unique();
337        let first: Arc<dyn Precompile<BaseSvmTypes>> = Arc::new(StubPrecompile { program_id });
338        let second: Arc<dyn Precompile<BaseSvmTypes>> = Arc::new(StubPrecompile { program_id });
339        let mut set = PrecompileSet::<BaseSvmTypes>::new();
340        assert!(set.insert(Arc::clone(&first)).is_none());
341
342        let shadowed = set.insert(Arc::clone(&second));
343        assert!(shadowed.is_some());
344        assert!(Arc::ptr_eq(&shadowed.unwrap(), &first));
345        assert_eq!(set.len(), 1);
346        assert!(Arc::ptr_eq(&set.get(&program_id).unwrap(), &second));
347    }
348
349    #[test]
350    fn clone_shares_handlers() {
351        let entry = stub();
352        let mut set = PrecompileSet::<BaseSvmTypes>::new();
353        set.insert(Arc::clone(&entry));
354        let cloned = set.clone();
355
356        assert!(Arc::ptr_eq(&set.get(&entry.program_id()).unwrap(), &entry));
357        assert!(Arc::ptr_eq(&cloned.get(&entry.program_id()).unwrap(), &entry));
358    }
359
360    #[test]
361    fn no_precompiles_enables_nothing() {
362        let provider = NoPrecompiles;
363        let typed: &dyn PrecompileProvider<BaseSvmTypes> = &provider;
364        assert!(typed.get(&Address::new_unique()).is_none());
365        assert!(typed.program_ids().is_empty());
366    }
367
368    #[test]
369    fn set_is_its_own_provider() {
370        let entry = stub();
371        let mut set = PrecompileSet::<BaseSvmTypes>::new();
372        set.insert(Arc::clone(&entry));
373        let provider: &dyn PrecompileProvider<BaseSvmTypes> = &set;
374
375        assert!(provider.get(&entry.program_id()).is_some());
376        assert_eq!(provider.program_ids(), [entry.program_id()]);
377    }
378
379    #[cfg(feature = "precompiles")]
380    #[test]
381    fn native_set_resolves_every_table_id() {
382        let native = NativePrecompiles::<BaseSvmTypes>::new();
383        for precompile in agave_precompiles::get_precompiles() {
384            let handler = native.get(&precompile.program_id);
385            assert!(handler.is_some(), "missing handler for {}", precompile.program_id);
386            assert!(Arc::ptr_eq(
387                &handler.unwrap(),
388                &PrecompileProvider::<BaseSvmTypes>::get(&native, &precompile.program_id).unwrap()
389            ));
390        }
391        assert!(!native.program_ids().is_empty());
392    }
393}