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
10const MAX_MEMOIZED_PROTOCOL_VERSIONS: usize = 2;
18
19type Materializations = BTreeMap<(ProtocolVersion, SystemDataContract), Arc<DataContract>>;
22
23pub 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 pub fn new() -> Self {
57 SystemDataContracts {
58 materialized: ArcSwap::from_pointee(Materializations::new()),
59 }
60 }
61
62 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 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 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 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 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 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 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 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 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 pub fn find_by_id(
171 &self,
172 id: Identifier,
173 platform_version: &PlatformVersion,
174 ) -> Result<Option<Arc<DataContract>>, Error> {
175 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 let activated_at_protocol_version: ProtocolVersion = match system_contract {
191 SystemDataContract::Withdrawals
193 | SystemDataContract::MasternodeRewards
194 | SystemDataContract::DPNS
195 | SystemDataContract::Dashpay => 1,
196 SystemDataContract::TokenHistory | SystemDataContract::KeywordSearch => 9,
198 SystemDataContract::DocumentHistory => 13,
200 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 pub fn drop_versions_below(&self, protocol_version: ProtocolVersion) {
223 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 fn drop_stale_protocol_versions(materialized: &mut Materializations) {
247 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 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 #[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 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 #[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 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}