Skip to content

Commit a27d356

Browse files
committed
[Hot State] Include HotVacant slots in root hash
In #18390, we simply set the format of the hot state Merkle tree to `Map<StateKey, StateValue>`. This means that for `HotVacant` entries, for instance, entries that are read recently and determined to not exist in storage, we do not hash them into the tree. (And if they existed before, we remove them from the Merkle tree.) This is inaccurate. This commit introduces the `HotStateValue` struct and changes the format to `Map<StateKey, HotStateValue>`. This way, both `HotOccupied` and `HotVacant` entries are summarized into the root hash.
1 parent 7e7f2f0 commit a27d356

File tree

6 files changed

+170
-44
lines changed

6 files changed

+170
-44
lines changed

storage/aptosdb/src/state_store/state_snapshot_committer.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use aptos_storage_interface::{
2525
jmt_update_refs, state_store::state_with_summary::StateWithSummary, Result,
2626
};
2727
use aptos_types::{
28-
state_store::{state_key::StateKey, NUM_STATE_SHARDS},
28+
state_store::{hot_state::HotStateValueRef, state_key::StateKey, NUM_STATE_SHARDS},
2929
transaction::Version,
3030
};
3131
use rayon::prelude::*;
@@ -99,6 +99,7 @@ impl StateSnapshotCommitter {
9999
.map(|(v, _e)| v);
100100
let min_version = self.last_snapshot.next_version();
101101

102+
// Element format: (key_hash, Option<(value_hash, key)>)
102103
let (hot_updates, all_updates): (Vec<_>, Vec<_>) = snapshot
103104
.make_delta(&self.last_snapshot)
104105
.shards
@@ -111,8 +112,10 @@ impl StateSnapshotCommitter {
111112
if slot.is_hot() {
112113
hot_updates.push((
113114
CryptoHash::hash(&key),
114-
slot.as_state_value_opt()
115-
.map(|value| (CryptoHash::hash(value), key.clone())),
115+
Some((
116+
HotStateValueRef::from_slot(&slot).hash(),
117+
key.clone(),
118+
)),
116119
));
117120
} else {
118121
hot_updates.push((CryptoHash::hash(&key), None));

storage/aptosdb/src/state_store/tests/speculative_state_workflow.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ use aptos_storage_interface::{
2121
use aptos_types::{
2222
proof::SparseMerkleProofExt,
2323
state_store::{
24-
hot_state::LRUEntry, state_key::StateKey, state_slot::StateSlot,
25-
state_storage_usage::StateStorageUsage, state_value::StateValue, StateViewId,
26-
StateViewResult, TStateView, NUM_STATE_SHARDS,
24+
hot_state::{HotStateValue, HotStateValueRef, LRUEntry},
25+
state_key::StateKey,
26+
state_slot::StateSlot,
27+
state_storage_usage::StateStorageUsage,
28+
state_value::StateValue,
29+
StateViewId, StateViewResult, TStateView, NUM_STATE_SHARDS,
2730
},
2831
transaction::Version,
2932
write_set::{BaseStateOp, HotStateOp, WriteOp},
@@ -209,10 +212,11 @@ impl VersionState {
209212
hot_since_version: version,
210213
lru_info: LRUEntry::uninitialized(),
211214
};
215+
hot_smt_updates
216+
.push((k.hash(), Some(HotStateValueRef::from_slot(&slot).hash())));
212217
hot_state[shard_id].put(k.clone(), slot);
213-
state.remove(k);
214-
hot_smt_updates.push((k.hash(), None));
215218
smt_updates.push((k.hash(), None));
219+
state.remove(k);
216220
},
217221
Some(v) => {
218222
let slot = StateSlot::HotOccupied {
@@ -221,10 +225,11 @@ impl VersionState {
221225
hot_since_version: version,
222226
lru_info: LRUEntry::uninitialized(),
223227
};
228+
hot_smt_updates
229+
.push((k.hash(), Some(HotStateValueRef::from_slot(&slot).hash())));
224230
hot_state[shard_id].put(k.clone(), slot);
225-
state.insert(k.clone(), (version, v.clone()));
226-
hot_smt_updates.push((k.hash(), Some(v.hash())));
227231
smt_updates.push((k.hash(), Some(v.hash())));
232+
state.insert(k.clone(), (version, v.clone()));
228233
},
229234
}
230235
}
@@ -234,7 +239,7 @@ impl VersionState {
234239
let shard_id = k.get_shard_id();
235240
if let Some(slot) = hot_state[shard_id].get_mut(k) {
236241
slot.refresh(version);
237-
continue;
242+
// continue;
238243
}
239244
let slot = match state.get(k) {
240245
Some((value_version, value)) => StateSlot::HotOccupied {
@@ -248,7 +253,7 @@ impl VersionState {
248253
lru_info: LRUEntry::uninitialized(),
249254
},
250255
};
251-
hot_smt_updates.push((k.hash(), slot.as_state_value_opt().map(|v| v.hash())));
256+
hot_smt_updates.push((k.hash(), Some(HotStateValue::clone_from_slot(&slot).hash())));
252257
hot_state[shard_id].put(k.clone(), slot);
253258
}
254259

@@ -268,6 +273,11 @@ impl VersionState {
268273
let bytes = state.iter().map(|(k, v)| k.size() + v.1.size()).sum();
269274
let usage = StateStorageUsage::new(items, bytes);
270275

276+
assert_eq!(
277+
hot_state.iter().map(|x| x.len()).sum::<usize>(),
278+
hot_summary.leaves.len()
279+
);
280+
271281
Self {
272282
usage,
273283
hot_state,

storage/storage-interface/src/state_store/mod.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,18 @@ pub mod state_view;
1010
pub mod state_with_summary;
1111
pub mod versioned_state_value;
1212

13-
use aptos_types::state_store::{state_key::StateKey, state_value::StateValue, NUM_STATE_SHARDS};
14-
use std::collections::HashMap;
13+
use aptos_types::state_store::{hot_state::HotStateValue, state_key::StateKey, NUM_STATE_SHARDS};
14+
use std::collections::{HashMap, HashSet};
1515

16-
// TODO(HotState): this isn't the final form yet. For instance, `HotVacant` entries probably should
17-
// be included, but right now they are `None` in these and will be removed from the Merkle trees.
1816
#[derive(Debug)]
1917
pub(crate) struct HotStateShardUpdates {
20-
insertions: HashMap<StateKey, Option<StateValue>>,
21-
evictions: HashMap<StateKey, Option<StateValue>>,
18+
insertions: HashMap<StateKey, HotStateValue>,
19+
// TODO(HotState): only keys are needed for now, since evictions do not affect cold state.
20+
evictions: HashSet<StateKey>,
2221
}
2322

2423
impl HotStateShardUpdates {
25-
pub fn new(
26-
insertions: HashMap<StateKey, Option<StateValue>>,
27-
evictions: HashMap<StateKey, Option<StateValue>>,
28-
) -> Self {
24+
pub fn new(insertions: HashMap<StateKey, HotStateValue>, evictions: HashSet<StateKey>) -> Self {
2925
Self {
3026
insertions,
3127
evictions,

storage/storage-interface/src/state_store/state.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,20 @@ use aptos_experimental_layered_map::{LayeredMap, MapLayer};
2424
use aptos_metrics_core::TimerHelper;
2525
use aptos_types::{
2626
state_store::{
27-
state_key::StateKey, state_slot::StateSlot, state_storage_usage::StateStorageUsage,
28-
state_value::StateValue, StateViewId, NUM_STATE_SHARDS,
27+
hot_state::HotStateValue, state_key::StateKey, state_slot::StateSlot,
28+
state_storage_usage::StateStorageUsage, StateViewId, NUM_STATE_SHARDS,
2929
},
3030
transaction::Version,
3131
};
3232
use arr_macro::arr;
3333
use derive_more::Deref;
3434
use itertools::Itertools;
3535
use rayon::prelude::*;
36-
use std::{collections::HashMap, num::NonZeroUsize, sync::Arc};
36+
use std::{
37+
collections::{HashMap, HashSet},
38+
num::NonZeroUsize,
39+
sync::Arc,
40+
};
3741

3842
#[derive(Clone, Debug)]
3943
pub struct HotStateMetadata {
@@ -200,7 +204,7 @@ impl State {
200204
);
201205
let mut all_updates = per_version.iter();
202206
let mut insertions = HashMap::new();
203-
let mut evictions = HashMap::new();
207+
let mut evictions = HashSet::new();
204208
for ckpt_version in all_checkpoint_versions {
205209
for (key, update) in
206210
all_updates.take_while_ref(|(_k, u)| u.version <= *ckpt_version)
@@ -215,7 +219,7 @@ impl State {
215219
evictions.extend(lru.maybe_evict().into_iter().map(|(key, slot)| {
216220
insertions.remove(&key);
217221
assert!(slot.is_hot());
218-
(key, slot.into_state_value_opt())
222+
key
219223
}));
220224
}
221225
for (key, update) in all_updates {
@@ -264,16 +268,18 @@ impl State {
264268
)
265269
}
266270

271+
/// Applies the update the returns the `HotStateValue` that will later go into the hot state
272+
/// Merkle tree.
267273
fn apply_one_update(
268274
lru: &mut HotStateLRU,
269275
overlay: &LayeredMap<StateKey, StateSlot>,
270276
read_cache: &StateCacheShard,
271277
key: &StateKey,
272278
update: &StateUpdateRef,
273-
) -> Option<StateValue> {
279+
) -> HotStateValue {
274280
if let Some(state_value_opt) = update.state_op.as_state_value_opt() {
275281
lru.insert((*key).clone(), update.to_result_slot().unwrap());
276-
return state_value_opt.cloned();
282+
return HotStateValue::new(state_value_opt.cloned(), update.version);
277283
}
278284

279285
if let Some(mut slot) = lru.get_slot(key) {
@@ -283,14 +289,15 @@ impl State {
283289
} else {
284290
slot.to_hot(update.version)
285291
};
286-
let ret = slot_to_insert.as_state_value_opt().cloned();
292+
let ret = HotStateValue::clone_from_slot(&slot_to_insert);
287293
lru.insert((*key).clone(), slot_to_insert);
288294
ret
289295
} else {
290296
let slot = Self::expect_old_slot(overlay, read_cache, key);
291297
assert!(slot.is_cold());
292-
let ret = slot.as_state_value_opt().cloned();
293-
lru.insert((*key).clone(), slot.to_hot(update.version));
298+
let slot = slot.to_hot(update.version);
299+
let ret = HotStateValue::clone_from_slot(&slot);
300+
lru.insert((*key).clone(), slot);
294301
ret
295302
}
296303
}

storage/storage-interface/src/state_store/state_summary.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ impl StateSummary {
125125
shard
126126
.insertions
127127
.iter()
128-
.map(|(k, value_opt)| (k, value_opt.as_ref().map(|v| v.hash())))
129-
.chain(shard.evictions.keys().map(|k| (k, None)))
128+
.map(|(k, item)| (k, Some(item.hash())))
129+
.chain(shard.evictions.iter().map(|k| (k, None)))
130130
.sorted_by_key(|(k, _)| k.crypto_hash_ref())
131131
.collect_vec()
132132
})
@@ -297,23 +297,20 @@ impl<'db> ProvableStateSummary<'db> {
297297
root_depth: usize,
298298
use_hot_state: bool,
299299
) -> Result<SparseMerkleProofExt> {
300-
if rand::random::<usize>() % 10000 == 0 {
300+
// TODO(HotState): we cannot verify proof yet. In order to verify the proof, we need to
301+
// fetch and construct the corresponding `HotStateValue` for `key` at `version`, including
302+
// `hot_since_version`. However, the current in-memory hot state does not support this
303+
// query, and we might need persist hot state KV to db first.
304+
if !use_hot_state && rand::random::<usize>() % 10000 == 0 {
301305
// 1 out of 10000 times, verify the proof.
302306
let (val_opt, proof) = self
303307
.db
304308
// check the full proof
305309
.get_state_value_with_proof_by_version_ext(
306-
key,
307-
version,
308-
/* root_depth = */ 0,
309-
use_hot_state,
310+
key, version, /* root_depth = */ 0, /* use_hot_state = */ false,
310311
)?;
311312
proof.verify(
312-
if use_hot_state {
313-
self.state_summary.hot_state_summary.root_hash()
314-
} else {
315-
self.state_summary.global_state_summary.root_hash()
316-
},
313+
self.state_summary.global_state_summary.root_hash(),
317314
*key,
318315
val_opt.as_ref(),
319316
)?;

types/src/state_store/hot_state.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
// Copyright (c) Aptos Foundation
22
// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE
33

4+
use crate::{
5+
state_store::{state_slot::StateSlot, state_value::StateValue},
6+
transaction::Version,
7+
};
8+
use aptos_crypto::{
9+
hash::{CryptoHash, CryptoHasher},
10+
HashValue,
11+
};
12+
use aptos_crypto_derive::{BCSCryptoHash, CryptoHasher};
13+
use serde::{Deserialize, Serialize};
14+
415
#[derive(Clone, Debug, Eq, PartialEq)]
516
pub struct LRUEntry<K> {
617
/// The key that is slightly newer than the current entry. `None` for the newest entry.
@@ -29,3 +40,105 @@ pub trait THotStateSlot {
2940
fn set_prev(&mut self, prev: Option<Self::Key>);
3041
fn set_next(&mut self, next: Option<Self::Key>);
3142
}
43+
44+
/// `HotStateValue` is what gets hashed into the hot state Merkle tree.
45+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, BCSCryptoHash, CryptoHasher)]
46+
pub struct HotStateValue {
47+
/// `Some` means occupied and `None` means vacant.
48+
value: Option<StateValue>,
49+
hot_since_version: Version,
50+
}
51+
52+
impl HotStateValue {
53+
pub fn new(value: Option<StateValue>, hot_since_version: Version) -> Self {
54+
Self {
55+
value,
56+
hot_since_version,
57+
}
58+
}
59+
60+
pub fn clone_from_slot(slot: &StateSlot) -> Self {
61+
match slot {
62+
StateSlot::HotOccupied {
63+
value,
64+
hot_since_version,
65+
..
66+
} => Self::new(Some(value.clone()), *hot_since_version),
67+
StateSlot::HotVacant {
68+
hot_since_version, ..
69+
} => Self::new(None, *hot_since_version),
70+
_ => panic!("Must be hot slot"),
71+
}
72+
}
73+
}
74+
75+
/// A reference-based version of `HotStateValue` that avoids cloning `StateValue`.
76+
/// When hashed, it produces the same hash as the equivalent `HotStateValue`.
77+
#[derive(Serialize)]
78+
pub struct HotStateValueRef<'a> {
79+
value: Option<&'a StateValue>,
80+
hot_since_version: Version,
81+
}
82+
83+
impl<'a> HotStateValueRef<'a> {
84+
pub fn new(value: Option<&'a StateValue>, hot_since_version: Version) -> Self {
85+
Self {
86+
value,
87+
hot_since_version,
88+
}
89+
}
90+
91+
pub fn from_slot(slot: &'a StateSlot) -> Self {
92+
match slot {
93+
StateSlot::HotOccupied {
94+
value,
95+
hot_since_version,
96+
..
97+
} => Self::new(Some(value), *hot_since_version),
98+
StateSlot::HotVacant {
99+
hot_since_version, ..
100+
} => Self::new(None, *hot_since_version),
101+
_ => panic!("Must be hot slot"),
102+
}
103+
}
104+
}
105+
106+
impl CryptoHash for HotStateValueRef<'_> {
107+
type Hasher = HotStateValueHasher;
108+
109+
fn hash(&self) -> HashValue {
110+
let mut state = Self::Hasher::default();
111+
bcs::serialize_into(&mut state, &self)
112+
.expect("BCS serialization of HotStateValueRef should not fail");
113+
state.finish()
114+
}
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use crate::{
120+
state_store::{
121+
hot_state::{HotStateValue, HotStateValueRef},
122+
state_value::StateValue,
123+
},
124+
transaction::Version,
125+
};
126+
use aptos_crypto::hash::CryptoHash;
127+
use proptest::prelude::*;
128+
129+
proptest! {
130+
#[test]
131+
fn test_hot_state_value_ref_hash(
132+
state_value in any::<StateValue>(),
133+
hot_since_version in any::<Version>(),
134+
) {
135+
let owned = HotStateValue::new(Some(state_value.clone()), hot_since_version);
136+
let borrowed = HotStateValueRef::new(Some(&state_value), hot_since_version);
137+
assert_eq!(owned.hash(), borrowed.hash());
138+
139+
let owned_none = HotStateValue::new(None, hot_since_version);
140+
let borrowed_none = HotStateValueRef::new(None, hot_since_version);
141+
assert_eq!(owned_none.hash(), borrowed_none.hash());
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)