Skip to content

Commit

Permalink
go/consensus: Expose read-only state via light client interface
Browse files Browse the repository at this point in the history
Nodes configured as consensus RPC services workers now expose read-only
access to consensus state via the usual MKVS ReadSyncer interface, allowing
light clients to remotely query state while transparently verifying proofs.
  • Loading branch information
kostko committed Jul 1, 2020
1 parent 1b03f50 commit b98ccd5
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changelog/3077.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
go/consensus: Expose read-only state via light client interface

Nodes configured as consensus RPC services workers now expose read-only
access to consensus state via the usual MKVS ReadSyncer interface, allowing
light clients to remotely query state while transparently verifying proofs.
5 changes: 5 additions & 0 deletions go/consensus/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
roothash "github.com/oasisprotocol/oasis-core/go/roothash/api"
scheduler "github.com/oasisprotocol/oasis-core/go/scheduler/api"
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
mkvsNode "github.com/oasisprotocol/oasis-core/go/storage/mkvs/node"
)

const (
Expand Down Expand Up @@ -99,6 +100,8 @@ type Block struct {
Hash []byte `json:"hash"`
// Time is the second-granular consensus time.
Time time.Time `json:"time"`
// StateRoot is the Merkle root of the consensus state tree.
StateRoot mkvsNode.Root `json:"state_root"`
// Meta contains the consensus backend specific block metadata.
Meta cbor.RawMessage `json:"meta"`
}
Expand All @@ -119,6 +122,8 @@ type Status struct {
LatestHash []byte `json:"latest_hash"`
// LatestTime is the timestamp of the latest block.
LatestTime time.Time `json:"latest_time"`
// LatestStateRoot is the Merkle root of the consensus state tree.
LatestStateRoot mkvsNode.Root `json:"latest_state_root"`

// GenesisHeight is the height of the genesis block.
GenesisHeight int64 `json:"genesis_height"`
Expand Down
124 changes: 124 additions & 0 deletions go/consensus/api/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
epochtime "github.com/oasisprotocol/oasis-core/go/epochtime/api"
genesis "github.com/oasisprotocol/oasis-core/go/genesis/api"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs/syncer"
)

var (
Expand Down Expand Up @@ -48,6 +49,12 @@ var (
methodGetValidatorSet = lightServiceName.NewMethod("GetValidatorSet", int64(0))
// methodGetParameters is the GetParameters method.
methodGetParameters = lightServiceName.NewMethod("GetParameters", int64(0))
// methodStateSyncGet is the StateSyncGet method.
methodStateSyncGet = lightServiceName.NewMethod("StateSyncGet", syncer.GetRequest{})
// methodStateSyncGetPrefixes is the StateSyncGetPrefixes method.
methodStateSyncGetPrefixes = lightServiceName.NewMethod("StateSyncGetPrefixes", syncer.GetPrefixesRequest{})
// methodStateSyncIterate is the StateSyncIterate method.
methodStateSyncIterate = lightServiceName.NewMethod("StateSyncIterate", syncer.IterateRequest{})

// serviceDesc is the gRPC service descriptor.
serviceDesc = grpc.ServiceDesc{
Expand Down Expand Up @@ -121,6 +128,18 @@ var (
MethodName: methodGetParameters.ShortName(),
Handler: handlerGetParameters,
},
{
MethodName: methodStateSyncGet.ShortName(),
Handler: handlerStateSyncGet,
},
{
MethodName: methodStateSyncGetPrefixes.ShortName(),
Handler: handlerStateSyncGetPrefixes,
},
{
MethodName: methodStateSyncIterate.ShortName(),
Handler: handlerStateSyncIterate,
},
},
}
)
Expand Down Expand Up @@ -444,6 +463,75 @@ func handlerGetParameters( // nolint: golint
return interceptor(ctx, height, info, handler)
}

func handlerStateSyncGet( // nolint: golint
srv interface{},
ctx context.Context,
dec func(interface{}) error,
interceptor grpc.UnaryServerInterceptor,
) (interface{}, error) {
rq := new(syncer.GetRequest)
if err := dec(rq); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LightClientBackend).State().SyncGet(ctx, rq)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: methodStateSyncGet.FullName(),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LightClientBackend).State().SyncGet(ctx, req.(*syncer.GetRequest))
}
return interceptor(ctx, rq, info, handler)
}

func handlerStateSyncGetPrefixes( // nolint: golint
srv interface{},
ctx context.Context,
dec func(interface{}) error,
interceptor grpc.UnaryServerInterceptor,
) (interface{}, error) {
rq := new(syncer.GetPrefixesRequest)
if err := dec(rq); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LightClientBackend).State().SyncGetPrefixes(ctx, rq)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: methodStateSyncGetPrefixes.FullName(),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LightClientBackend).State().SyncGetPrefixes(ctx, req.(*syncer.GetPrefixesRequest))
}
return interceptor(ctx, rq, info, handler)
}

