diff --git a/.changelog/3782.breaking.md b/.changelog/3782.breaking.md new file mode 100644 index 00000000000..465ddf76ea8 --- /dev/null +++ b/.changelog/3782.breaking.md @@ -0,0 +1,5 @@ +go/consensus: Store runtime state roots in a single key + +That makes it easier to construct proofs starting at the consensus state root +that prove what the state root of a specific runtime is. You can then chain +these proofs to prove something about runtime state. diff --git a/go/consensus/tendermint/apps/roothash/state/state.go b/go/consensus/tendermint/apps/roothash/state/state.go index dbab4eab575..fc3dac0e48a 100644 --- a/go/consensus/tendermint/apps/roothash/state/state.go +++ b/go/consensus/tendermint/apps/roothash/state/state.go @@ -34,6 +34,14 @@ var ( // // Key format is: 0x24 evidenceKeyFmt = keyformat.New(0x24, keyformat.H(&common.Namespace{}), uint64(0), &hash.Hash{}) + // stateRootKeyFmt is the key format used for runtime state roots. + // + // Value is the runtime's latest state root. + stateRootKeyFmt = keyformat.New(0x25, keyformat.H(&common.Namespace{})) + // ioRootKeyFmt is the key format used for runtime I/O roots. + // + // Value is the runtime's latest I/O root. + ioRootKeyFmt = keyformat.New(0x26, keyformat.H(&common.Namespace{})) cborTrue = cbor.Marshal(true) ) @@ -114,6 +122,32 @@ func (s *ImmutableState) RuntimeState(ctx context.Context, id common.Namespace) return &state, nil } +func (s *ImmutableState) getRoot(ctx context.Context, id common.Namespace, kf *keyformat.KeyFormat) (hash.Hash, error) { + raw, err := s.is.Get(ctx, kf.Encode(&id)) + if err != nil { + return hash.Hash{}, api.UnavailableStateError(err) + } + if raw == nil { + return hash.Hash{}, roothash.ErrInvalidRuntime + } + + var h hash.Hash + if err = h.UnmarshalBinary(raw); err != nil { + return hash.Hash{}, api.UnavailableStateError(err) + } + return h, nil +} + +// StateRoot returns the state root for a specific runtime. +func (s *ImmutableState) StateRoot(ctx context.Context, id common.Namespace) (hash.Hash, error) { + return s.getRoot(ctx, id, stateRootKeyFmt) +} + +// IORoot returns the state root for a specific runtime. +func (s *ImmutableState) IORoot(ctx context.Context, id common.Namespace) (hash.Hash, error) { + return s.getRoot(ctx, id, ioRootKeyFmt) +} + // Runtimes returns the list of all roothash runtime states. func (s *ImmutableState) Runtimes(ctx context.Context) ([]*roothash.RuntimeState, error) { it := s.is.NewIterator(ctx) @@ -193,8 +227,22 @@ func NewMutableState(tree mkvs.KeyValueTree) *MutableState { // SetRuntimeState sets a runtime's roothash state. func (s *MutableState) SetRuntimeState(ctx context.Context, state *roothash.RuntimeState) error { - err := s.ms.Insert(ctx, runtimeKeyFmt.Encode(&state.Runtime.ID), cbor.Marshal(state)) - return api.UnavailableStateError(err) + if err := s.ms.Insert(ctx, runtimeKeyFmt.Encode(&state.Runtime.ID), cbor.Marshal(state)); err != nil { + return api.UnavailableStateError(err) + } + + // Store the current state and I/O roots separately to make them easier to retrieve when + // constructing proofs of runtime state. + stateRoot, _ := state.CurrentBlock.Header.StateRoot.MarshalBinary() + ioRoot, _ := state.CurrentBlock.Header.IORoot.MarshalBinary() + + if err := s.ms.Insert(ctx, stateRootKeyFmt.Encode(&state.Runtime.ID), stateRoot); err != nil { + return api.UnavailableStateError(err) + } + if err := s.ms.Insert(ctx, ioRootKeyFmt.Encode(&state.Runtime.ID), ioRoot); err != nil { + return api.UnavailableStateError(err) + } + return nil } // SetConsensusParameters sets roothash consensus parameters. diff --git a/go/consensus/tendermint/apps/roothash/state/state_test.go b/go/consensus/tendermint/apps/roothash/state/state_test.go index 3525bb8b569..a7d0f188e00 100644 --- a/go/consensus/tendermint/apps/roothash/state/state_test.go +++ b/go/consensus/tendermint/apps/roothash/state/state_test.go @@ -8,7 +8,9 @@ import ( "github.com/oasisprotocol/oasis-core/go/common" abciAPI "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/api" + 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" ) func TestEvidence(t *testing.T) { @@ -111,3 +113,41 @@ func TestEvidence(t *testing.T) { require.NoError(err, "EvidenceHashExists") require.True(b, "Not expired evidence hash should still exist") } + +func TestSeparateRuntimeRoots(t *testing.T) { + require := require.New(t) + + now := time.Unix(1580461674, 0) + appState := abciAPI.NewMockApplicationState(&abciAPI.MockApplicationStateConfig{}) + ctx := appState.NewContext(abciAPI.ContextBeginBlock, now) + defer ctx.Close() + + st := NewMutableState(ctx.State()) + + var runtime registry.Runtime + err := runtime.ID.UnmarshalHex("8000000000000000000000000000000000000000000000000000000000000000") + require.NoError(err, "UnmarshalHex") + + blk := block.NewGenesisBlock(runtime.ID, 0) + err = blk.Header.StateRoot.UnmarshalHex("0000000000000000000000000000000000000000000000000000000000000001") + require.NoError(err, "UnmarshalHex") + err = blk.Header.IORoot.UnmarshalHex("0000000000000000000000000000000000000000000000000000000000000002") + require.NoError(err, "UnmarshalHex") + err = st.SetRuntimeState(ctx, &api.RuntimeState{ + Runtime: &runtime, + GenesisBlock: blk, + CurrentBlock: blk, + CurrentBlockHeight: 1, + LastNormalRound: 0, + LastNormalHeight: 1, + }) + require.NoError(err, "SetRuntimeState") + + stateRoot, err := st.StateRoot(ctx, runtime.ID) + require.NoError(err, "StateRoot") + require.EqualValues(blk.Header.StateRoot, stateRoot) + + ioRoot, err := st.IORoot(ctx, runtime.ID) + require.NoError(err, "IORoot") + require.EqualValues(blk.Header.IORoot, ioRoot) +}