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