Skip to content

Commit

Permalink
Merge pull request #5457 from oasisprotocol/ptrus/feature/rs-round-roots
Browse files Browse the repository at this point in the history
runtime: Add roothash round roots rust state wrappers
  • Loading branch information
ptrus authored Nov 20, 2023
2 parents 983a407 + 0039ebf commit 0fe5e68
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 20 deletions.
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
2 changes: 1 addition & 1 deletion go/consensus/cometbft/apps/beacon/state/interop/interop.go
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

0 comments on commit 0fe5e68

Please sign in to comment.