diff --git a/.changelog/3169.feature.md b/.changelog/3169.feature.md new file mode 100644 index 00000000000..6a5e861c18b --- /dev/null +++ b/.changelog/3169.feature.md @@ -0,0 +1,3 @@ +go/consensus: Move SubmitEvidence to LightClientBackend + +This allows light clients to submit evidence of Byzantine behavior. diff --git a/go/consensus/api/api.go b/go/consensus/api/api.go index d045b754aa2..81f6e34097b 100644 --- a/go/consensus/api/api.go +++ b/go/consensus/api/api.go @@ -209,9 +209,6 @@ type ServicesBackend interface { // consensus Halt epoch height is reached. RegisterHaltHook(func(ctx context.Context, blockHeight int64, epoch epochtime.EpochTime)) - // SubmitEvidence submits evidence of misbehavior. - SubmitEvidence(ctx context.Context, evidence Evidence) error - // SubmissionManager returns the transaction submission manager. SubmissionManager() SubmissionManager @@ -245,56 +242,6 @@ type TransactionAuthHandler interface { GetSignerNonce(ctx context.Context, req *GetSignerNonceRequest) (uint64, error) } -// EvidenceKind is kind of evindence of a node misbehaving. -type EvidenceKind int - -const ( - // EvidenceKindConsensus is consensus-layer specific evidence. - EvidenceKindConsensus EvidenceKind = 0 - - EvidenceKindMax = EvidenceKindConsensus -) - -// String returns a string representation of an EvidenceKind. -func (k EvidenceKind) String() string { - switch k { - case EvidenceKindConsensus: - return "consensus" - default: - return "[unknown evidence kind]" - } -} - -// Evidence is evidence of a node misbehaving. -type Evidence interface { - // Kind returns the evidence kind. - Kind() EvidenceKind - // Unwrap returns the unwrapped evidence (if any). - Unwrap() interface{} -} - -// ConsensusEvidence is consensus backend-specific evidence. -type ConsensusEvidence struct { - inner interface{} -} - -var _ Evidence = (*ConsensusEvidence)(nil) - -// Kind returns the evidence kind. -func (ce ConsensusEvidence) Kind() EvidenceKind { - return EvidenceKindConsensus -} - -// Unwrap returns the unwrapped evidence (if any). -func (ce ConsensusEvidence) Unwrap() interface{} { - return ce.inner -} - -// NewConsensusEvidence creates new consensus backend-specific evidence. -func NewConsensusEvidence(inner interface{}) ConsensusEvidence { - return ConsensusEvidence{inner: inner} -} - // EstimateGasRequest is a EstimateGas request. type EstimateGasRequest struct { Signer signature.PublicKey `json:"signer"` diff --git a/go/consensus/api/base.go b/go/consensus/api/base.go index ec5c60a05ce..c3ba7346699 100644 --- a/go/consensus/api/base.go +++ b/go/consensus/api/base.go @@ -75,7 +75,7 @@ func (b *BaseBackend) RegisterHaltHook(func(ctx context.Context, blockHeight int } // Implements Backend. -func (b *BaseBackend) SubmitEvidence(ctx context.Context, evidence Evidence) error { +func (b *BaseBackend) SubmitEvidence(ctx context.Context, evidence *Evidence) error { panic(ErrUnsupported) } diff --git a/go/consensus/api/grpc.go b/go/consensus/api/grpc.go index 452a3038498..04e03d6c921 100644 --- a/go/consensus/api/grpc.go +++ b/go/consensus/api/grpc.go @@ -59,6 +59,8 @@ var ( methodStateSyncIterate = lightServiceName.NewMethod("StateSyncIterate", syncer.IterateRequest{}) // methodSubmitTxNoWait is the SubmitTxNoWait method. methodSubmitTxNoWait = lightServiceName.NewMethod("SubmitTxNoWait", transaction.SignedTransaction{}) + // methodSubmitEvidence is the SubmitEvidence method. + methodSubmitEvidence = lightServiceName.NewMethod("SubmitEvidence", &Evidence{}) // serviceDesc is the gRPC service descriptor. serviceDesc = grpc.ServiceDesc{ @@ -152,6 +154,10 @@ var ( MethodName: methodSubmitTxNoWait.ShortName(), Handler: handlerSubmitTxNoWait, }, + { + MethodName: methodSubmitEvidence.ShortName(), + Handler: handlerSubmitEvidence, + }, }, } ) @@ -590,6 +596,29 @@ func handlerSubmitTxNoWait( // nolint: golint return interceptor(ctx, rq, info, handler) } +func handlerSubmitEvidence( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + rq := new(Evidence) + if err := dec(rq); err != nil { + return nil, err + } + if interceptor == nil { + return nil, srv.(LightClientBackend).SubmitEvidence(ctx, rq) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodSubmitEvidence.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return nil, srv.(LightClientBackend).SubmitEvidence(ctx, req.(*Evidence)) + } + 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) @@ -673,6 +702,11 @@ func (c *consensusLightClient) SubmitTxNoWait(ctx context.Context, tx *transacti return c.conn.Invoke(ctx, methodSubmitTxNoWait.FullName(), tx, nil) } +// Implements LightClientBackend. +func (c *consensusLightClient) SubmitEvidence(ctx context.Context, evidence *Evidence) error { + return c.conn.Invoke(ctx, methodSubmitEvidence.FullName(), evidence, nil) +} + type consensusClient struct { consensusLightClient diff --git a/go/consensus/api/light.go b/go/consensus/api/light.go index a4d379fb1dd..52aa1ec7409 100644 --- a/go/consensus/api/light.go +++ b/go/consensus/api/light.go @@ -26,7 +26,8 @@ type LightClientBackend interface { // to be included in a block. Use SubmitTx if you need to wait for execution. SubmitTxNoWait(ctx context.Context, tx *transaction.SignedTransaction) error - // TODO: Move SubmitEvidence etc. from Backend. + // SubmitEvidence submits evidence of misbehavior. + SubmitEvidence(ctx context.Context, evidence *Evidence) error } // SignedHeader is a signed consensus block header. @@ -54,3 +55,9 @@ type Parameters struct { // TODO: Consider also including consensus/genesis.Parameters which are backend-agnostic. } + +// Evidence is evidence of a node's Byzantine behavior. +type Evidence struct { + // Meta contains the consensus backend specific evidence. + Meta []byte `json:"meta"` +} diff --git a/go/consensus/tendermint/full/full.go b/go/consensus/tendermint/full/full.go index 3cd8071eb27..ac0ba0f314f 100644 --- a/go/consensus/tendermint/full/full.go +++ b/go/consensus/tendermint/full/full.go @@ -23,6 +23,7 @@ import ( tmmempool "github.com/tendermint/tendermint/mempool" tmnode "github.com/tendermint/tendermint/node" tmp2p "github.com/tendermint/tendermint/p2p" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmproxy "github.com/tendermint/tendermint/proxy" tmcli "github.com/tendermint/tendermint/rpc/client/local" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" @@ -537,17 +538,18 @@ func (t *fullService) newSubscriberID() string { return fmt.Sprintf("%s/subscriber-%d", tmSubscriberID, atomic.AddUint64(&t.nextSubscriberID, 1)) } -func (t *fullService) SubmitEvidence(ctx context.Context, evidence consensusAPI.Evidence) error { - if evidence.Kind() != consensusAPI.EvidenceKindConsensus { - return fmt.Errorf("tendermint: unsupported evidence kind") +func (t *fullService) SubmitEvidence(ctx context.Context, evidence *consensusAPI.Evidence) error { + var protoEv tmproto.Evidence + if err := protoEv.Unmarshal(evidence.Meta); err != nil { + return fmt.Errorf("tendermint: malformed evidence while unmarshalling: %w", err) } - tmEvidence, ok := evidence.Unwrap().(tmtypes.Evidence) - if !ok { - return fmt.Errorf("tendermint: expected tendermint evidence, got something else") + ev, err := tmtypes.EvidenceFromProto(&protoEv) + if err != nil { + return fmt.Errorf("tendermint: malformed evidence while converting: %w", err) } - if _, err := t.client.BroadcastEvidence(tmEvidence); err != nil { + if _, err := t.client.BroadcastEvidence(ev); err != nil { return fmt.Errorf("tendermint: broadcast evidence failed: %w", err) } diff --git a/go/consensus/tendermint/light/client.go b/go/consensus/tendermint/light/client.go index b22119216cb..c4b9e4f254f 100644 --- a/go/consensus/tendermint/light/client.go +++ b/go/consensus/tendermint/light/client.go @@ -112,8 +112,16 @@ func (lp *lightClientProvider) ValidatorSet(height int64) (*tmtypes.ValidatorSet // Implements tmlightprovider.Provider. func (lp *lightClientProvider) ReportEvidence(ev tmtypes.Evidence) error { - // TODO: Implement SubmitEvidence. - return fmt.Errorf("not yet implemented") + proto, err := tmtypes.EvidenceToProto(ev) + if err != nil { + return fmt.Errorf("failed to convert evidence: %w", err) + } + meta, err := proto.Marshal() + if err != nil { + return fmt.Errorf("failed to marshal evidence: %w", err) + } + + return lp.client.SubmitEvidence(lp.ctx, &consensus.Evidence{Meta: meta}) } // newLightClientProvider creates a new provider for the Tendermint's light client. @@ -178,6 +186,11 @@ func (lc *lightClient) SubmitTxNoWait(ctx context.Context, tx *transaction.Signe return lc.getPrimary().SubmitTxNoWait(ctx, tx) } +// Implements consensus.LightClientBackend. +func (lc *lightClient) SubmitEvidence(ctx context.Context, evidence *consensus.Evidence) error { + return lc.getPrimary().SubmitEvidence(ctx, evidence) +} + // Implements Client. func (lc *lightClient) GetVerifiedSignedHeader(ctx context.Context, height int64) (*tmtypes.SignedHeader, error) { return lc.tmc.VerifyHeaderAtHeight(height, time.Now()) diff --git a/go/consensus/tendermint/tests/evidence.go b/go/consensus/tendermint/tests/evidence.go index 3608db1d8a6..3084a17ec34 100644 --- a/go/consensus/tendermint/tests/evidence.go +++ b/go/consensus/tendermint/tests/evidence.go @@ -18,7 +18,7 @@ import ( ) // MakeDoubleSignEvidence creates consensus evidence of double signing. -func MakeDoubleSignEvidence(t *testing.T, ident *identity.Identity) consensus.Evidence { +func MakeDoubleSignEvidence(t *testing.T, ident *identity.Identity) *consensus.Evidence { require := require.New(t) // Create empty directory for private validator metadata. @@ -64,7 +64,13 @@ func MakeDoubleSignEvidence(t *testing.T, ident *identity.Identity) consensus.Ev VoteA: makeVote(pv1, genesisTestHelpers.TestChainID, 0, 1, 2, 1, blockID1, now), VoteB: makeVote(pv2, genesisTestHelpers.TestChainID, 0, 1, 2, 1, blockID2, now), } - return consensus.NewConsensusEvidence(ev) + + proto, err := tmtypes.EvidenceToProto(ev) + require.NoError(err, "EvidenceToProto") + meta, err := proto.Marshal() + require.NoError(err, "proto.Marshal") + + return &consensus.Evidence{Meta: meta} } // makeVote copied from Tendermint test suite. diff --git a/go/consensus/tests/tester.go b/go/consensus/tests/tester.go index 78d33b47cae..40776a216c1 100644 --- a/go/consensus/tests/tester.go +++ b/go/consensus/tests/tester.go @@ -130,6 +130,9 @@ func ConsensusImplementationTests(t *testing.T, backend consensus.ClientBackend) err = backend.SubmitTxNoWait(ctx, testSigTx) require.NoError(err, "SubmitTxNoWait") + err = backend.SubmitEvidence(ctx, &consensus.Evidence{}) + require.Error(err, "SubmitEvidence should fail with invalid evidence") + // 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.