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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub enum StorageOp {
112 Read,
114 Write,
116}
117
118impl StorageOp {
119 pub fn covers(self, op: StorageOp) -> bool {
121 match self {
122 StorageOp::Write => true,
123 StorageOp::Read => matches!(op, StorageOp::Read),
124 }
125 }
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub enum Warmth {
131 Hot { charged: StorageOp },
133 Cold { revertible: bool },
136}
137
138impl Warmth {
139 #[cfg(any(test, feature = "runtime-benchmarks"))]
141 pub(crate) fn is_cold(&self) -> bool {
142 matches!(self, Self::Cold { .. })
143 }
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub struct AccessListMetrics {
149 pub size: usize,
151 pub cold: u32,
153 pub hot: u32,
155}
156
157#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Clone)]
163pub struct AccessEntry {
164 pub slot: Slot,
166 pub address: H160,
168}
169
170#[derive(Default)]
186pub struct AccessList {
187 accessed: BTreeMap<AccessEntry, StorageOp>,
192 journal: BoundedVec<AccessEntry, ConstU32<{ MAX_ACCESS_LIST_ENTRIES as u32 }>>,
195 upgrades: BoundedVec<AccessEntry, ConstU32<{ MAX_ACCESS_LIST_ENTRIES as u32 }>>,
197 checkpoints: Vec<(usize, usize)>,
199 cold_count: u32,
202 hot_count: u32,
205}
206
207impl AccessList {
208 pub fn new() -> Self {
210 Self::default()
211 }
212
213 pub fn enter_frame(&mut self) {
219 self.checkpoints.push((self.journal.len(), self.upgrades.len()));
220 }
221
222 pub fn commit_frame(&mut self) {
231 self.checkpoints.pop().expect(
232 "A call to commit_frame must be preceded by a corresponding call to enter_frame;
233 Stack::run closes every checkpoint it opens; qed",
234 );
235 }
236
237 pub fn rollback_frame(&mut self) {
246 let (journal_checkpoint, upgrades_checkpoint) = self.checkpoints.pop().expect(
247 "A call to rollback_frame must be preceded by a corresponding call to enter_frame;
248 Stack::run closes every checkpoint it opens; qed",
249 );
250 for entry in self.journal.drain(journal_checkpoint..) {
251 self.accessed.remove(&entry);
252 }
253 for entry in self.upgrades.drain(upgrades_checkpoint..) {
254 if let Some(charged) = self.accessed.get_mut(&entry) {
256 *charged = StorageOp::Read;
257 }
258 }
259 }
260
261 pub fn peek(&self, entry: &AccessEntry) -> Warmth {
264 match self.accessed.get(entry) {
265 Some(charged) => Warmth::Hot { charged: *charged },
266 None if self.is_full() => Warmth::Cold { revertible: false },
267 None => Warmth::Cold { revertible: self.in_nested_frame() },
268 }
269 }
270
271 fn is_full(&self) -> bool {
273 self.accessed.len() >= MAX_ACCESS_LIST_ENTRIES
274 }
275
276 fn in_nested_frame(&self) -> bool {
278 !self.checkpoints.is_empty()
279 }
280
281 pub fn touch(&mut self, access_entry: AccessEntry, op: StorageOp) -> Warmth {
287 let at_cap = self.is_full();
288 match self.accessed.entry(access_entry) {
289 Entry::Occupied(mut tree_entry) => {
290 self.hot_count = self.hot_count.saturating_add(1);
291 let prev_charged = *tree_entry.get();
292 if !prev_charged.covers(op) {
293 let journaled = self.upgrades.try_push(tree_entry.key().clone());
296 debug_assert!(journaled.is_ok(), "at most one live upgrade per tracked slot");
297 if journaled.is_ok() {
298 *tree_entry.get_mut() = StorageOp::Write;
299 }
300 }
301 Warmth::Hot { charged: prev_charged }
302 },
303 Entry::Vacant(tree_entry) => {
304 self.cold_count = self.cold_count.saturating_add(1);
305 if at_cap {
306 return Warmth::Cold { revertible: false };
307 }
308 self.journal
309 .try_push(tree_entry.key().clone())
310 .expect("journal grows in lockstep with accessed and shares its bound; qed");
311 tree_entry.insert(op);
312 Warmth::Cold { revertible: self.in_nested_frame() }
313 },
314 }
315 }
316
317 pub fn metrics(&self) -> AccessListMetrics {
319 AccessListMetrics { size: self.accessed.len(), cold: self.cold_count, hot: self.hot_count }
320 }
321
322 pub fn frame_depth(&self) -> usize {
324 self.checkpoints.len()
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn nested_commit_then_parent_rollback_drops_all() {
334 let mut al = AccessList::new();
335 let (a, b, c, d) = (
336 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xA; 32]) },
337 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xB; 32]) },
338 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xC; 32]) },
339 AccessEntry { address: H160::zero(), slot: Slot::Fix([0xD; 32]) },
340 );
341
342 assert_eq!(
344 al.touch(a.clone(), StorageOp::Read),
345 Warmth::Cold { revertible: false },
346 "A: first touch cold"
347 );
348 assert!(!al.touch(a.clone(), StorageOp::Read).is_cold(), "A: second touch hot");
349
350 al.enter_frame();
351 assert_eq!(al.frame_depth(), 1);
352
353 assert_eq!(
355 al.touch(b.clone(), StorageOp::Read),
356 Warmth::Cold { revertible: true },
357 "B in F1: cold"
358 );
359 assert!(!al.touch(a.clone(), StorageOp::Read).is_cold(), "A in F1: hot via parent");
360
361 al.enter_frame();
362 assert!(al.touch(c.clone(), StorageOp::Read).is_cold(), "C in F2: cold");
363
364 al.commit_frame();
365 assert_eq!(al.frame_depth(), 1);
366 assert!(!al.peek(&c).is_cold(), "C: survives F2 commit");
367
368 assert!(al.touch(d.clone(), StorageOp::Read).is_cold(), "D in F1: cold");
369 assert_eq!(al.metrics().size, 4);
370
371 al.rollback_frame();
372 assert_eq!(al.frame_depth(), 0);
373 assert!(!al.peek(&a).is_cold(), "A: first frame, survives F1 revert");
374 assert!(al.peek(&b).is_cold(), "B: inserted by F1, rolled back");
375 assert!(al.peek(&c).is_cold(), "C: F2-committed-into-F1, gone when F1 reverts");
376 assert!(al.peek(&d).is_cold(), "D: inserted by F1, rolled back");
377
378 assert_eq!(
382 al.metrics(),
383 AccessListMetrics { size: 1, cold: 4, hot: 2 },
384 "counters must include rolled-back touches",
385 );
386 }
387
388 fn fill_to_cap(al: &mut AccessList) {
390 for i in 0..MAX_ACCESS_LIST_ENTRIES {
391 let address = H160::from_low_u64_be(i as u64);
392 let entry = AccessEntry { address, slot: Slot::Fix([0; 32]) };
393 assert!(al.touch(entry, StorageOp::Read).is_cold(), "fill entries must be new");
394 }
395 assert_eq!(al.metrics().size, MAX_ACCESS_LIST_ENTRIES, "map filled to the cap");
396 }
397
398 #[test]
399 fn touch_caps_at_max_entries() {
400 let mut al = AccessList::new();
401 fill_to_cap(&mut al);
402
403 let new_entry = AccessEntry {
404 address: H160::from_low_u64_be(MAX_ACCESS_LIST_ENTRIES as u64),
405 slot: Slot::Fix([0; 32]),
406 };
407 al.enter_frame();
408 assert_eq!(
409 al.touch(new_entry.clone(), StorageOp::Read),
410 Warmth::Cold { revertible: false },
411 "past cap: bills cold, not revertible",
412 );
413 al.commit_frame();
414 assert_eq!(al.metrics().size, MAX_ACCESS_LIST_ENTRIES, "map size stays at cap");
415 assert!(al.peek(&new_entry).is_cold(), "past-cap entry is not tracked");
416
417 assert!(
418 al.touch(new_entry, StorageOp::Read).is_cold(),
419 "past cap re-touch: still cold (not tracked)"
420 );
421
422 let existing = AccessEntry { address: H160::zero(), slot: Slot::Fix([0; 32]) };
423 assert!(
424 !al.touch(existing.clone(), StorageOp::Read).is_cold(),
425 "existing entry still hot at cap"
426 );
427
428 assert_eq!(
430 al.touch(existing.clone(), StorageOp::Write),
431 Warmth::Hot { charged: StorageOp::Read },
432 "first write at cap: was read-paid",
433 );
434 assert_eq!(
435 al.touch(existing, StorageOp::Write),
436 Warmth::Hot { charged: StorageOp::Write },
437 "write at cap: upgraded",
438 );
439
440 assert_eq!(
441 al.metrics().size,
442 MAX_ACCESS_LIST_ENTRIES,
443 "the cap holds across past-cap touches and upgrades",
444 );
445 }
446
447 #[test]
448 fn peek_does_not_mutate() {
449 let mut al = AccessList::new();
450 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([1; 32]) };
451
452 assert!(al.peek(&entry).is_cold(), "untouched entry: cold");
453 assert!(al.peek(&entry).is_cold(), "repeated query: still cold");
454 assert_eq!(
455 al.metrics(),
456 AccessListMetrics { size: 0, cold: 0, hot: 0 },
457 "peek must not bump counters",
458 );
459
460 al.touch(entry.clone(), StorageOp::Read);
461
462 let read_paid = Warmth::Hot { charged: StorageOp::Read };
463 assert_eq!(al.peek(&entry), read_paid, "peek reports the paid level");
464 assert_eq!(al.peek(&entry), read_paid, "peek must not upgrade");
465 assert_eq!(
466 al.metrics(),
467 AccessListMetrics { size: 1, cold: 1, hot: 0 },
468 "peek must not bump the hot counter",
469 );
470 }
471
472 #[test]
473 fn touches_never_downgrade_the_paid_level() {
474 let mut al = AccessList::new();
475 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([2; 32]) };
476
477 let read_paid = Warmth::Hot { charged: StorageOp::Read };
478 let write_paid = Warmth::Hot { charged: StorageOp::Write };
479
480 assert!(al.touch(entry.clone(), StorageOp::Read).is_cold(), "first read: cold");
481 assert_eq!(al.touch(entry.clone(), StorageOp::Read), read_paid, "read after read");
482 assert_eq!(
483 al.touch(entry.clone(), StorageOp::Write),
484 read_paid,
485 "first write: was read-paid"
486 );
487 assert_eq!(al.touch(entry.clone(), StorageOp::Write), write_paid, "write after write");
488 assert_eq!(al.touch(entry.clone(), StorageOp::Read), write_paid, "read after write");
489 assert_eq!(
490 al.touch(entry, StorageOp::Write),
491 write_paid,
492 "a read never downgrades the level"
493 );
494
495 let written = AccessEntry { address: H160::zero(), slot: Slot::Fix([3; 32]) };
496 assert!(al.touch(written.clone(), StorageOp::Write).is_cold(), "first write: cold");
497 assert_eq!(al.touch(written, StorageOp::Write), write_paid, "cold write starts at Write");
498 }
499
500 #[test]
501 fn peek_agrees_with_touch() {
502 fn agree(al: &mut AccessList, entry: AccessEntry, op: StorageOp, expected: Warmth) {
503 assert_eq!(al.peek(&entry), expected, "peek must report like touch");
504 assert_eq!(al.touch(entry, op), expected, "touch must report like peek");
505 }
506
507 let entry = |i: u8| AccessEntry { address: H160::zero(), slot: Slot::Fix([i; 32]) };
508 let mut al = AccessList::new();
509
510 agree(&mut al, entry(1), StorageOp::Read, Warmth::Cold { revertible: false });
511 agree(&mut al, entry(1), StorageOp::Read, Warmth::Hot { charged: StorageOp::Read });
512 agree(&mut al, entry(1), StorageOp::Write, Warmth::Hot { charged: StorageOp::Read });
513 agree(&mut al, entry(1), StorageOp::Write, Warmth::Hot { charged: StorageOp::Write });
514 agree(&mut al, entry(1), StorageOp::Read, Warmth::Hot { charged: StorageOp::Write });
515
516 al.enter_frame();
517 agree(&mut al, entry(2), StorageOp::Write, Warmth::Cold { revertible: true });
518 al.rollback_frame();
519
520 fill_to_cap(&mut al);
521
522 al.enter_frame();
523 agree(&mut al, entry(3), StorageOp::Write, Warmth::Cold { revertible: false });
525 let filled = AccessEntry { address: H160::from_low_u64_be(0), slot: Slot::Fix([0; 32]) };
527 agree(&mut al, filled.clone(), StorageOp::Write, Warmth::Hot { charged: StorageOp::Read });
528 al.rollback_frame();
529 assert_eq!(
530 al.peek(&filled),
531 Warmth::Hot { charged: StorageOp::Read },
532 "an at-cap upgrade rolls back with its frame"
533 );
534 }
535
536 #[test]
537 fn upgrade_rolls_back_with_the_reverting_frame() {
538 let mut al = AccessList::new();
539 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([9; 32]) };
540 al.touch(entry.clone(), StorageOp::Read);
541
542 al.enter_frame();
543 assert_eq!(
544 al.touch(entry.clone(), StorageOp::Write),
545 Warmth::Hot { charged: StorageOp::Read }
546 );
547 al.rollback_frame();
548 assert_eq!(
549 al.peek(&entry),
550 Warmth::Hot { charged: StorageOp::Read },
551 "the reverted frame's write was undone, so the next write pays again"
552 );
553
554 al.enter_frame();
555 al.enter_frame();
556 assert_eq!(
557 al.touch(entry.clone(), StorageOp::Write),
558 Warmth::Hot { charged: StorageOp::Read }
559 );
560 al.commit_frame();
561 assert_eq!(
562 al.peek(&entry),
563 Warmth::Hot { charged: StorageOp::Write },
564 "a committed upgrade belongs to the parent frame"
565 );
566 al.rollback_frame();
567 assert_eq!(
568 al.peek(&entry),
569 Warmth::Hot { charged: StorageOp::Read },
570 "the parent's revert drops the committed upgrade"
571 );
572 }
573
574 #[test]
575 fn upgrade_survives_a_nested_frames_rollback() {
576 let mut al = AccessList::new();
577 let upgraded = AccessEntry { address: H160::zero(), slot: Slot::Fix([7; 32]) };
578 al.touch(upgraded.clone(), StorageOp::Read);
579 al.touch(upgraded.clone(), StorageOp::Write);
580
581 al.enter_frame();
582 al.touch(AccessEntry { address: H160::zero(), slot: Slot::Fix([6; 32]) }, StorageOp::Write);
583 al.rollback_frame();
584
585 assert_eq!(
586 al.peek(&upgraded),
587 Warmth::Hot { charged: StorageOp::Write },
588 "a rollback must only drop its own frame's upgrades"
589 );
590 }
591
592 #[test]
593 fn same_frame_insert_and_upgrade_roll_back_together() {
594 let mut al = AccessList::new();
595 let entry = AccessEntry { address: H160::zero(), slot: Slot::Fix([8; 32]) };
596 al.enter_frame();
597 al.touch(entry.clone(), StorageOp::Read);
598 al.touch(entry.clone(), StorageOp::Write);
599 al.rollback_frame();
600 assert!(al.peek(&entry).is_cold(), "the entry and its upgrade are both gone");
601 }
602}