Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime: Add roothash round roots rust state wrappers #5457

Merged
merged 2 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5457.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
runtime: Add roothash round roots state wrappers in rust
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
73 changes: 73 additions & 0 deletions go/consensus/cometbft/apps/roothash/state/interop/interop.go
Original file line number Diff line number Diff line change
@@ -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: &registry.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
}
22 changes: 11 additions & 11 deletions go/consensus/cometbft/apps/roothash/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(<runtime-id>) <round>
// 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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
57 changes: 57 additions & 0 deletions go/roothash/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
}
}
9 changes: 9 additions & 0 deletions go/storage/mkvs/interop/fixtures/consensus_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/common/key_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down
52 changes: 51 additions & 1 deletion runtime/src/consensus/roothash/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -125,6 +128,14 @@ pub struct RoundResults {
pub bad_compute_entities: Vec<PublicKey>,
}

/// 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::*;
Expand Down Expand Up @@ -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"
);
}
}
}
2 changes: 1 addition & 1 deletion runtime/src/consensus/state/beacon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/consensus/state/keymanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/consensus/state/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading