diff --git a/.changelog/5457.feature.md b/.changelog/5457.feature.md new file mode 100644 index 00000000000..e3b4b1fdf7d --- /dev/null +++ b/.changelog/5457.feature.md @@ -0,0 +1 @@ +runtime: Add roothash round roots state wrappers in rust diff --git a/go/consensus/cometbft/apps/beacon/state/interop/interop.go b/go/consensus/cometbft/apps/beacon/state/interop/interop.go index 3ccb566702c..3de282669fa 100644 --- a/go/consensus/cometbft/apps/beacon/state/interop/interop.go +++ b/go/consensus/cometbft/apps/beacon/state/interop/interop.go @@ -7,7 +7,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/storage/mkvs" ) -// InitializeTestBeaconState must be keet in sync with tests in runtimes/consensus/state/beacon.rs. +// InitializeTestBeaconState must be kept in sync with tests in runtimes/consensus/state/beacon.rs. func InitializeTestBeaconState(ctx context.Context, mkvs mkvs.Tree) error { state := beaconState.NewMutableState(mkvs) diff --git a/go/consensus/cometbft/apps/roothash/state/interop/interop.go b/go/consensus/cometbft/apps/roothash/state/interop/interop.go new file mode 100644 index 00000000000..62ba567571a --- /dev/null +++ b/go/consensus/cometbft/apps/roothash/state/interop/interop.go @@ -0,0 +1,73 @@ +package interop + +import ( + "context" + "fmt" + + "github.com/oasisprotocol/oasis-core/go/common" + "github.com/oasisprotocol/oasis-core/go/common/crypto/hash" + roothashState "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/roothash/state" + registry "github.com/oasisprotocol/oasis-core/go/registry/api" + "github.com/oasisprotocol/oasis-core/go/roothash/api" + "github.com/oasisprotocol/oasis-core/go/roothash/api/block" + "github.com/oasisprotocol/oasis-core/go/storage/mkvs" +) + +// InitializeTestRoothashState must be kept in sync with tests in runtimes/consensus/state/roothash.rs. +func InitializeTestRoothashState(ctx context.Context, mkvs mkvs.Tree) error { + var runtimeID common.Namespace + if err := runtimeID.UnmarshalHex("8000000000000000000000000000000000000000000000000000000000000010"); err != nil { + return err + } + + state := roothashState.NewMutableState(mkvs) + + if err := state.SetConsensusParameters(ctx, &api.ConsensusParameters{ + MaxPastRootsStored: 100, + }); err != nil { + return err + } + + // Prepare initial runtime state. + // TODO: fill the rest if needed for interop tests in future. + runtimeState := &api.RuntimeState{ + Runtime: ®istry.Runtime{ + ID: runtimeID, + }, + Suspended: false, + GenesisBlock: &block.Block{Header: block.Header{ + Round: 1, + IORoot: hash.NewFromBytes([]byte("genesis")), + StateRoot: hash.NewFromBytes([]byte("genesis")), + }}, + LastBlock: &block.Block{Header: block.Header{ + Round: 1, + IORoot: hash.NewFromBytes([]byte("genesis")), + StateRoot: hash.NewFromBytes([]byte("genesis")), + }}, + LastBlockHeight: 1, + LastNormalRound: 1, + LastNormalHeight: 1, + } + if err := state.SetRuntimeState(ctx, runtimeState); err != nil { + return err + } + + // Save some runtime state rounds, so we fill past roots state. + for i := 0; i < 10; i++ { + runtimeState.LastBlock = &block.Block{Header: block.Header{ + Round: uint64(i + 1), + IORoot: hash.NewFromBytes([]byte(fmt.Sprintf("io %d", i+1))), + StateRoot: hash.NewFromBytes([]byte(fmt.Sprintf("state %d", i+1))), + }} + runtimeState.LastNormalRound = uint64(i + 1) + runtimeState.LastBlockHeight = int64(i * 10) + runtimeState.LastNormalHeight = int64(i * 10) + + if err := state.SetRuntimeState(ctx, runtimeState); err != nil { + return err + } + } + + return nil +} diff --git a/go/consensus/cometbft/apps/roothash/state/state.go b/go/consensus/cometbft/apps/roothash/state/state.go index 452476f023f..ba03c27d377 100644 --- a/go/consensus/cometbft/apps/roothash/state/state.go +++ b/go/consensus/cometbft/apps/roothash/state/state.go @@ -51,13 +51,13 @@ var ( // // Value is CBOR-serialized message.IncomingMessage. inMsgQueueKeyFmt = keyformat.New(0x29, keyformat.H(&common.Namespace{}), uint64(0)) - // pastRootsFmt is the key format for previous state and I/O runtime roots. + // pastRootsKeyFmt is the key format for previous state and I/O runtime roots. // // Key format is: 0x2a H() // Value is CBOR-serialized roothash.RoundRoots for that round and runtime. // The maximum number of rounds that this map stores is defined by the // roothash consensus parameters as MaxPastRootsStored. - pastRootsFmt = keyformat.New(0x2a, keyformat.H(&common.Namespace{}), uint64(0)) + pastRootsKeyFmt = keyformat.New(0x2a, keyformat.H(&common.Namespace{}), uint64(0)) ) // ImmutableState is the immutable roothash state wrapper. @@ -281,7 +281,7 @@ func (s *ImmutableState) IncomingMessageQueue(ctx context.Context, runtimeID com // // If no roots are present for the given runtime and round, nil is returned. func (s *ImmutableState) RoundRoots(ctx context.Context, runtimeID common.Namespace, round uint64) (*roothash.RoundRoots, error) { - raw, err := s.is.Get(ctx, pastRootsFmt.Encode(&runtimeID, round)) + raw, err := s.is.Get(ctx, pastRootsKeyFmt.Encode(&runtimeID, round)) if err != nil { return nil, api.UnavailableStateError(err) } @@ -312,12 +312,12 @@ func (s *ImmutableState) PastRoundRoots(ctx context.Context, runtimeID common.Na // Round -> [state, I/O] roots. ret := make(map[uint64]roothash.RoundRoots) - for it.Seek(pastRootsFmt.Encode(&runtimeID)); it.Valid(); it.Next() { + for it.Seek(pastRootsKeyFmt.Encode(&runtimeID)); it.Valid(); it.Next() { var ( rtID keyformat.PreHashed round uint64 ) - if !pastRootsFmt.Decode(it.Key(), &rtID, &round) { + if !pastRootsKeyFmt.Decode(it.Key(), &rtID, &round) { break } if rtID != hID { @@ -348,12 +348,12 @@ func (s *ImmutableState) PastRoundRootsCount(ctx context.Context, runtimeID comm hID := keyformat.PreHashed(runtimeID.Hash()) var count uint64 - for it.Seek(pastRootsFmt.Encode(&runtimeID)); it.Valid(); it.Next() { + for it.Seek(pastRootsKeyFmt.Encode(&runtimeID)); it.Valid(); it.Next() { var ( rtID keyformat.PreHashed round uint64 ) - if !pastRootsFmt.Decode(it.Key(), &rtID, &round) { + if !pastRootsKeyFmt.Decode(it.Key(), &rtID, &round) { break } if rtID != hID { @@ -417,12 +417,12 @@ func (s *MutableState) SetRuntimeState(ctx context.Context, state *roothash.Runt // Delete the oldest root to make room for the new one. if newRound >= maxStored { - if err = s.ms.Remove(ctx, pastRootsFmt.Encode(&state.Runtime.ID, newRound-maxStored)); err != nil { + if err = s.ms.Remove(ctx, pastRootsKeyFmt.Encode(&state.Runtime.ID, newRound-maxStored)); err != nil { return api.UnavailableStateError(err) } } - if err = s.ms.Insert(ctx, pastRootsFmt.Encode(&state.Runtime.ID, newRound), newRoots); err != nil { + if err = s.ms.Insert(ctx, pastRootsKeyFmt.Encode(&state.Runtime.ID, newRound), newRoots); err != nil { return api.UnavailableStateError(err) } } @@ -459,7 +459,7 @@ func (s *MutableState) ShrinkPastRoots(ctx context.Context, max uint64) error { hID := keyformat.PreHashed(id.Hash()) keysToRemove := make([][]byte, 0, numPastRootsToDelete) - for it.Seek(pastRootsFmt.Encode(&id)); it.Valid(); it.Next() { + for it.Seek(pastRootsKeyFmt.Encode(&id)); it.Valid(); it.Next() { if uint64(len(keysToRemove)) >= numPastRootsToDelete { break } @@ -468,7 +468,7 @@ func (s *MutableState) ShrinkPastRoots(ctx context.Context, max uint64) error { runtimeID keyformat.PreHashed round uint64 ) - if !pastRootsFmt.Decode(it.Key(), &runtimeID, &round) { + if !pastRootsKeyFmt.Decode(it.Key(), &runtimeID, &round) { break } if runtimeID != hID { diff --git a/go/roothash/api/api_test.go b/go/roothash/api/api_test.go index ce91bf0acb2..151dd489626 100644 --- a/go/roothash/api/api_test.go +++ b/go/roothash/api/api_test.go @@ -2,11 +2,13 @@ package api import ( "crypto/rand" + "encoding/base64" "testing" "github.com/stretchr/testify/require" "github.com/oasisprotocol/oasis-core/go/common" + "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-core/go/common/crypto/hash" memorySigner "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/memory" "github.com/oasisprotocol/oasis-core/go/consensus/api/events" @@ -668,3 +670,58 @@ func TestRuntimeIDAttribute(t *testing.T) { val2 := events.EncodeValue(&attribute) require.EqualValues(t, val, val2, "events.EncodeValue should encode correctly") } + +func TestRoundRootsSerialization(t *testing.T) { + require := require.New(t) + + for _, tc := range []struct { + rr RoundRoots + expectedBase64 string + }{ + { + rr: RoundRoots{}, + expectedBase64: "glggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }, + { + rr: RoundRoots{ + StateRoot: hash.NewFromBytes([]byte("test")), + }, + expectedBase64: "glggPTf+WENeDYcyPe5KLBsznvlU3mNxbuefV0f5TZdPkT9YIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }, + { + rr: RoundRoots{ + IORoot: hash.NewFromBytes([]byte("test")), + }, + expectedBase64: "glggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYID03/lhDXg2HMj3uSiwbM575VN5jcW7nn1dH+U2XT5E/", + }, + { + rr: RoundRoots{ + StateRoot: hash.NewFromBytes([]byte("test")), + IORoot: hash.NewFromBytes([]byte("test")), + }, + expectedBase64: "glggPTf+WENeDYcyPe5KLBsznvlU3mNxbuefV0f5TZdPkT9YID03/lhDXg2HMj3uSiwbM575VN5jcW7nn1dH+U2XT5E/", + }, + { + rr: RoundRoots{ + StateRoot: hash.NewFromBytes([]byte("test1")), + IORoot: hash.NewFromBytes([]byte("test2")), + }, + expectedBase64: "glggC4+lzfqNgLxCHLxwDp+Bf5PLLb0DILrUZWwF+lp6Z/NYIJ3seczGUDFDvmAEdVCeep6Xsn8XRosTKWpu9wZ3mQRq", + }, + { + rr: RoundRoots{ + StateRoot: hash.NewFromBytes([]byte("test2")), + IORoot: hash.NewFromBytes([]byte("test1")), + }, + expectedBase64: "glggnex5zMZQMUO+YAR1UJ56npeyfxdGixMpam73BneZBGpYIAuPpc36jYC8Qhy8cA6fgX+Tyy29AyC61GVsBfpaemfz", + }, + } { + enc := cbor.Marshal(tc.rr) + require.Equal(tc.expectedBase64, base64.StdEncoding.EncodeToString(enc), "serialization should match") + + var dec RoundRoots + err := cbor.Unmarshal(enc, &dec) + require.NoError(err, "Unmarshal") + require.EqualValues(tc.rr, dec, "Runtime serialization should round-trip") + } +} diff --git a/go/storage/mkvs/interop/fixtures/consensus_mock.go b/go/storage/mkvs/interop/fixtures/consensus_mock.go index 5557bab1a04..5b044286ae1 100644 --- a/go/storage/mkvs/interop/fixtures/consensus_mock.go +++ b/go/storage/mkvs/interop/fixtures/consensus_mock.go @@ -3,11 +3,14 @@ package fixtures import ( "context" "fmt" + "time" "github.com/oasisprotocol/oasis-core/go/common" + "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api" beaconInterop "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/beacon/state/interop" keymanagerInterop "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/keymanager/state/interop" registryInterop "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/registry/state/interop" + roothashInterop "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/roothash/state/interop" stakingInterop "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/staking/state/interop" storage "github.com/oasisprotocol/oasis-core/go/storage/api" "github.com/oasisprotocol/oasis-core/go/storage/mkvs" @@ -32,6 +35,9 @@ func (c *consensusMock) Populate(ctx context.Context, ndb db.NodeDB) (*node.Root Version: 1, } + // Use a dummy ABCI InitChain context, as SetConsensusParameters methods require a specific ABCI context. + ctx = api.NewContext(ctx, api.ContextInitChain, time.Time{}, nil, nil, nil, 0, nil, 0) + mkvsTree := mkvs.New(nil, ndb, node.RootTypeState, mkvs.WithoutWriteLog()) if err = stakingInterop.InitializeTestStakingState(ctx, mkvsTree); err != nil { return nil, fmt.Errorf("consensus-mock: failed to initialize staking state: %w", err) @@ -45,6 +51,9 @@ func (c *consensusMock) Populate(ctx context.Context, ndb db.NodeDB) (*node.Root if err = keymanagerInterop.InitializeTestKeyManagerState(ctx, mkvsTree); err != nil { return nil, fmt.Errorf("consensus-mock: failed to initialize key manager state: %w", err) } + if err = roothashInterop.InitializeTestRoothashState(ctx, mkvsTree); err != nil { + return nil, fmt.Errorf("consensus-mock: failed to initialize roothash state: %w", err) + } _, testRoot.Hash, err = mkvsTree.Commit(ctx, common.Namespace{}, 1) if err != nil { return nil, fmt.Errorf("consensus-mock: failed to committ tree: %w", err) diff --git a/runtime/src/common/key_format.rs b/runtime/src/common/key_format.rs index a4d7438ea05..e7de698b8a0 100644 --- a/runtime/src/common/key_format.rs +++ b/runtime/src/common/key_format.rs @@ -187,7 +187,7 @@ impl KeyFormatAtom for Tuple { } } -/// Define a KeyFormat from KeyFromatAtom and a prefix. +/// Define a KeyFormat from KeyFormatAtom and a prefix. /// /// # Examples /// diff --git a/runtime/src/consensus/roothash/mod.rs b/runtime/src/consensus/roothash/mod.rs index e82e684ef3d..8159a4f58eb 100644 --- a/runtime/src/consensus/roothash/mod.rs +++ b/runtime/src/consensus/roothash/mod.rs @@ -7,7 +7,10 @@ use thiserror::Error; use crate::{ - common::{crypto::signature::PublicKey, namespace::Namespace}, + common::{ + crypto::{hash::Hash, signature::PublicKey}, + namespace::Namespace, + }, consensus::state::StateError, }; @@ -125,6 +128,14 @@ pub struct RoundResults { pub bad_compute_entities: Vec, } +/// Per-round state and I/O roots that are stored in consensus state. +#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, cbor::Encode, cbor::Decode)] +#[cbor(as_array)] +pub struct RoundRoots { + pub state_root: Hash, + pub io_root: Hash, +} + #[cfg(test)] mod tests { use super::*; @@ -165,4 +176,43 @@ mod tests { assert_eq!(dec, rr, "decoded results should match the expected value"); } } + + #[test] + fn test_consistent_round_roots() { + let tcs = vec![ + ("glggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", RoundRoots::default()), + ("glggPTf+WENeDYcyPe5KLBsznvlU3mNxbuefV0f5TZdPkT9YIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", RoundRoots { + state_root: Hash::digest_bytes(b"test"), + ..Default::default() + }), + ("glggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYID03/lhDXg2HMj3uSiwbM575VN5jcW7nn1dH+U2XT5E/", RoundRoots { + io_root: Hash::digest_bytes(b"test"), + ..Default::default() + }), + ("glggPTf+WENeDYcyPe5KLBsznvlU3mNxbuefV0f5TZdPkT9YID03/lhDXg2HMj3uSiwbM575VN5jcW7nn1dH+U2XT5E/", + RoundRoots { + state_root: Hash::digest_bytes(b"test"), + io_root: Hash::digest_bytes(b"test"), + }), + ("glggC4+lzfqNgLxCHLxwDp+Bf5PLLb0DILrUZWwF+lp6Z/NYIJ3seczGUDFDvmAEdVCeep6Xsn8XRosTKWpu9wZ3mQRq", + RoundRoots { + state_root: Hash::digest_bytes(b"test1"), + io_root: Hash::digest_bytes(b"test2"), + }), + ("glggnex5zMZQMUO+YAR1UJ56npeyfxdGixMpam73BneZBGpYIAuPpc36jYC8Qhy8cA6fgX+Tyy29AyC61GVsBfpaemfz", + RoundRoots { + state_root: Hash::digest_bytes(b"test2"), + io_root: Hash::digest_bytes(b"test1"), + }), + ]; + + for (encoded_base64, rr) in tcs { + let dec: RoundRoots = cbor::from_slice(&base64::decode(encoded_base64).unwrap()) + .expect("round roots should deserialize correctly"); + assert_eq!( + dec, rr, + "decoded round roots should match the expected value" + ); + } + } } diff --git a/runtime/src/consensus/state/beacon.rs b/runtime/src/consensus/state/beacon.rs index d381a54f012..b590c898b14 100644 --- a/runtime/src/consensus/state/beacon.rs +++ b/runtime/src/consensus/state/beacon.rs @@ -154,7 +154,7 @@ mod test { let mock_consensus_root = Root { version: 1, root_type: RootType::State, - hash: Hash::from("123d46d530ebb004f6de9da7e1f41f7acde10b824e79ca8e718651dab2047c23"), + hash: Hash::from("f637a80b24e3ffaaf3de0da96f1dfd94d0a135348f40006d578d557d70d5fa42"), ..Default::default() }; let mkvs = Tree::builder() diff --git a/runtime/src/consensus/state/keymanager.rs b/runtime/src/consensus/state/keymanager.rs index 14a02032369..72d27b80348 100644 --- a/runtime/src/consensus/state/keymanager.rs +++ b/runtime/src/consensus/state/keymanager.rs @@ -166,7 +166,7 @@ mod test { let mock_consensus_root = Root { version: 1, root_type: RootType::State, - hash: Hash::from("123d46d530ebb004f6de9da7e1f41f7acde10b824e79ca8e718651dab2047c23"), + hash: Hash::from("f637a80b24e3ffaaf3de0da96f1dfd94d0a135348f40006d578d557d70d5fa42"), ..Default::default() }; let mkvs = Tree::builder() diff --git a/runtime/src/consensus/state/registry.rs b/runtime/src/consensus/state/registry.rs index c77181b017f..d259cd257c1 100644 --- a/runtime/src/consensus/state/registry.rs +++ b/runtime/src/consensus/state/registry.rs @@ -131,7 +131,7 @@ mod test { let mock_consensus_root = Root { version: 1, root_type: RootType::State, - hash: Hash::from("123d46d530ebb004f6de9da7e1f41f7acde10b824e79ca8e718651dab2047c23"), + hash: Hash::from("f637a80b24e3ffaaf3de0da96f1dfd94d0a135348f40006d578d557d70d5fa42"), ..Default::default() }; let mkvs = Tree::builder() diff --git a/runtime/src/consensus/state/roothash.rs b/runtime/src/consensus/state/roothash.rs index 73d6495fdab..2a212b8736d 100644 --- a/runtime/src/consensus/state/roothash.rs +++ b/runtime/src/consensus/state/roothash.rs @@ -1,5 +1,5 @@ //! Roothash state in the consensus layer. -use std::convert::TryInto; +use std::{collections::BTreeMap, convert::TryInto}; use anyhow::anyhow; @@ -10,7 +10,7 @@ use crate::{ namespace::Namespace, }, consensus::{ - roothash::{Error, RoundResults}, + roothash::{Error, RoundResults, RoundRoots}, state::StateError, }, key_format, @@ -31,6 +31,7 @@ impl<'a, T: ImmutableMKVS> ImmutableState<'a, T> { key_format!(StateRootKeyFmt, 0x25, Hash); key_format!(LastRoundResultsKeyFmt, 0x27, Hash); +key_format!(PastRootsKeyFmt, 0x2a, (Hash, u64)); impl<'a, T: ImmutableMKVS> ImmutableState<'a, T> { /// Returns the state root for a specific runtime. @@ -60,4 +61,135 @@ impl<'a, T: ImmutableMKVS> ImmutableState<'a, T> { Err(err) => Err(StateError::Unavailable(anyhow!(err)).into()), } } + + // Returns the state and I/O roots for the given runtime and round. + pub fn round_roots(&self, id: Namespace, round: u64) -> Result, Error> { + match self + .mkvs + .get(&PastRootsKeyFmt((Hash::digest_bytes(id.as_ref()), round)).encode()) + { + Ok(Some(b)) => { + cbor::from_slice(&b).map_err(|err| StateError::Unavailable(anyhow!(err)).into()) + } + Ok(None) => Ok(None), + Err(err) => Err(StateError::Unavailable(anyhow!(err)).into()), + } + } + + // Returns all past round roots for the given runtime. + pub fn past_round_roots(&self, id: Namespace) -> Result, Error> { + let h = Hash::digest_bytes(id.as_ref()); + let mut it = self.mkvs.iter(); + it.seek(&PastRootsKeyFmt((h, Default::default())).encode_partial(1)); + + let mut result: BTreeMap = BTreeMap::new(); + + for (round, value) in it.map_while(|(key, value)| { + PastRootsKeyFmt::decode(&key) + .filter(|PastRootsKeyFmt((ns, _))| ns == &h) + .map(|PastRootsKeyFmt((_, round))| (round, value)) + }) { + result.insert( + round, + cbor::from_slice(&value).map_err(|err| StateError::Unavailable(anyhow!(err)))?, + ); + } + + Ok(result) + } +} + +#[cfg(test)] +mod test { + use crate::{ + common::crypto::hash::Hash, + storage::mkvs::{ + interop::{Fixture, ProtocolServer}, + Root, RootType, Tree, + }, + }; + + use super::*; + #[test] + fn test_roothash_state_interop() { + // Keep in sync with go/consensus/cometbft/apps/roothash/state/interop/interop.go. + // If mock consensus state changes, update the root hash bellow. + // See protocol server stdout for hash. + // To make the hash show up during tests, run "cargo test" as + // "cargo test -- --nocapture". + + // Setup protocol server with initialized mock consensus state. + let server = ProtocolServer::new(Fixture::ConsensusMock.into()); + let mock_consensus_root = Root { + version: 1, + root_type: RootType::State, + hash: Hash::from("f637a80b24e3ffaaf3de0da96f1dfd94d0a135348f40006d578d557d70d5fa42"), + ..Default::default() + }; + let mkvs = Tree::builder() + .with_capacity(100_000, 10_000_000) + .with_root(mock_consensus_root) + .build(server.read_sync()); + let state = ImmutableState::new(&mkvs); + + let runtime_id = + Namespace::from("8000000000000000000000000000000000000000000000000000000000000010"); + + // Test fetching past round roots. + let past_round_roots = state + .past_round_roots(runtime_id) + .expect("past round roots query should work"); + assert_eq!( + 10, + past_round_roots.len(), + "expected number of roots should match" + ); + past_round_roots.iter().for_each(|(round, roots)| { + assert_eq!( + RoundRoots { + state_root: Hash::digest_bytes(format!("state {}", round).as_bytes()), + io_root: Hash::digest_bytes(format!("io {}", round).as_bytes()) + }, + *roots, + "expected roots should match" + ); + }); + + // Test fetching latest round roots. + let round_roots = state + .round_roots(runtime_id, 100) + .expect("round roots query should work"); + assert_eq!(None, round_roots, "round root should be missing"); + + let round_roots = state + .round_roots(runtime_id, 10) + .expect("round roots query should work"); + assert_eq!( + Some(RoundRoots { + state_root: Hash::digest_bytes(format!("state {}", 10).as_bytes()), + io_root: Hash::digest_bytes(format!("io {}", 10).as_bytes()) + }), + round_roots, + "round root should be missing" + ); + + // Test non-existing runtime. + let runtime_id = + Namespace::from("8000000000000000000000000000000000000000000000000000000000000000"); + let past_round_roots = state + .past_round_roots(runtime_id) + .expect("past round roots query should work"); + assert_eq!( + 0, + past_round_roots.len(), + "there should be no roots for non-existing runtime" + ); + let round_roots = state + .round_roots(runtime_id, 10) + .expect("round roots query should work"); + assert_eq!( + None, round_roots, + "round root should be missing for non-existing runtime" + ) + } } diff --git a/runtime/src/consensus/state/staking.rs b/runtime/src/consensus/state/staking.rs index 659b38629e0..8728cba9730 100644 --- a/runtime/src/consensus/state/staking.rs +++ b/runtime/src/consensus/state/staking.rs @@ -221,7 +221,7 @@ mod test { let mock_consensus_root = Root { version: 1, root_type: RootType::State, - hash: Hash::from("123d46d530ebb004f6de9da7e1f41f7acde10b824e79ca8e718651dab2047c23"), + hash: Hash::from("f637a80b24e3ffaaf3de0da96f1dfd94d0a135348f40006d578d557d70d5fa42"), ..Default::default() }; let mkvs = Tree::builder()