func handlerStateSyncIterate( // nolint: golint
srv interface{},
ctx context.Context,
dec func(interface{}) error,
interceptor grpc.UnaryServerInterceptor,
) (interface{}, error) {
rq := new(syncer.IterateRequest)
if err := dec(rq); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LightClientBackend).State().SyncIterate(ctx, rq)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: methodStateSyncIterate.FullName(),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LightClientBackend).State().SyncIterate(ctx, req.(*syncer.IterateRequest))
}
return interceptor(ctx, rq, info, handler)
}

// RegisterService registers a new client backend service with the given gRPC server.
func RegisterService(server *grpc.Server, service ClientBackend) {
server.RegisterService(&serviceDesc, service)
Expand Down Expand Up @@ -486,6 +574,42 @@ func (c *consensusLightClient) GetParameters(ctx context.Context, height int64)
return &rsp, nil
}

type stateReadSync struct {
c *consensusLightClient
}

// Implements syncer.ReadSyncer.
func (rs *stateReadSync) SyncGet(ctx context.Context, request *syncer.GetRequest) (*syncer.ProofResponse, error) {
var rsp syncer.ProofResponse
if err := rs.c.conn.Invoke(ctx, methodStateSyncGet.FullName(), request, &rsp); err != nil {
return nil, err
}
return &rsp, nil
}

// Implements syncer.ReadSyncer.
func (rs *stateReadSync) SyncGetPrefixes(ctx context.Context, request *syncer.GetPrefixesRequest) (*syncer.ProofResponse, error) {
var rsp syncer.ProofResponse
if err := rs.c.conn.Invoke(ctx, methodStateSyncGetPrefixes.FullName(), request, &rsp); err != nil {
return nil, err
}
return &rsp, nil
}

// Implements syncer.ReadSyncer.
func (rs *stateReadSync) SyncIterate(ctx context.Context, request *syncer.IterateRequest) (*syncer.ProofResponse, error) {
var rsp syncer.ProofResponse
if err := rs.c.conn.Invoke(ctx, methodStateSyncIterate.FullName(), request, &rsp); err != nil {
return nil, err
}
return &rsp, nil
}

// Implements LightClientBackend.
func (c *consensusLightClient) State() syncer.ReadSyncer {
return &stateReadSync{c}
}

type consensusClient struct {
consensusLightClient

Expand Down
10 changes: 9 additions & 1 deletion go/consensus/api/light.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package api

import "context"
import (
"context"

"github.com/oasisprotocol/oasis-core/go/storage/mkvs/syncer"
)

// LightClientBackend is the limited consensus interface used by light clients.
type LightClientBackend interface {
Expand All @@ -13,6 +17,10 @@ type LightClientBackend interface {
// GetParameters returns the consensus parameters for a specific height.
GetParameters(ctx context.Context, height int64) (*Parameters, error)

// State returns a MKVS read syncer that can be used to read consensus state from a remote node
// and verify it against the trusted local root.
State() syncer.ReadSyncer

// TODO: Move SubmitEvidence etc. from Backend.
}

Expand Down
6 changes: 3 additions & 3 deletions go/consensus/tendermint/abci/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,9 @@ func (a *ApplicationServer) EstimateGas(caller signature.PublicKey, tx *transact
return a.mux.EstimateGas(caller, tx)
}

// BlockHeight returns the last committed block height.
func (a *ApplicationServer) BlockHeight() int64 {
return a.mux.state.BlockHeight()
// State returns the application state.
func (a *ApplicationServer) State() api.ApplicationQueryState {
return a.mux.state
}

// NewApplicationServer returns a new ApplicationServer, using the provided
Expand Down
18 changes: 17 additions & 1 deletion go/consensus/tendermint/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/oasisprotocol/oasis-core/go/common/node"
consensus "github.com/oasisprotocol/oasis-core/go/consensus/api"
"github.com/oasisprotocol/oasis-core/go/consensus/tendermint/crypto"
mkvsNode "github.com/oasisprotocol/oasis-core/go/storage/mkvs/node"
)

// BackendName is the consensus backend name.
Expand Down Expand Up @@ -175,10 +176,25 @@ func NewBlock(blk *tmtypes.Block) *consensus.Block {
}
rawMeta := cbor.Marshal(meta)

var stateRoot hash.Hash
switch blk.Header.AppHash {
case nil:
stateRoot.Empty()
default:
if err := stateRoot.UnmarshalBinary(blk.Header.AppHash); err != nil {
// This should NEVER happen.
panic(err)
}
}

return &consensus.Block{
Height: blk.Header.Height,
Hash: blk.Header.Hash(),
Time: blk.Header.Time,
Meta: rawMeta,
StateRoot: mkvsNode.Root{
Version: uint64(blk.Header.Height) - 1,
Hash: stateRoot,
},
Meta: rawMeta,
}
}
6 changes: 6 additions & 0 deletions go/consensus/tendermint/full.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
tmstate "github.com/tendermint/tendermint/state"

consensusAPI "github.com/oasisprotocol/oasis-core/go/consensus/api"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs/syncer"
)

