oasis_core_runtime/consensus/state/
roothash.rs

1//! Roothash state in the consensus layer.
2use std::{collections::BTreeMap, convert::TryInto};
3
4use anyhow::anyhow;
5
6use crate::{
7    common::{
8        crypto::hash::Hash,
9        key_format::{KeyFormat, KeyFormatAtom},
10        namespace::Namespace,
11    },
12    consensus::{
13        roothash::{Error, RoundResults, RoundRoots, RuntimeState},
14        state::StateError,
15    },
16    key_format,
17    storage::mkvs::ImmutableMKVS,
18};
19
20/// Consensus roothash state wrapper.
21pub struct ImmutableState<'a, T: ImmutableMKVS> {
22    mkvs: &'a T,
23}
24
25impl<'a, T: ImmutableMKVS> ImmutableState<'a, T> {
26    /// Constructs a new ImmutableMKVS.
27    pub fn new(mkvs: &'a T) -> ImmutableState<'a, T> {
28        ImmutableState { mkvs }
29    }
30}
31
32key_format!(RuntimeKeyFmt, 0x20, Hash);
33key_format!(StateRootKeyFmt, 0x25, Hash);
34key_format!(LastRoundResultsKeyFmt, 0x27, Hash);
35key_format!(PastRootsKeyFmt, 0x2a, (Hash, u64));
36
37impl<'a, T: ImmutableMKVS> ImmutableState<'a, T> {
38    /// Returns the latest runtime state.
39    pub fn runtime_state(&self, id: Namespace) -> Result<RuntimeState, Error> {
40        match self
41            .mkvs
42            .get(&RuntimeKeyFmt(Hash::digest_bytes(id.as_ref())).encode())
43        {
44            Ok(Some(b)) => cbor::from_slice_non_strict(&b)
45                .map_err(|err| StateError::Unavailable(anyhow!(err)).into()),
46            Ok(None) => Err(Error::InvalidRuntime(id)),
47            Err(err) => Err(StateError::Unavailable(anyhow!(err)).into()),
48        }
49    }
50
51    /// Returns the state root for a specific runtime.
52    pub fn state_root(&self, id: Namespace) -> Result<Hash, Error> {
53        match self
54            .mkvs
55            .get(&StateRootKeyFmt(Hash::digest_bytes(id.as_ref())).encode())
56        {
57            Ok(Some(b)) => Ok(Hash(b.try_into().map_err(|_| -> Error {
58                StateError::Unavailable(anyhow!("corrupted hash value")).into()
59            })?)),
60            Ok(None) => Err(Error::InvalidRuntime(id)),
61            Err(err) => Err(StateError::Unavailable(anyhow!(err)).into()),
62        }
63    }
64
65    /// Returns the last round results for a specific runtime.
66    pub fn last_round_results(&self, id: Namespace) -> Result<RoundResults, Error> {
67        match self
68            .mkvs
69            .get(&LastRoundResultsKeyFmt(Hash::digest_bytes(id.as_ref())).encode())
70        {
71            Ok(Some(b)) => {
72                cbor::from_slice(&b).map_err(|err| StateError::Unavailable(anyhow!(err)).into())
73            }
74            Ok(None) => Err(Error::InvalidRuntime(id)),
75            Err(err) => Err(StateError::Unavailable(anyhow!(err)).into()),
76        }
77    }
78
79    // Returns the state and I/O roots for the given runtime and round.
80    pub fn round_roots(&self, id: Namespace, round: u64) -> Result<Option<RoundRoots>, StateError> {
81        match self
82            .mkvs
83            .get(&PastRootsKeyFmt((Hash::digest_bytes(id.as_ref()), round)).encode())
84        {
85            Ok(Some(b)) => {
86                cbor::from_slice(&b).map_err(|err| StateError::Unavailable(anyhow!(err)))
87            }
88            Ok(None) => Ok(None),
89            Err(err) => Err(StateError::Unavailable(anyhow!(err))),
90        }
91    }
92
93    // Returns all past round roots for the given runtime.
94    pub fn past_round_roots(&self, id: Namespace) -> Result<BTreeMap<u64, RoundRoots>, StateError> {
95        let h = Hash::digest_bytes(id.as_ref());
96        let mut it = self.mkvs.iter();
97        it.seek(&PastRootsKeyFmt((h, Default::default())).encode_partial(1));
98
99        let mut result: BTreeMap<u64, RoundRoots> = BTreeMap::new();
100
101        for (round, value) in it.map_while(|(key, value)| {
102            PastRootsKeyFmt::decode(&key)
103                .filter(|PastRootsKeyFmt((ns, _))| ns == &h)
104                .map(|PastRootsKeyFmt((_, round))| (round, value))
105        }) {
106            result.insert(
107                round,
108                cbor::from_slice(&value).map_err(|err| StateError::Unavailable(anyhow!(err)))?,
109            );
110        }
111
112        Ok(result)
113    }
114}
115
116#[cfg(test)]
117mod test {
118    use crate::storage::mkvs::{
119        interop::{Fixture, ProtocolServer},
120        Root, RootType, Tree,
121    };
122
123    use super::*;
124    #[test]
125    fn test_roothash_state_interop() {
126        // Keep in sync with go/consensus/cometbft/apps/roothash/state/interop/interop.go.
127        // If mock consensus state changes, update the root hash bellow.
128        // See protocol server stdout for hash.
129        // To make the hash show up during tests, run "cargo test" as
130        // "cargo test -- --nocapture".
131
132        // Setup protocol server with initialized mock consensus state.
133        let server = ProtocolServer::new(Fixture::ConsensusMock.into());
134        let mock_consensus_root = Root {
135            version: 1,
136            root_type: RootType::State,
137            hash: Hash::from("8e39bf193f8a954ab8f8d7cb6388c591fd0785ea060bbd8e3752e266b54499d3"),
138            ..Default::default()
139        };
140        let mkvs = Tree::builder()
141            .with_capacity(100_000, 10_000_000)
142            .with_root(mock_consensus_root)
143            .build(server.read_sync());
144        let state = ImmutableState::new(&mkvs);
145
146        let runtime_id =
147            Namespace::from("8000000000000000000000000000000000000000000000000000000000000010");
148
149        // Test fetching runtime state.
150        let runtime_state = state
151            .runtime_state(runtime_id)
152            .expect("runtime state query should work");
153        println!("{:?}", runtime_state);
154        assert_eq!(runtime_state.runtime.id, runtime_id);
155        assert_eq!(runtime_state.suspended, false);
156        assert_eq!(runtime_state.genesis_block.header.round, 1);
157        assert_eq!(
158            runtime_state.genesis_block.header.io_root,
159            Hash::digest_bytes(format!("genesis").as_bytes())
160        );
161        assert_eq!(
162            runtime_state.genesis_block.header.state_root,
163            Hash::digest_bytes(format!("genesis").as_bytes())
164        );
165        assert_eq!(runtime_state.last_block.header.round, 10);
166        assert_eq!(
167            runtime_state.last_block.header.io_root,
168            Hash::digest_bytes(format!("io 10").as_bytes())
169        );
170        assert_eq!(
171            runtime_state.last_block.header.state_root,
172            Hash::digest_bytes(format!("state 10").as_bytes())
173        );
174        assert_eq!(runtime_state.last_block_height, 90);
175        assert_eq!(runtime_state.last_normal_round, 10);
176        assert_eq!(runtime_state.last_normal_height, 90);
177
178        // Test fetching past round roots.
179        let past_round_roots = state
180            .past_round_roots(runtime_id)
181            .expect("past round roots query should work");
182        assert_eq!(
183            10,
184            past_round_roots.len(),
185            "expected number of roots should match"
186        );
187        past_round_roots.iter().for_each(|(round, roots)| {
188            assert_eq!(
189                RoundRoots {
190                    state_root: Hash::digest_bytes(format!("state {}", round).as_bytes()),
191                    io_root: Hash::digest_bytes(format!("io {}", round).as_bytes())
192                },
193                *roots,
194                "expected roots should match"
195            );
196        });
197
198        // Test fetching latest round roots.
199        let round_roots = state
200            .round_roots(runtime_id, 100)
201            .expect("round roots query should work");
202        assert_eq!(None, round_roots, "round root should be missing");
203
204        let round_roots = state
205            .round_roots(runtime_id, 10)
206            .expect("round roots query should work");
207        assert_eq!(
208            Some(RoundRoots {
209                state_root: Hash::digest_bytes(format!("state {}", 10).as_bytes()),
210                io_root: Hash::digest_bytes(format!("io {}", 10).as_bytes())
211            }),
212            round_roots,
213            "round root should be missing"
214        );
215
216        // Test non-existing runtime.
217        let runtime_id =
218            Namespace::from("8000000000000000000000000000000000000000000000000000000000000000");
219        let past_round_roots = state
220            .past_round_roots(runtime_id)
221            .expect("past round roots query should work");
222        assert_eq!(
223            0,
224            past_round_roots.len(),
225            "there should be no roots for non-existing runtime"
226        );
227        let round_roots = state
228            .round_roots(runtime_id, 10)
229            .expect("round roots query should work");
230        assert_eq!(
231            None, round_roots,
232            "round root should be missing for non-existing runtime"
233        )
234    }
235}