Skip to main content

drive/cache/
system_contracts.rs

1use crate::error::Error;
2use arc_swap::ArcSwap;
3use dpp::data_contract::DataContract;
4use dpp::prelude::Identifier;
5use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract};
6use platform_version::version::feature_initial_protocol_versions::{
7    APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION,
8    MODERATION_CHARTERS_CONTRACT_INITIAL_PROTOCOL_VERSION,
9};
10use platform_version::version::{PlatformVersion, ProtocolVersion};
11use std::collections::BTreeMap;
12use std::sync::Arc;
13
14/// How many distinct protocol versions the cache keeps materializations for.
15///
16/// Live reads only ever ask for two: the committed protocol version — used by `check_tx`, which
17/// validates against the last committed state on its own connection and thread pool, and by
18/// ordinary block execution — and, on the first block of an upgrade, the candidate version the
19/// block is being executed at. Anything older is dead weight, so inserting a materialization
20/// for a third version drops the lowest one.
21const MAX_MEMOIZED_PROTOCOL_VERSIONS: usize = 2;
22
23/// Materializations held by [`SystemDataContracts`], keyed so that entries for one protocol
24/// version can never be reached by a read pinned to another.
25type Materializations = BTreeMap<(ProtocolVersion, SystemDataContract), Arc<DataContract>>;
26
27/// Memoized materializations of the compiled-in system data contracts.
28///
29/// `load_system_data_contract(variant, platform_version)` is a deterministic pure function of
30/// the variant and the protocol version, and its result changes across protocol versions (the
31/// DPNS `domain` document type gains its history flags at protocol version 13, for instance).
32/// This cache only avoids repeating that work — schema compilation and validation — so it holds
33/// no authoritative state: any entry may be dropped and rebuilt with a bit-identical result.
34///
35/// There is no "current" contract. Every read is pinned to the protocol version the caller is
36/// executing under, which it already carries in its [`PlatformVersion`]. That matters because
37/// materializations are loaded **speculatively**: the first block of a protocol change is
38/// executed for a candidate block that may be rejected, and the in-memory cache has no part in
39/// the grovedb rollback that follows. Keying every entry by its protocol version keeps the
40/// candidate's materializations on keys no committed-version read can reach, so a rejected
41/// candidate is inert rather than something that has to be undone.
42///
43/// Reads are lock-free and writes replace the whole map. The set of materializations only
44/// changes when a protocol version is first read from — a handful of times per protocol
45/// upgrade — while reads happen on every block and every contract fetch, so cloning a map of
46/// at most a dozen `Arc` pointers on that rare path is cheaper than making every reader
47/// contend on a lock word.
48pub struct SystemDataContracts {
49    materialized: ArcSwap<Materializations>,
50}
51
52impl Default for SystemDataContracts {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl SystemDataContracts {
59    /// Creates an empty cache. Contracts are materialized on first use.
60    pub fn new() -> Self {
61        SystemDataContracts {
62            materialized: ArcSwap::from_pointee(Materializations::new()),
63        }
64    }
65
66    /// Returns `system_contract` materialized for `platform_version`, reusing the memoized
67    /// materialization when one is present.
68    ///
69    /// The contract is materialized at the protocol version the caller is executing at, which
70    /// is what the state holds: whenever a system contract's materialization changes at a
71    /// protocol version, the first block of that change rewrites the persisted contract (see
72    /// `perform_events_on_first_block_of_protocol_change`).
73    ///
74    /// # Errors
75    /// Propagates any error from `load_system_data_contract`, notably when a contract's schema
76    /// is not expressible under `platform_version` — which is the case for contracts asked for
77    /// below their activation version.
78    pub fn load(
79        &self,
80        system_contract: SystemDataContract,
81        platform_version: &PlatformVersion,
82    ) -> Result<Arc<DataContract>, Error> {
83        let key = (platform_version.protocol_version, system_contract);
84
85        let materialized = self.materialized.load();
86        if let Some(contract) = materialized.get(&key) {
87            return Ok(Arc::clone(contract));
88        }
89        drop(materialized);
90
91        let contract = Arc::new(load_system_data_contract(
92            system_contract,
93            platform_version,
94        )?);
95
96        // Copy-on-write publish. The closure is pure and idempotent, so `rcu` re-running it
97        // under a concurrent publish is harmless; and because materialization is a pure
98        // function of the key, a racing thread that wins the swap has stored an identical
99        // contract, which is why returning our own is equivalent.
100        self.materialized.rcu(|materialized| {
101            let mut next = Materializations::clone(materialized);
102            next.insert(key, Arc::clone(&contract));
103            Self::drop_stale_protocol_versions(&mut next);
104            next
105        });
106
107        Ok(contract)
108    }
109
110    /// Returns the withdrawals contract materialized for `platform_version`.
111    pub fn load_withdrawals(
112        &self,
113        platform_version: &PlatformVersion,
114    ) -> Result<Arc<DataContract>, Error> {
115        self.load(SystemDataContract::Withdrawals, platform_version)
116    }
117
118    /// Returns the token history contract materialized for `platform_version`.
119    pub fn load_token_history(
120        &self,
121        platform_version: &PlatformVersion,
122    ) -> Result<Arc<DataContract>, Error> {
123        self.load(SystemDataContract::TokenHistory, platform_version)
124    }
125
126    /// Returns the DPNS contract materialized for `platform_version`.
127    pub fn load_dpns(
128        &self,
129        platform_version: &PlatformVersion,
130    ) -> Result<Arc<DataContract>, Error> {
131        self.load(SystemDataContract::DPNS, platform_version)
132    }
133
134    /// Returns the Dashpay contract materialized for `platform_version`.
135    pub fn load_dashpay(
136        &self,
137        platform_version: &PlatformVersion,
138    ) -> Result<Arc<DataContract>, Error> {
139        self.load(SystemDataContract::Dashpay, platform_version)
140    }
141
142    /// Returns the masternode reward shares contract materialized for `platform_version`.
143    pub fn load_masternode_reward_shares(
144        &self,
145        platform_version: &PlatformVersion,
146    ) -> Result<Arc<DataContract>, Error> {
147        self.load(SystemDataContract::MasternodeRewards, platform_version)
148    }
149
150    /// Returns the keyword search contract materialized for `platform_version`.
151    pub fn load_keyword_search(
152        &self,
153        platform_version: &PlatformVersion,
154    ) -> Result<Arc<DataContract>, Error> {
155        self.load(SystemDataContract::KeywordSearch, platform_version)
156    }
157
158    /// Returns the document history contract materialized for `platform_version`.
159    pub fn load_document_history(
160        &self,
161        platform_version: &PlatformVersion,
162    ) -> Result<Arc<DataContract>, Error> {
163        self.load(SystemDataContract::DocumentHistory, platform_version)
164    }
165
166    /// Returns the app-connect contract materialized for `platform_version`.
167    pub fn load_app_connect(
168        &self,
169        platform_version: &PlatformVersion,
170    ) -> Result<Arc<DataContract>, Error> {
171        self.load(SystemDataContract::AppConnect, platform_version)
172    }
173
174    /// Returns the moderation charters contract materialized for `platform_version`.
175    pub fn load_moderation_charters(
176        &self,
177        platform_version: &PlatformVersion,
178    ) -> Result<Arc<DataContract>, Error> {
179        self.load(SystemDataContract::ModerationCharters, platform_version)
180    }
181
182    /// Returns the system contract whose deterministic identifier matches `id`, materialized
183    /// for `platform_version`.
184    ///
185    /// Returns `None` for user contracts, for system contracts this cache does not materialize
186    /// (`WalletUtils`, which lives only in grovedb), and for system contracts that are not yet
187    /// active at `platform_version`: before activation the contract does not exist in the
188    /// state, so the lookup must fall through to the billed grovedb fetch and report it absent
189    /// exactly like a non-upgraded node would.
190    pub fn find_by_id(
191        &self,
192        id: Identifier,
193        platform_version: &PlatformVersion,
194    ) -> Result<Option<Arc<DataContract>>, Error> {
195        // Linear scan over each contract's static id. The set is small and fixed, which makes
196        // this cheaper than building and holding a map.
197        let Some(&system_contract) = SystemDataContract::ALL
198            .iter()
199            .find(|system_contract| system_contract.id() == id)
200        else {
201            return Ok(None);
202        };
203
204        // Which contracts this cache may answer for, and from which protocol version each one
205        // exists in the state. Exhaustive so that a new system contract cannot be added without
206        // deciding both. Before its activation a contract is absent from the state, so the
207        // lookup must fall through to the billed grovedb fetch and report it missing, exactly
208        // as a node that has not upgraded does — answering early would turn a billed "not
209        // found" into a free "found".
210        let activated_at_protocol_version: ProtocolVersion = match system_contract {
211            // Registered in the genesis state.
212            SystemDataContract::Withdrawals
213            | SystemDataContract::MasternodeRewards
214            | SystemDataContract::DPNS
215            | SystemDataContract::Dashpay => 1,
216            // Written to state by the transition to protocol version 9.
217            SystemDataContract::TokenHistory | SystemDataContract::KeywordSearch => 9,
218            // Written to state by the transition to protocol version 13.
219            SystemDataContract::DocumentHistory => 13,
220            // Written to state by the transition to protocol version 14.
221            SystemDataContract::AppConnect => APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION,
222            // Written to state by the transition to protocol version 14.
223            SystemDataContract::ModerationCharters => {
224                MODERATION_CHARTERS_CONTRACT_INITIAL_PROTOCOL_VERSION
225            }
226            // Never served from this cache: `WalletUtils` is only ever read from grovedb, and
227            // the reserved `FeatureFlags` slot has no implementation.
228            SystemDataContract::WalletUtils | SystemDataContract::FeatureFlags => return Ok(None),
229        };
230
231        if activated_at_protocol_version > platform_version.protocol_version {
232            return Ok(None);
233        }
234
235        self.load(system_contract, platform_version).map(Some)
236    }
237
238    /// Drops materializations for protocol versions below `protocol_version`.
239    ///
240    /// Call this when a block commits, passing the committed protocol version: from that point
241    /// every live read asks for it or higher — `check_tx` validates against the committed
242    /// state, and the next block executes at the committed version until another upgrade
243    /// proposes a candidate — so everything below is garbage. Materializations *above* it are kept: those belong to an
244    /// upgrade candidate that was proposed and will be proposed again.
245    ///
246    /// Nothing depends on this being called. Entries are reproducible, so skipping it costs
247    /// memory bounded by [`MAX_MEMOIZED_PROTOCOL_VERSIONS`], never correctness.
248    pub fn drop_versions_below(&self, protocol_version: ProtocolVersion) {
249        // Checked before publishing so that ordinary blocks, which have nothing to drop, do no
250        // work beyond a lock-free read.
251        if self
252            .materialized
253            .load()
254            .keys()
255            .all(|(memoized, _)| *memoized >= protocol_version)
256        {
257            return;
258        }
259
260        self.materialized.rcu(|materialized| {
261            let mut next = Materializations::clone(materialized);
262            next.retain(|(memoized, _), _| *memoized >= protocol_version);
263            next
264        });
265    }
266
267    /// Keeps materializations for at most [`MAX_MEMOIZED_PROTOCOL_VERSIONS`] protocol versions
268    /// by dropping the lowest ones.
269    ///
270    /// Dropping is always safe: every entry is reproducible from its key alone, so a discarded
271    /// materialization is rebuilt identically the next time it is read.
272    fn drop_stale_protocol_versions(materialized: &mut Materializations) {
273        // Keys are ordered by protocol version first, so equal versions form runs that `dedup`
274        // collapses into the ascending list of distinct versions held.
275        let mut protocol_versions: Vec<ProtocolVersion> = materialized
276            .keys()
277            .map(|(protocol_version, _)| *protocol_version)
278            .collect();
279        protocol_versions.dedup();
280
281        let Some(lowest_kept) = protocol_versions
282            .len()
283            .checked_sub(MAX_MEMOIZED_PROTOCOL_VERSIONS)
284            .and_then(|first_kept| protocol_versions.get(first_kept))
285            .copied()
286        else {
287            return;
288        };
289
290        materialized.retain(|(protocol_version, _), _| *protocol_version >= lowest_kept);
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use dpp::data_contract::accessors::v0::DataContractV0Getters;
298    use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
299
300    fn platform_version(protocol_version: ProtocolVersion) -> &'static PlatformVersion {
301        PlatformVersion::get(protocol_version).expect("expected a supported platform version")
302    }
303
304    fn dpns_history_flags(contract: &DataContract) -> (bool, bool, bool) {
305        let domain = contract
306            .document_type_for_name("domain")
307            .expect("DPNS must define the domain document type");
308
309        (
310            domain.documents_keep_transfer_history(),
311            domain.documents_keep_purchase_history(),
312            domain.documents_keep_pricing_history(),
313        )
314    }
315
316    #[test]
317    fn reloading_v13_must_not_make_dpns_v2_visible_to_v12_reads() {
318        let contracts = SystemDataContracts::new();
319
320        assert_eq!(
321            dpns_history_flags(
322                &contracts
323                    .find_by_id(SystemDataContract::DPNS.id(), platform_version(12))
324                    .expect("expected the v12 DPNS lookup to succeed")
325                    .expect("DPNS must be active at protocol v12")
326            ),
327            (false, false, false)
328        );
329
330        // Stand in for the speculative load performed on the first block of a candidate
331        // protocol change, which happens before that block is known to commit.
332        contracts
333            .load_dpns(platform_version(13))
334            .expect("speculatively materialize protocol v13 DPNS");
335
336        assert_eq!(
337            dpns_history_flags(
338                &contracts
339                    .find_by_id(SystemDataContract::DPNS.id(), platform_version(12))
340                    .expect("expected the v12 DPNS lookup to succeed")
341                    .expect("DPNS must remain available at protocol v12")
342            ),
343            (false, false, false),
344            "a speculative v13 materialization must preserve explicit v12 reads"
345        );
346        assert_eq!(
347            dpns_history_flags(
348                &contracts
349                    .load_dpns(platform_version(12))
350                    .expect("expected the v12 DPNS accessor to succeed")
351            ),
352            (false, false, false),
353            "a speculative v13 materialization must preserve v12 accessor reads"
354        );
355        assert_eq!(
356            dpns_history_flags(
357                &contracts
358                    .find_by_id(SystemDataContract::DPNS.id(), platform_version(13))
359                    .expect("expected the v13 DPNS lookup to succeed")
360                    .expect("DPNS must be active at protocol v13")
361            ),
362            (true, true, true)
363        );
364    }
365
366    #[test]
367    fn same_feature_version_must_preserve_distinct_protocol_materializations() {
368        let contracts = SystemDataContracts::new();
369        let platform_version_8 = platform_version(8);
370        let platform_version_9 = platform_version(9);
371        let expected_v8 =
372            load_system_data_contract(SystemDataContract::Withdrawals, platform_version_8)
373                .expect("load protocol v8 withdrawals");
374        let expected_v9 =
375            load_system_data_contract(SystemDataContract::Withdrawals, platform_version_9)
376                .expect("load protocol v9 withdrawals");
377        assert_ne!(
378            expected_v8, expected_v9,
379            "the regression requires distinct materialized contracts"
380        );
381
382        contracts
383            .load_withdrawals(platform_version_8)
384            .expect("materialize protocol v8 withdrawals");
385        contracts
386            .load_withdrawals(platform_version_9)
387            .expect("speculatively materialize protocol v9 withdrawals");
388
389        assert_eq!(
390            contracts
391                .find_by_id(SystemDataContract::Withdrawals.id(), platform_version_8)
392                .expect("expected the v8 withdrawals lookup to succeed")
393                .expect("withdrawals must be active at protocol v8")
394                .as_ref(),
395            &expected_v8,
396            "a v9 materialization must preserve the protocol v8 one"
397        );
398        assert_eq!(
399            contracts
400                .find_by_id(SystemDataContract::Withdrawals.id(), platform_version_9)
401                .expect("expected the v9 withdrawals lookup to succeed")
402                .expect("withdrawals must be active at protocol v9")
403                .as_ref(),
404            &expected_v9
405        );
406    }
407
408    #[test]
409    fn document_history_cache_respects_its_activation_version() {
410        let contracts = SystemDataContracts::new();
411
412        assert!(contracts
413            .find_by_id(
414                SystemDataContract::DocumentHistory.id(),
415                platform_version(12)
416            )
417            .expect("expected the pre-activation lookup to succeed")
418            .is_none());
419        assert!(contracts
420            .find_by_id(
421                SystemDataContract::DocumentHistory.id(),
422                platform_version(13)
423            )
424            .expect("expected the v13 lookup to succeed")
425            .is_some());
426    }
427
428    #[test]
429    fn should_serve_moderation_charters_only_from_its_activation_version() {
430        let contracts = SystemDataContracts::new();
431
432        assert!(contracts
433            .find_by_id(
434                SystemDataContract::ModerationCharters.id(),
435                platform_version(13)
436            )
437            .expect("expected the pre-activation lookup to succeed")
438            .is_none());
439        assert!(contracts
440            .find_by_id(
441                SystemDataContract::ModerationCharters.id(),
442                PlatformVersion::latest()
443            )
444            .expect("expected the v14 lookup to succeed")
445            .is_some());
446        assert!(contracts
447            .find_by_id(
448                SystemDataContract::ModerationCharters.id(),
449                platform_version(13)
450            )
451            .expect("an old-version lookup after materialization must succeed")
452            .is_none());
453    }
454
455    #[test]
456    fn should_serve_app_connect_only_from_its_activation_version() {
457        let contracts = SystemDataContracts::new();
458
459        assert!(contracts
460            .find_by_id(SystemDataContract::AppConnect.id(), platform_version(13))
461            .expect("expected the pre-activation lookup to succeed")
462            .is_none());
463        assert!(contracts
464            .find_by_id(
465                SystemDataContract::AppConnect.id(),
466                PlatformVersion::latest()
467            )
468            .expect("expected the v14 lookup to succeed")
469            .is_some());
470        assert!(contracts
471            .find_by_id(SystemDataContract::AppConnect.id(), platform_version(13))
472            .expect("an old-version lookup after materialization must succeed")
473            .is_none());
474    }
475
476    fn memoized_protocol_versions(contracts: &SystemDataContracts) -> Vec<ProtocolVersion> {
477        let materialized = contracts.materialized.load();
478        let mut protocol_versions: Vec<ProtocolVersion> = materialized
479            .keys()
480            .map(|(protocol_version, _)| *protocol_version)
481            .collect();
482        protocol_versions.dedup();
483        protocol_versions
484    }
485
486    /// A chain replayed from genesis crosses every protocol upgrade, so the cache must not
487    /// accumulate a materialization set per version it has ever executed at.
488    #[test]
489    fn committing_a_block_releases_the_outgoing_protocol_version() {
490        let contracts = SystemDataContracts::new();
491
492        for committed_protocol_version in 9..=14 {
493            // The block that switches protocol version executes at the candidate while
494            // `check_tx` still validates against the committed one, so both are live at once.
495            contracts
496                .load_dpns(platform_version(committed_protocol_version))
497                .expect("materialize DPNS for the block being executed");
498            assert!(
499                memoized_protocol_versions(&contracts).len() <= 2,
500                "at most the committed and candidate versions may be live"
501            );
502
503            contracts.drop_versions_below(committed_protocol_version);
504            assert_eq!(
505                memoized_protocol_versions(&contracts),
506                vec![committed_protocol_version],
507                "committing the switch must release the outgoing version"
508            );
509        }
510    }
511
512    /// The commit-time release is an optimisation, not a guarantee the bound relies on.
513    #[test]
514    fn memoization_is_bounded_to_the_live_protocol_versions() {
515        let contracts = SystemDataContracts::new();
516
517        for protocol_version in [9, 10, 11, 12, 13, 14] {
518            contracts
519                .load_dpns(platform_version(protocol_version))
520                .expect("materialize DPNS");
521        }
522
523        assert_eq!(
524            memoized_protocol_versions(&contracts),
525            vec![13, 14],
526            "only the most recent protocol versions stay memoized"
527        );
528
529        // An evicted protocol version is rebuilt identically, so eviction is not observable.
530        assert_eq!(
531            dpns_history_flags(
532                &contracts
533                    .load_dpns(platform_version(9))
534                    .expect("re-materialize protocol v9 DPNS")
535            ),
536            (false, false, false)
537        );
538    }
539}