1use alloc::{
25 collections::btree_map::{BTreeMap, Entry},
26 vec::Vec,
27};
28use frame_support::BoundedVec;
29use sp_core::{ConstU32, H160};
30
31use crate::{exec::Key, limits};
32
33pub const MAX_INLINE_KEY_LEN: usize = 36;
37
38pub const MAX_ACCESS_LIST_ENTRIES: usize = 2_048;
65
66const MAX_ACCESS_LIST_ENTRY_BYTES: usize = 768;
72
73pub const MAX_ACCESS_LIST_BYTES: u32 =
75 MAX_ACCESS_LIST_ENTRIES.saturating_mul(MAX_ACCESS_LIST_ENTRY_BYTES) as u32;
76
77#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Clone)]
79pub enum Slot {
80 Fix([u8; 32]),
82 VarInline { bytes: [u8; MAX_INLINE_KEY_LEN], len: u8 },
86 VarLong(BoundedVec<u8, ConstU32<{ limits::STORAGE_KEY_BYTES }>>),
89}
90
91impl From<&Key> for Slot {
92 fn from(key: &Key) -> Self {
93 match key {
94 Key::Fix(v) => Slot::Fix(*v),
95 Key::Var(v) => {
96 let raw: &[u8] = v.as_ref();
97 if raw.len() <= MAX_INLINE_KEY_LEN {
98 let mut bytes = [0u8; MAX_INLINE_KEY_LEN];
99 bytes[..raw.len()].copy_from_slice(raw);
100 Slot::VarInline { bytes, len: raw.len() as u8 }
101 } else {
102 Slot::VarLong(v.clone())
103 }
104 },
105 }
106 }
107}
108
109#[cfg_attr(test, derive(PartialEq, Eq))]
111#[derive(Clone, Copy, Debug)]
112pub enum StorageAccessKind {
113 Persistent(Warmth),
115 Transient,
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub enum StorageOp {
122 Read,
124 Write,
126}
127
128impl StorageOp {
129 pub fn covers(self, op: StorageOp) -> bool {
131 match self {
132 StorageOp::Write => true,
133 StorageOp::Read => matches!(op, StorageOp::Read),
134 }
135 }
136}
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub enum Warmth {
141 Hot { charged: StorageOp },
143 Cold { revertible: bool },
146}
147
148impl Warmth {
149 #[cfg(any(test, feature = "runtime-benchmarks"))]
151 pub(crate) fn is_cold(&self) -> bool {
152 matches!(self, Self::Cold { .. })
153 }
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct AccessListMetrics {
159 pub size: usize,
161 pub cold: u32,
163 pub hot: u32,
165}
166
167#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Clone)]
173pub struct AccessEntry {
174 pub slot: Slot,
176 pub address: H160,
178}
179
180#[derive(Default)]
196pub struct AccessList {
197 accessed: BTreeMap<AccessEntry, StorageOp>,
202 journal: BoundedVec<AccessEntry, ConstU32<{ MAX_ACCESS_LIST_ENTRIES as u32 }>>,
205 upgrades: BoundedVec<AccessEntry, ConstU32<{ MAX_ACCESS_LIST_ENTRIES as u32 }>>,
207 checkpoints: Vec<(usize, usize)>,
209 cold_count: u32,
212 hot_count: u32,
215}
216
217impl AccessList {
218 pub fn new() -> Self {
220 Self::default()
221 }
222
223 pub fn enter_frame(&mut self) {
229 self.checkpoints.push((self.journal.len(), self.upgrades.len()));
230 }
231
232 pub fn commit_frame(&mut self) {
241 self.checkpoints.pop().expect(
242 "A call to commit_frame must be preceded by a corresponding call to enter_frame;
243 Stack::run closes every checkpoint it opens; qed",
244 );
245 }
246
247 pub fn rollback_frame(&mut self) {
256 let (journal_checkpoint, upgrades_checkpoint) = self.checkpoints.pop().expect(
257 "A call to rollback_frame must be preceded by a corresponding call to enter_frame;
258 Stack::run closes every checkpoint it opens; qed",
259 );
260 for entry in self.journal.drain(journal_checkpoint..) {
261 self.accessed.remove(&entry);
262 }
263 for entry in self.upgrades.drain(upgrades_checkpoint..) {
264 if let Some(charged) = self.accessed.get_mut(&entry) {
266 *charged = StorageOp::Read;
267 }
268 }
269 }
270
271 pub fn peek(&self, entry: &AccessEntry) -> Warmth {
274 match self.accessed.get(entry) {
275 Some(charged) => Warmth::Hot { charged: *charged },
276 None if self.is_full() => Warmth::Cold { revertible: false },
277 None => Warmth::Cold { revertible: self.in_nested_frame() },
278 }
279 }
280
281 fn is_full(&self) -> bool {
283 self.accessed.len() >= MAX_ACCESS_LIST_ENTRIES
284 }
285
286 fn in_nested_frame(&self) -> bool {
288 !self.checkpoints.is_empty()
289 }
290
291 pub fn touch(&mut self, access_entry: AccessEntry, op: StorageOp) -> Warmth {
297 let at_cap = self.is_full();
298 match self.accessed.entry(access_entry) {
299 Entry::Occupied(mut tree_entry) => {
300 self.hot_count = self.hot_count.saturating_add(1);
301 let prev_charged = *tree_entry.get();
302 if !prev_charged.covers(op) {
303 let journaled = self.upgrades.try_push(tree_entry.key().clone());
306 debug_assert!(journaled.is_ok(), "at most one live upgrade per tracked slot");
307 if journaled.is_ok() {
308 *tree_entry.get_mut() = StorageOp::Write;
309 }
310 }
311 Warmth::Hot { charged: prev_charged }
312 },
313 Entry::Vacant(tree_entry) => {
314 self.cold_count = self.cold_count.saturating_add(1);
315 if at_cap {
316 return Warmth::Cold { revertible: false };
317 }
318 self.journal
319 .try_push(tree_entry.key().clone())
320 .expect("journal grows in lockstep with accessed and shares its bound; qed");
321 tree_entry.insert(op);
322 Warmth::Cold { revertible: self.in_nested_frame() }
323 },
324 }
325 }
326
327 pub fn metrics(&self) -> AccessListMetrics {
329 AccessListMetrics { size: self.accessed.len(), cold: self.cold_count, hot: self.hot_count }
330 }
331
332 pub fn frame_depth(&self) -> usize {
334 self.checkpoints.len()
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn nested_commit_then_parent_rollback_drops_all() {
344 let mut al = AccessList::new();
345 let (a, b, c, d) = (
346 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xA; 32]) },
347 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xB; 32]) },
348 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xC; 32]) },
349 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xD; 32]) },
350 );
351
352 assert_eq!(
354 al.touch(a.clone(), StorageOp::Read),
355 Warmth::Cold { revertible: false },
356 "A: first touch cold"
357 );
358 assert!(!al.touch(a.clone(), StorageOp::Read).is_cold(), "A: second touch hot");
359
360 al.enter_frame();
361 assert_eq!(al.frame_depth(), 1);
362
363 assert_eq!(
365 al.touch(b.clone(), StorageOp::Read),
366 Warmth::Cold { revertible: true },
367 "B in F1: cold"
368 );
369 assert!(!al.touch(a.clone(), StorageOp::Read).is_cold(), "A in F1: hot via parent");
370
371 al.enter_frame();
372 assert!(al.touch(c.clone(), StorageOp::Read).is_cold(), "C in F2: cold");
373
374 al.commit_frame();
375 assert_eq!(al.frame_depth(), 1);
376 assert!(!al.peek(&c).is_cold(), "C: survives F2 commit");
377
378 assert!(al.touch(d.clone(), StorageOp::Read).is_cold(), "D in F1: cold");
379 assert_eq!(al.metrics().size, 4);
380
381 al.rollback_frame();
382 assert_eq!(al.frame_depth(), 0);
383 assert!(!al.peek(&a).is_cold(), "A: first frame, survives F1 revert");
384 assert!(al.peek(&b).is_cold(), "B: inserted by F1, rolled back");
385 assert!(al.peek(&c).is_cold(), "C: F2-committed-into-F1, gone when F1 reverts");
386 assert!(al.peek(&d).is_cold(), "D: inserted by F1, rolled back");
387
388 assert_eq!(
392 al.metrics(),
393 AccessListMetrics { size: 1, cold: 4, hot: 2 },
394 "counters must include rolled-back touches",
395 );
396 }
397
398 fn fill_to_cap(al: &mut AccessList) {
400 for i in 0..MAX_ACCESS_LIST_ENTRIES {
401 let address = H160::from_low_u64_be(i as u64);
402 let entry = AccessEntry { address, slot: Slot::Fix([0; 32]) };
403 assert!(al.touch(entry, StorageOp::Read).is_cold(), "fill entries must be new");
404 }
405 assert_eq!(al.metrics().size, MAX_ACCESS_LIST_ENTRIES, "map filled to the cap");
406 }
407
408 #[test]
409 fn touch_caps_at_max_entries() {
410 let mut al = AccessList::new();
411 fill_to_cap(&mut al);
412
413 let new_entry = AccessEntry {
414 address: H160::from_low_u64_be(MAX_ACCESS_LIST_ENTRIES as u64),
415 slot: Slot::Fix([0; 32]),
416 };
417 al.enter_frame();
418 assert_eq!(
419 al.touch(new_entry.clone(), StorageOp::Read),
420 Warmth::Cold { revertible: false },
421 "past cap: bills cold, not revertible",
422 );
423 al.commit_frame();
424 assert_eq!(al.metrics().size, MAX_ACCESS_LIST_ENTRIES, "map size stays at cap");
425 assert!(al.peek(&new_entry).is_cold(), "past-cap entry is not tracked");
426
427 assert!(
428 al.touch(new_entry, StorageOp::Read).is_cold(),
429 "past cap re-touch: still cold (not tracked)"
430 );
431
432 let existing = AccessEntry { address: H160::zero(), slot: Slot::Fix([0; 32]) };
433 assert!(
434 !al.touch(existing.clone(), StorageOp::Read).is_cold(),
435 "existing entry still hot at cap"
436 );
437
438 assert_eq!(
440 al.touch(existing.clone(), StorageOp::Write),
441 Warmth::Hot { charged: StorageOp::Read },
442 "first write at cap: was read-paid",
443 );
444 assert_eq!(
445 al.touch(existing, StorageOp::Write),
446 Warmth::Hot { charged: StorageOp::Write },
447 "write at cap: upgraded",
448 );
449
450 assert_eq!(
451 al.metrics().size,
452 MAX_ACCESS_LIST_ENTRIES,
453 "the cap holds across past-cap touches and upgrades",
454 );
455 }
456
457 #[test]
458 fn peek_does_not_mutate() {
459 let mut al = AccessList::new();
460 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([1; 32]) };
461
462 assert!(al.peek(&entry).is_cold(), "untouched entry: cold");
463 assert!(al.peek(&entry).is_cold(), "repeated query: still cold");
464 assert_eq!(
465 al.metrics(),
466 AccessListMetrics { size: 0, cold: 0, hot: 0 },
467 "peek must not bump counters",
468 );
469
470 al.touch(entry.clone(), StorageOp::Read);
471
472 let read_paid = Warmth::Hot { charged: StorageOp::Read };
473 assert_eq!(al.peek(&entry), read_paid, "peek reports the paid level");
474 assert_eq!(al.peek(&entry), read_paid, "peek must not upgrade");
475 assert_eq!(
476 al.metrics(),
477 AccessListMetrics { size: 1, cold: 1, hot: 0 },
478 "peek must not bump the hot counter",
479 );
480 }
481
482 #[test]
483 fn touches_never_downgrade_the_paid_level() {
484 let mut al = AccessList::new();
485 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([2; 32]) };
486
487 let read_paid = Warmth::Hot { charged: StorageOp::Read };
488 let write_paid = Warmth::Hot { charged: StorageOp::Write };
489
490 assert!(al.touch(entry.clone(), StorageOp::Read).is_cold(), "first read: cold");
491 assert_eq!(al.touch(entry.clone(), StorageOp::Read), read_paid, "read after read");
492 assert_eq!(
493 al.touch(entry.clone(), StorageOp::Write),
494 read_paid,
495 "first write: was read-paid"
496 );
497 assert_eq!(al.touch(entry.clone(), StorageOp::Write), write_paid, "write after write");
498 assert_eq!(al.touch(entry.clone(), StorageOp::Read), write_paid, "read after write");
499 assert_eq!(
500 al.touch(entry, StorageOp::Write),
501 write_paid,
502 "a read never downgrades the level"
503 );
504
505 let written = AccessEntry { address: H160::zero(), slot: Slot::Fix([3; 32]) };
506 assert!(al.touch(written.clone(), StorageOp::Write).is_cold(), "first write: cold");
507 assert_eq!(al.touch(written, StorageOp::Write), write_paid, "cold write starts at Write");
508 }
509
510 #[test]
511 fn peek_agrees_with_touch() {
512 fn agree(al: &mut AccessList, entry: AccessEntry, op: StorageOp, expected: Warmth) {
513 assert_eq!(al.peek(&entry), expected, "peek must report like touch");
514 assert_eq!(al.touch(entry, op), expected, "touch must report like peek");
515 }
516
517 let entry = |i: u8| AccessEntry { address: H160::zero(), slot: Slot::Fix([i; 32]) };
518 let mut al = AccessList::new();
519
520 agree(&mut al, entry(1), StorageOp::Read, Warmth::Cold { revertible: false });
521 agree(&mut al, entry(1), StorageOp::Read, Warmth::Hot { charged: StorageOp::Read });
522 agree(&mut al, entry(1), StorageOp::Write, Warmth::Hot { charged: StorageOp::Read });
523 agree(&mut al, entry(1), StorageOp::Write, Warmth::Hot { charged: StorageOp::Write });
524 agree(&mut al, entry(1), StorageOp::Read, Warmth::Hot { charged: StorageOp::Write });
525
526 al.enter_frame();
527 agree(&mut al, entry(2), StorageOp::Write, Warmth::Cold { revertible: true });
528 al.rollback_frame();
529
530 fill_to_cap(&mut al);
531
532 al.enter_frame();
533 agree(&mut al, entry(3), StorageOp::Write, Warmth::Cold { revertible: false });
535 let filled = AccessEntry { address: H160::from_low_u64_be(0), slot: Slot::Fix([0; 32]) };
537 agree(&mut al, filled.clone(), StorageOp::Write, Warmth::Hot { charged: StorageOp::Read });
538 al.rollback_frame();
539 assert_eq!(
540 al.peek(&filled),
541 Warmth::Hot { charged: StorageOp::Read },
542 "an at-cap upgrade rolls back with its frame"
543 );
544 }
545
546 #[test]
547 fn upgrade_rolls_back_with_the_reverting_frame() {
548 let mut al = AccessList::new();
549 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([9; 32]) };
550 al.touch(entry.clone(), StorageOp::Read);
551
552 al.enter_frame();
553 assert_eq!(
554 al.touch(entry.clone(), StorageOp::Write),
555 Warmth::Hot { charged: StorageOp::Read }
556 );
557 al.rollback_frame();
558 assert_eq!(
559 al.peek(&entry),
560 Warmth::Hot { charged: StorageOp::Read },
561 "the reverted frame's write was undone, so the next write pays again"
562 );
563
564 al.enter_frame();
565 al.enter_frame();
566 assert_eq!(
567 al.touch(entry.clone(), StorageOp::Write),
568 Warmth::Hot { charged: StorageOp::Read }
569 );
570 al.commit_frame();
571 assert_eq!(
572 al.peek(&entry),
573 Warmth::Hot { charged: StorageOp::Write },
574 "a committed upgrade belongs to the parent frame"
575 );
576 al.rollback_frame();
577 assert_eq!(
578 al.peek(&entry),
579 Warmth::Hot { charged: StorageOp::Read },
580 "the parent's revert drops the committed upgrade"
581 );
582 }
583
584 #[test]
585 fn upgrade_survives_a_nested_frames_rollback() {
586 let mut al = AccessList::new();
587 let upgraded = AccessEntry { address: H160::zero(), slot: Slot::Fix([7; 32]) };
588 al.touch(upgraded.clone(), StorageOp::Read);
589 al.touch(upgraded.clone(), StorageOp::Write);
590
591 al.enter_frame();
592 al.touch(AccessEntry { address: H160::zero(), slot: Slot::Fix([6; 32]) }, StorageOp::Write);
593 al.rollback_frame();
594
595 assert_eq!(
596 al.peek(&upgraded),
597 Warmth::Hot { charged: StorageOp::Write },
598 "a rollback must only drop its own frame's upgrades"
599 );
600 }
601
602 #[test]
603 fn same_frame_insert_and_upgrade_roll_back_together() {
604 let mut al = AccessList::new();
605 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([8; 32]) };
606 al.enter_frame();
607 al.touch(entry.clone(), StorageOp::Read);
608 al.touch(entry.clone(), StorageOp::Write);
609 al.rollback_frame();
610 assert!(al.peek(&entry).is_cold(), "the entry and its upgrade are both gone");
611 }
612}