// We must use Tendermint's amino codec as some Tendermint's types are not easily unmarshallable.
Expand Down Expand Up @@ -73,3 +74,8 @@ func (t *tendermintService) GetParameters(ctx context.Context, height int64) (*c
Meta: aminoCodec.MustMarshalBinaryBare(params.ConsensusParams),
}, nil
}

// Implements LightClientBackend.
func (t *tendermintService) State() syncer.ReadSyncer {
return t.mux.State().Storage()
}
5 changes: 3 additions & 2 deletions go/consensus/tendermint/tendermint.go
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ func (t *tendermintService) GetStatus(ctx context.Context) (*consensusAPI.Status
status.LatestHeight = latestBlk.Height
status.LatestHash = latestBlk.Hash
status.LatestTime = latestBlk.Time
status.LatestStateRoot = latestBlk.StateRoot
case consensusAPI.ErrNoCommittedBlocks:
// No committed blocks yet.
default:
Expand Down Expand Up @@ -904,7 +905,7 @@ func (t *tendermintService) GetTendermintBlock(ctx context.Context, height int64
// Do not let Tendermint determine the latest height (e.g., by passing nil here) as that
// completely ignores ABCI processing so it can return a block for which local state does
// not yet exist. Use our mux notion of latest height instead.
tmHeight = t.mux.BlockHeight()
tmHeight = t.mux.State().BlockHeight()
if tmHeight == 0 {
// No committed blocks yet.
return nil, nil
Expand Down Expand Up @@ -939,7 +940,7 @@ func (t *tendermintService) GetBlockResults(height int64) (*tmrpctypes.ResultBlo
// from our mux.
var tmHeight int64
if height == consensusAPI.HeightLatest {
tmHeight = t.mux.BlockHeight()
tmHeight = t.mux.State().BlockHeight()
if tmHeight == 0 {
// No committed blocks yet.
return nil, consensusAPI.ErrNoCommittedBlocks
Expand Down
29 changes: 29 additions & 0 deletions go/consensus/tests/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
"github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/epochtime_mock"
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs"
)

const (
Expand Down Expand Up @@ -43,8 +44,13 @@ func ConsensusImplementationTests(t *testing.T, backend consensus.ClientBackend)
require.NoError(err, "GetStatus")
require.NotNil(status, "returned status should not be nil")
require.EqualValues(1, status.GenesisHeight, "genesis height must be 1")

blk, err = backend.GetBlock(ctx, status.LatestHeight)
require.NoError(err, "GetBlock")

require.EqualValues(blk.Height, status.LatestHeight, "latest block heights should match")
require.EqualValues(blk.Hash, status.LatestHash, "latest block hashes should match")
require.EqualValues(blk.StateRoot, status.LatestStateRoot, "latest state roots should match")

_, err = backend.GetTransactions(ctx, consensus.HeightLatest)
require.NoError(err, "GetTransactions")
Expand Down Expand Up @@ -99,4 +105,27 @@ func ConsensusImplementationTests(t *testing.T, backend consensus.ClientBackend)
require.NoError(err, "GetParameters")
require.Equal(params.Height, blk.Height, "returned parameters height should be correct")
require.NotNil(params.Meta, "returned parameters should contain metadata")

// We should be able to do remote state queries. Of course the state format is backend-specific
// so we simply perform some usual storage operations like fetching random keys and iterating
// through everything.
state := mkvs.NewWithRoot(backend.State(), nil, blk.StateRoot)

it := state.NewIterator(ctx)
defer it.Close()

var keys [][]byte
for it.Rewind(); it.Valid(); it.Next() {
keys = append(keys, it.Key())
}
require.NoError(it.Err(), "iterator should not return an error")
require.NotEmpty(keys, "there should be some keys in consensus state")

for _, key := range keys {
_, err = state.Get(ctx, key)
require.NoError(err, "state.Get(%X)", key)
}

err = state.PrefetchPrefixes(ctx, keys[:1], 10)
require.NoError(err, "state.PrefetchPrefixes")
}

0 comments on commit b98ccd5

Please sign in to comment.