From a2f59b96dd7ed67367c8f8d398484f3eb9605784 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Tue, 5 May 2020 11:35:11 +0200 Subject: [PATCH] go/consensus: Add basic API for supporting light consensus clients --- .changelog/2440.feature.1.md | 1 + go/consensus/api/api.go | 10 +- go/consensus/api/grpc.go | 188 +++++++++++++++++++++++--- go/consensus/api/light.go | 44 ++++++ go/consensus/tendermint/api/api.go | 2 + go/consensus/tendermint/full.go | 75 ++++++++++ go/consensus/tendermint/tendermint.go | 52 +++++-- go/consensus/tests/tester.go | 16 +++ go/go.mod | 2 +- 9 files changed, 356 insertions(+), 34 deletions(-) create mode 100644 .changelog/2440.feature.1.md create mode 100644 go/consensus/api/light.go create mode 100644 go/consensus/tendermint/full.go diff --git a/.changelog/2440.feature.1.md b/.changelog/2440.feature.1.md new file mode 100644 index 00000000000..31aa0ef8842 --- /dev/null +++ b/.changelog/2440.feature.1.md @@ -0,0 +1 @@ +go/consensus: Add basic API for supporting light consensus clients diff --git a/go/consensus/api/api.go b/go/consensus/api/api.go index 9818a7287cb..1060196646f 100644 --- a/go/consensus/api/api.go +++ b/go/consensus/api/api.go @@ -4,6 +4,7 @@ package api import ( "context" + "time" beacon "github.com/oasislabs/oasis-core/go/beacon/api" "github.com/oasislabs/oasis-core/go/common/cbor" @@ -42,9 +43,10 @@ var ( ErrVersionNotFound = errors.New(moduleName, 3, "consensus: version not found") ) -// ClientBackend is a limited consensus interface used by clients that -// connect to the local node. +// ClientBackend is a limited consensus interface used by clients that connect to the local full +// node. This is separate from light clients which use the LightClientBackend interface. type ClientBackend interface { + LightClientBackend TransactionAuthHandler // SubmitTx submits a signed consensus transaction. @@ -87,6 +89,10 @@ type ClientBackend interface { type Block struct { // Height contains the block height. Height int64 `json:"height"` + // Hash contains the block header hash. + Hash []byte `json:"hash"` + // Time is the second-granular consensus time. + Time time.Time `json:"time"` // Meta contains the consensus backend specific block metadata. Meta cbor.RawMessage `json:"meta"` } diff --git a/go/consensus/api/grpc.go b/go/consensus/api/grpc.go index 03387d86ca7..61b9d4d0566 100644 --- a/go/consensus/api/grpc.go +++ b/go/consensus/api/grpc.go @@ -15,6 +15,8 @@ import ( var ( // serviceName is the gRPC service name. serviceName = cmnGrpc.NewServiceName("Consensus") + // lightServiceName is the gRPC service name for the light consensus interface. + lightServiceName = cmnGrpc.NewServiceName("ConsensusLight") // methodSubmitTx is the SubmitTx method. methodSubmitTx = serviceName.NewMethod("SubmitTx", transaction.SignedTransaction{}) @@ -36,10 +38,17 @@ var ( // methodWatchBlocks is the WatchBlocks method. methodWatchBlocks = serviceName.NewMethod("WatchBlocks", nil) + // methodGetSignedHeader is the GetSignedHeader method. + methodGetSignedHeader = lightServiceName.NewMethod("GetSignedHeader", int64(0)) + // methodGetValidatorSet is the GetValidatorSet method. + methodGetValidatorSet = lightServiceName.NewMethod("GetValidatorSet", int64(0)) + // methodGetParameters is the GetParameters method. + methodGetParameters = lightServiceName.NewMethod("GetParameters", int64(0)) + // serviceDesc is the gRPC service descriptor. serviceDesc = grpc.ServiceDesc{ ServiceName: string(serviceName), - HandlerType: (*Backend)(nil), + HandlerType: (*ClientBackend)(nil), Methods: []grpc.MethodDesc{ { MethodName: methodSubmitTx.ShortName(), @@ -82,6 +91,26 @@ var ( }, }, } + + // lightServiceDesc is the gRPC service descriptor for the light consensus service. + lightServiceDesc = grpc.ServiceDesc{ + ServiceName: string(lightServiceName), + HandlerType: (*LightClientBackend)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: methodGetSignedHeader.ShortName(), + Handler: handlerGetSignedHeader, + }, + { + MethodName: methodGetValidatorSet.ShortName(), + Handler: handlerGetValidatorSet, + }, + { + MethodName: methodGetParameters.ShortName(), + Handler: handlerGetParameters, + }, + }, + } ) func handlerSubmitTx( // nolint: golint @@ -95,14 +124,14 @@ func handlerSubmitTx( // nolint: golint return nil, err } if interceptor == nil { - return nil, srv.(Backend).SubmitTx(ctx, rq) + return nil, srv.(ClientBackend).SubmitTx(ctx, rq) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodSubmitTx.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return nil, srv.(Backend).SubmitTx(ctx, req.(*transaction.SignedTransaction)) + return nil, srv.(ClientBackend).SubmitTx(ctx, req.(*transaction.SignedTransaction)) } return interceptor(ctx, rq, info, handler) } @@ -118,14 +147,14 @@ func handlerStateToGenesis( // nolint: golint return nil, err } if interceptor == nil { - return srv.(Backend).StateToGenesis(ctx, height) + return srv.(ClientBackend).StateToGenesis(ctx, height) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodStateToGenesis.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(Backend).StateToGenesis(ctx, req.(int64)) + return srv.(ClientBackend).StateToGenesis(ctx, req.(int64)) } return interceptor(ctx, height, info, handler) } @@ -141,14 +170,14 @@ func handlerEstimateGas( // nolint: golint return nil, err } if interceptor == nil { - return srv.(Backend).EstimateGas(ctx, rq) + return srv.(ClientBackend).EstimateGas(ctx, rq) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodEstimateGas.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(Backend).EstimateGas(ctx, req.(*EstimateGasRequest)) + return srv.(ClientBackend).EstimateGas(ctx, req.(*EstimateGasRequest)) } return interceptor(ctx, rq, info, handler) } @@ -164,14 +193,14 @@ func handlerGetSignerNonce( // nolint: golint return nil, err } if interceptor == nil { - return srv.(Backend).GetSignerNonce(ctx, rq) + return srv.(ClientBackend).GetSignerNonce(ctx, rq) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodGetSignerNonce.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(Backend).GetSignerNonce(ctx, req.(*GetSignerNonceRequest)) + return srv.(ClientBackend).GetSignerNonce(ctx, req.(*GetSignerNonceRequest)) } return interceptor(ctx, rq, info, handler) } @@ -187,14 +216,14 @@ func handlerGetEpoch( // nolint: golint return nil, err } if interceptor == nil { - return srv.(Backend).GetEpoch(ctx, height) + return srv.(ClientBackend).GetEpoch(ctx, height) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodGetEpoch.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(Backend).GetEpoch(ctx, req.(int64)) + return srv.(ClientBackend).GetEpoch(ctx, req.(int64)) } return interceptor(ctx, height, info, handler) } @@ -210,14 +239,14 @@ func handlerWaitEpoch( // nolint: golint return nil, err } if interceptor == nil { - return nil, srv.(Backend).WaitEpoch(ctx, epoch) + return nil, srv.(ClientBackend).WaitEpoch(ctx, epoch) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodWaitEpoch.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return nil, srv.(Backend).WaitEpoch(ctx, req.(epochtime.EpochTime)) + return nil, srv.(ClientBackend).WaitEpoch(ctx, req.(epochtime.EpochTime)) } return interceptor(ctx, epoch, info, handler) } @@ -233,14 +262,14 @@ func handlerGetBlock( // nolint: golint return nil, err } if interceptor == nil { - return srv.(Backend).GetBlock(ctx, height) + return srv.(ClientBackend).GetBlock(ctx, height) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodGetBlock.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(Backend).GetBlock(ctx, req.(int64)) + return srv.(ClientBackend).GetBlock(ctx, req.(int64)) } return interceptor(ctx, height, info, handler) } @@ -256,14 +285,14 @@ func handlerGetTransactions( // nolint: golint return nil, err } if interceptor == nil { - return srv.(Backend).GetTransactions(ctx, height) + return srv.(ClientBackend).GetTransactions(ctx, height) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: methodGetTransactions.FullName(), } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(Backend).GetTransactions(ctx, req.(int64)) + return srv.(ClientBackend).GetTransactions(ctx, req.(int64)) } return interceptor(ctx, height, info, handler) } @@ -274,7 +303,7 @@ func handlerWatchBlocks(srv interface{}, stream grpc.ServerStream) error { } ctx := stream.Context() - ch, sub, err := srv.(Backend).WatchBlocks(ctx) + ch, sub, err := srv.(ClientBackend).WatchBlocks(ctx) if err != nil { return err } @@ -296,13 +325,120 @@ func handlerWatchBlocks(srv interface{}, stream grpc.ServerStream) error { } } -// RegisterService registers a new consensus backend service with the -// given gRPC server. -func RegisterService(server *grpc.Server, service Backend) { +func handlerGetSignedHeader( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + var height int64 + if err := dec(&height); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightClientBackend).GetSignedHeader(ctx, height) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodGetSignedHeader.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightClientBackend).GetSignedHeader(ctx, req.(int64)) + } + return interceptor(ctx, height, info, handler) +} + +func handlerGetValidatorSet( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + var height int64 + if err := dec(&height); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightClientBackend).GetValidatorSet(ctx, height) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodGetValidatorSet.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightClientBackend).GetValidatorSet(ctx, req.(int64)) + } + return interceptor(ctx, height, info, handler) +} + +func handlerGetParameters( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + var height int64 + if err := dec(&height); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightClientBackend).GetParameters(ctx, height) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodGetParameters.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightClientBackend).GetParameters(ctx, req.(int64)) + } + return interceptor(ctx, height, 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) + RegisterLightService(server, service) +} + +// RegisterLightService registers a new light client backend service with the given gRPC server. +func RegisterLightService(server *grpc.Server, service LightClientBackend) { + server.RegisterService(&lightServiceDesc, service) +} + +type consensusLightClient struct { + conn *grpc.ClientConn +} + +// Implements LightClientBackend. +func (c *consensusLightClient) GetSignedHeader(ctx context.Context, height int64) (*SignedHeader, error) { + var rsp SignedHeader + if err := c.conn.Invoke(ctx, methodGetSignedHeader.FullName(), height, &rsp); err != nil { + return nil, err + } + return &rsp, nil +} + +// Implements LightClientBackend. +func (c *consensusLightClient) GetValidatorSet(ctx context.Context, height int64) (*ValidatorSet, error) { + var rsp ValidatorSet + if err := c.conn.Invoke(ctx, methodGetValidatorSet.FullName(), height, &rsp); err != nil { + return nil, err + } + return &rsp, nil +} + +// Implements LightClientBackend. +func (c *consensusLightClient) GetParameters(ctx context.Context, height int64) (*Parameters, error) { + var rsp Parameters + if err := c.conn.Invoke(ctx, methodGetParameters.FullName(), height, &rsp); err != nil { + return nil, err + } + return &rsp, nil } type consensusClient struct { + consensusLightClient + conn *grpc.ClientConn } @@ -399,5 +535,13 @@ func (c *consensusClient) WatchBlocks(ctx context.Context) (<-chan *Block, pubsu // NewConsensusClient creates a new gRPC consensus client service. func NewConsensusClient(c *grpc.ClientConn) ClientBackend { - return &consensusClient{c} + return &consensusClient{ + consensusLightClient: consensusLightClient{c}, + conn: c, + } +} + +// NewConsensusLightClient creates a new gRPC consensus light client service. +func NewConsensusLightClient(c *grpc.ClientConn) LightClientBackend { + return &consensusLightClient{c} } diff --git a/go/consensus/api/light.go b/go/consensus/api/light.go new file mode 100644 index 00000000000..029f8978737 --- /dev/null +++ b/go/consensus/api/light.go @@ -0,0 +1,44 @@ +package api + +import "context" + +// LightClientBackend is the limited consensus interface used by light clients. +type LightClientBackend interface { + // GetSignedHeader returns the signed header for a specific height. + GetSignedHeader(ctx context.Context, height int64) (*SignedHeader, error) + + // GetValidatorSet returns the validator set for a specific height. + GetValidatorSet(ctx context.Context, height int64) (*ValidatorSet, error) + + // GetParameters returns the consensus parameters for a specific height. + GetParameters(ctx context.Context, height int64) (*Parameters, error) + + // TODO: Move SubmitEvidence etc. from Backend. +} + +// SignedHeader is a signed consensus block header. +type SignedHeader struct { + // Height contains the block height this header is for. + Height int64 `json:"height"` + // Meta contains the consensus backend specific signed header. + Meta []byte `json:"meta"` +} + +// ValidatorSet contains the validator set information. +type ValidatorSet struct { + // Height contains the block height this validator set is for. + Height int64 `json:"height"` + // Meta contains the consensus backend specific validator set. + Meta []byte `json:"meta"` +} + +// Parameters are the consensus backend parameters. +type Parameters struct { + // Height contains the block height these consensus parameters are for. + Height int64 `json:"height"` + + // TODO: Consider also including consensus/genesis.Parameters which are backend-agnostic. + + // Meta contains the consensus backend specific consensus parameters. + Meta []byte `json:"meta"` +} diff --git a/go/consensus/tendermint/api/api.go b/go/consensus/tendermint/api/api.go index 5d82ea2ff0f..866bc80e8ae 100644 --- a/go/consensus/tendermint/api/api.go +++ b/go/consensus/tendermint/api/api.go @@ -149,6 +149,8 @@ func NewBlock(blk *tmtypes.Block) *consensus.Block { return &consensus.Block{ Height: blk.Header.Height, + Hash: blk.Header.Hash(), + Time: blk.Header.Time, Meta: rawMeta, } } diff --git a/go/consensus/tendermint/full.go b/go/consensus/tendermint/full.go new file mode 100644 index 00000000000..95f201e21d7 --- /dev/null +++ b/go/consensus/tendermint/full.go @@ -0,0 +1,75 @@ +package tendermint + +import ( + "context" + "fmt" + + tmamino "github.com/tendermint/go-amino" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + tmstate "github.com/tendermint/tendermint/state" + + consensusAPI "github.com/oasislabs/oasis-core/go/consensus/api" +) + +// We must use Tendermint's amino codec as some Tendermint's types are not easily unmarshallable. +var aminoCodec = tmamino.NewCodec() + +func init() { + tmrpctypes.RegisterAmino(aminoCodec) +} + +// Implements LightClientBackend. +func (t *tendermintService) GetSignedHeader(ctx context.Context, height int64) (*consensusAPI.SignedHeader, error) { + if err := t.ensureStarted(ctx); err != nil { + return nil, err + } + + commit, err := t.client.Commit(&height) + if err != nil { + return nil, fmt.Errorf("%w: tendermint: header query failed: %s", consensusAPI.ErrVersionNotFound, err.Error()) + } + + if commit.Header == nil { + return nil, fmt.Errorf("tendermint: header is nil") + } + + return &consensusAPI.SignedHeader{ + Height: commit.Header.Height, + Meta: aminoCodec.MustMarshalBinaryBare(commit.SignedHeader), + }, nil +} + +// Implements LightClientBackend. +func (t *tendermintService) GetValidatorSet(ctx context.Context, height int64) (*consensusAPI.ValidatorSet, error) { + if err := t.ensureStarted(ctx); err != nil { + return nil, err + } + + // Don't use the client as that imposes stupid pagination. Access the state database directly. + vals, err := tmstate.LoadValidators(t.stateDb, height) + if err != nil { + return nil, consensusAPI.ErrVersionNotFound + } + + return &consensusAPI.ValidatorSet{ + Height: height, + Meta: aminoCodec.MustMarshalBinaryBare(vals), + }, nil +} + +// Implements LightClientBackend. +func (t *tendermintService) GetParameters(ctx context.Context, height int64) (*consensusAPI.Parameters, error) { + if err := t.ensureStarted(ctx); err != nil { + return nil, err + } + + params, err := t.client.ConsensusParams(&height) + if err != nil { + return nil, fmt.Errorf("%w: tendermint: consensus params query failed: %s", consensusAPI.ErrVersionNotFound, err.Error()) + } + + return &consensusAPI.Parameters{ + Height: params.BlockHeight, + Meta: aminoCodec.MustMarshalBinaryBare(params.ConsensusParams), + }, nil +} diff --git a/go/consensus/tendermint/tendermint.go b/go/consensus/tendermint/tendermint.go index 16653342c4e..98951bfab1a 100644 --- a/go/consensus/tendermint/tendermint.go +++ b/go/consensus/tendermint/tendermint.go @@ -25,6 +25,7 @@ import ( tmcli "github.com/tendermint/tendermint/rpc/client/local" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" tmtypes "github.com/tendermint/tendermint/types" + tmdb "github.com/tendermint/tm-db" beaconAPI "github.com/oasislabs/oasis-core/go/beacon/api" "github.com/oasislabs/oasis-core/go/common" @@ -199,6 +200,8 @@ type tendermintService struct { blockNotifier *pubsub.Broker failMonitor *failMonitor + stateDb tmdb.DB + beacon beaconAPI.Backend epochtime epochtimeAPI.Backend keymanager keymanagerAPI.Backend @@ -728,6 +731,20 @@ func (t *tendermintService) WatchBlocks(ctx context.Context) (<-chan *consensusA return mapCh, sub, nil } +func (t *tendermintService) ensureStarted(ctx context.Context) error { + // Make sure that the Tendermint service has started so that we + // have the client interface available. + select { + case <-t.startedCh: + case <-t.ctx.Done(): + return t.ctx.Err() + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} + func (t *tendermintService) initialize() error { t.Lock() defer t.Unlock() @@ -802,14 +819,8 @@ func (t *tendermintService) initialize() error { } func (t *tendermintService) GetTendermintBlock(ctx context.Context, height int64) (*tmtypes.Block, error) { - // Make sure that the Tendermint service has started so that we - // have the client interface available. - select { - case <-t.startedCh: - case <-t.ctx.Done(): - return nil, t.ctx.Err() - case <-ctx.Done(): - return nil, ctx.Err() + if err := t.ensureStarted(ctx); err != nil { + return nil, err } var tmHeight int64 @@ -1035,6 +1046,25 @@ func (t *tendermintService) lazyInit() error { return err } + // HACK: Wrap the provider so we can extract the state database handle. This is required because + // Tendermint does not expose a way to access the state database and we need it to bypass some + // stupid things like pagination on the in-process "client". + wrapDbProvider := func(dbCtx *tmnode.DBContext) (tmdb.DB, error) { + db, derr := dbProvider(dbCtx) + if derr != nil { + return nil, derr + } + + switch dbCtx.ID { + case "state": + // Tendermint state database. + t.stateDb = db + default: + } + + return db, nil + } + // HACK: tmnode.NewNode() triggers block replay and or ABCI chain // initialization, instead of t.node.Start(). This is a problem // because at the time that lazyInit() is called, none of the ABCI @@ -1048,13 +1078,17 @@ func (t *tendermintService) lazyInit() error { &tmp2p.NodeKey{PrivKey: crypto.SignerToTendermint(t.nodeSigner)}, tmproxy.NewLocalClientCreator(t.mux.Mux()), tendermintGenesisProvider, - dbProvider, + wrapDbProvider, tmnode.DefaultMetricsProvider(tenderConfig.Instrumentation), newLogAdapter(!viper.GetBool(cfgLogDebug)), ) if err != nil { return fmt.Errorf("tendermint: failed to create node: %w", err) } + if t.stateDb == nil { + // Sanity check for the above wrapDbProvider hack in case the DB provider changes. + panic("tendermint: state database not set") + } t.client = tmcli.New(t.node) t.failMonitor = newFailMonitor(t.Logger, t.node.ConsensusState().Wait) diff --git a/go/consensus/tests/tester.go b/go/consensus/tests/tester.go index d87d282dca7..2ad2e005628 100644 --- a/go/consensus/tests/tester.go +++ b/go/consensus/tests/tester.go @@ -68,4 +68,20 @@ func ConsensusImplementationTests(t *testing.T, backend consensus.ClientBackend) }) require.NoError(err, "GetSignerNonce") require.Equal(uint64(0), nonce, "Nonce should be zero") + + // Light client API. + shdr, err := backend.GetSignedHeader(ctx, blk.Height) + require.NoError(err, "GetSignedHeader") + require.Equal(shdr.Height, blk.Height, "returned header height should be correct") + require.NotNil(shdr.Meta, "returned header should contain metadata") + + vals, err := backend.GetValidatorSet(ctx, blk.Height) + require.NoError(err, "GetValidatorSet") + require.Equal(vals.Height, blk.Height, "returned validator set height should be correct") + require.NotNil(vals.Meta, "returned validator set should contain metadata") + + params, err := backend.GetParameters(ctx, blk.Height) + 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") } diff --git a/go/go.mod b/go/go.mod index ed5aa187592..36cc0d19fbf 100644 --- a/go/go.mod +++ b/go/go.mod @@ -55,7 +55,7 @@ require ( github.com/spf13/viper v1.6.3 github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 // indirect github.com/stretchr/testify v1.5.1 - github.com/tendermint/go-amino v0.15.0 // indirect + github.com/tendermint/go-amino v0.15.0 github.com/tendermint/tendermint v0.32.8 github.com/tendermint/tm-db v0.5.1 github.com/thepudds/fzgo v0.2.2