From 95ddc242ad024ca78a359a13122dade6f14fd676 Mon Sep 17 00:00:00 2001 From: Alexander Bezobchuk Date: Wed, 6 Nov 2019 14:08:02 -0700 Subject: [PATCH] Merge PR #5240: x/evidence module implementation --- CHANGELOG.md | 1 + docs/architecture/adr-009-evidence-module.md | 13 +- docs/building-modules/README.md | 75 ++++---- simapp/app.go | 114 ++++++++---- types/errors.go | 16 ++ types/errors/errors.go | 12 +- types/router.go | 6 + x/evidence/alias.go | 50 +++++ x/evidence/client/cli/query.go | 116 ++++++++++++ x/evidence/client/cli/tx.go | 45 +++++ x/evidence/client/evidence_handler.go | 31 ++++ x/evidence/client/rest/query.go | 91 ++++++++++ x/evidence/client/rest/rest.go | 30 +++ x/evidence/client/rest/tx.go | 11 ++ x/evidence/doc.go | 44 +++++ x/evidence/exported/evidence.go | 29 +++ x/evidence/genesis.go | 30 +++ x/evidence/genesis_test.go | 111 ++++++++++++ x/evidence/handler.go | 40 ++++ x/evidence/handler_test.go | 103 +++++++++++ x/evidence/internal/keeper/keeper.go | 147 +++++++++++++++ x/evidence/internal/keeper/keeper_test.go | 181 +++++++++++++++++++ x/evidence/internal/keeper/querier.go | 87 +++++++++ x/evidence/internal/keeper/querier_test.go | 86 +++++++++ x/evidence/internal/types/codec.go | 28 +++ x/evidence/internal/types/codec_test.go | 42 +++++ x/evidence/internal/types/errors.go | 58 ++++++ x/evidence/internal/types/events.go | 9 + x/evidence/internal/types/genesis.go | 31 ++++ x/evidence/internal/types/genesis_test.go | 71 ++++++++ x/evidence/internal/types/keys.go | 23 +++ x/evidence/internal/types/msgs.go | 59 ++++++ x/evidence/internal/types/msgs_test.go | 66 +++++++ x/evidence/internal/types/querier.go | 26 +++ x/evidence/internal/types/router.go | 81 +++++++++ x/evidence/internal/types/router_test.go | 28 +++ x/evidence/internal/types/test_util.go | 135 ++++++++++++++ x/evidence/module.go | 167 +++++++++++++++++ x/evidence/spec/01_concepts.md | 58 ++++++ x/evidence/spec/02_state.md | 12 ++ x/evidence/spec/03_messages.md | 42 +++++ x/evidence/spec/04_events.md | 14 ++ x/evidence/spec/README.md | 28 +++ x/gov/types/router.go | 4 +- 44 files changed, 2373 insertions(+), 78 deletions(-) create mode 100644 x/evidence/alias.go create mode 100644 x/evidence/client/cli/query.go create mode 100644 x/evidence/client/cli/tx.go create mode 100644 x/evidence/client/evidence_handler.go create mode 100644 x/evidence/client/rest/query.go create mode 100644 x/evidence/client/rest/rest.go create mode 100644 x/evidence/client/rest/tx.go create mode 100644 x/evidence/doc.go create mode 100644 x/evidence/exported/evidence.go create mode 100644 x/evidence/genesis.go create mode 100644 x/evidence/genesis_test.go create mode 100644 x/evidence/handler.go create mode 100644 x/evidence/handler_test.go create mode 100644 x/evidence/internal/keeper/keeper.go create mode 100644 x/evidence/internal/keeper/keeper_test.go create mode 100644 x/evidence/internal/keeper/querier.go create mode 100644 x/evidence/internal/keeper/querier_test.go create mode 100644 x/evidence/internal/types/codec.go create mode 100644 x/evidence/internal/types/codec_test.go create mode 100644 x/evidence/internal/types/errors.go create mode 100644 x/evidence/internal/types/events.go create mode 100644 x/evidence/internal/types/genesis.go create mode 100644 x/evidence/internal/types/genesis_test.go create mode 100644 x/evidence/internal/types/keys.go create mode 100644 x/evidence/internal/types/msgs.go create mode 100644 x/evidence/internal/types/msgs_test.go create mode 100644 x/evidence/internal/types/querier.go create mode 100644 x/evidence/internal/types/router.go create mode 100644 x/evidence/internal/types/router_test.go create mode 100644 x/evidence/internal/types/test_util.go create mode 100644 x/evidence/module.go create mode 100644 x/evidence/spec/01_concepts.md create mode 100644 x/evidence/spec/02_state.md create mode 100644 x/evidence/spec/03_messages.md create mode 100644 x/evidence/spec/04_events.md create mode 100644 x/evidence/spec/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d9cb926..670f26a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ increased significantly due to modular `AnteHandler` support. Increase GasLimit ### Features +* (x/evidence) [\#5240](https://github.com/cosmos/cosmos-sdk/pull/5240) Initial implementation of the `x/evidence` module. * (cli) [\#5212](https://github.com/cosmos/cosmos-sdk/issues/5212) The `q gov proposals` command now supports pagination. * (store) [\#4724](https://github.com/cosmos/cosmos-sdk/issues/4724) Multistore supports substore migrations upon load. New `rootmulti.Store.LoadLatestVersionAndUpgrade` method in `Baseapp` supports `StoreLoader` to enable various upgrade strategies. It no diff --git a/docs/architecture/adr-009-evidence-module.md b/docs/architecture/adr-009-evidence-module.md index ac73d60a3..b7d0fdc49 100644 --- a/docs/architecture/adr-009-evidence-module.md +++ b/docs/architecture/adr-009-evidence-module.md @@ -3,10 +3,11 @@ ## Changelog - 2019 July 31: Initial draft +- 2019 October 24: Initial implementation ## Status -Proposed +Accepted ## Context @@ -55,7 +56,8 @@ type Evidence interface { Route() string Type() string String() string - ValidateBasic() Error + Hash() HexBytes + ValidateBasic() error // The consensus address of the malicious validator at time of infraction GetConsensusAddress() ConsAddress @@ -78,7 +80,7 @@ the `x/evidence` module. It accomplishes this through the `Router` implementatio ```go type Router interface { - AddRoute(r string, h Handler) + AddRoute(r string, h Handler) Router HasRoute(r string) bool GetRoute(path string) Handler Seal() @@ -97,7 +99,7 @@ necessary in order for the `Handler` to make the necessary state transitions. If no error is returned, the `Evidence` is considered valid. ```go -type Handler func(Context, Evidence) Error +type Handler func(Context, Evidence) error ``` ### Submission @@ -128,7 +130,7 @@ the module's router and invoking the corresponding `Handler` which may include slashing and jailing the validator. Upon success, the submitted evidence is persisted. ```go -func (k Keeper) SubmitEvidence(ctx Context, evidence Evidence) Error { +func (k Keeper) SubmitEvidence(ctx Context, evidence Evidence) error { handler := keeper.router.GetRoute(evidence.Route()) if err := handler(ctx, evidence); err != nil { return ErrInvalidEvidence(keeper.codespace, err) @@ -177,3 +179,4 @@ due to the inability to introduce the new evidence type's corresponding handler - [ICS](https://github.com/cosmos/ics) - [IBC Architecture](https://github.com/cosmos/ics/blob/master/ibc/1_IBC_ARCHITECTURE.md) +- [Tendermint Fork Accountability](https://github.com/tendermint/tendermint/blob/master/docs/spec/consensus/fork-accountability.md) diff --git a/docs/building-modules/README.md b/docs/building-modules/README.md index 5b5743671..145e6e501 100644 --- a/docs/building-modules/README.md +++ b/docs/building-modules/README.md @@ -1,78 +1,75 @@ -# Auth +# Modules + +## Auth The `x/auth` modules is used for accounts -See the [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth) +- [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth) +- [Specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/auth) -See the [specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/auth) - -# Bank +## Bank The `x/bank` module is for transferring coins between accounts. -See the [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank). +- [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank) +- [Specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/bank) -See the [specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/bank) - -# Stake +## Staking The `x/staking` module is for Cosmos Delegated-Proof-of-Stake. -See the [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/staking). +- [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/staking) +- [Specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/staking) -See the -[specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/staking) - -# Slashing +## Slashing The `x/slashing` module is for Cosmos Delegated-Proof-of-Stake. -See the [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/slashing) +- [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/slashing) +- [Specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/slashing) -See the -[specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/slashing) - -# Distribution +## Distribution The `x/distribution` module is for distributing fees and inflation across bonded stakeholders. -See the [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/distribution) +- [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/distribution) +- [Specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/distribution) -See the -[specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/distribution) - -# Governance +## Governance The `x/gov` module is for bonded stakeholders to make proposals and vote on them. -See the [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/gov) - -See the -[specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/governance) +- [API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/gov) +- [Specification](https://github.com/cosmos/cosmos-sdk/tree/master/docs/spec/governance) To keep up with the current status of IBC, follow and contribute to [ICS](https://github.com/cosmos/ics) -# Crisis +## Crisis The `x/crisis` module is for halting the blockchain under certain circumstances. -See the [API Docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/crisis) +- [API Docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/crisis) +- [Specification](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/crisis) -See the [specification](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/crisis) - -# Mint +## Mint The `x/mint` module is for flexible inflation rates and effect a balance between market liquidity and staked supply. -See the [API Docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/mint) +- [API Docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/mint) +- [Specification](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/mint) -See the [specification](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/mint) - -# Params +## Params The `x/params` module provides a globally available parameter store. -See the [API Docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/params) +- [API Docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/params) +- [Specification](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/params) -See the [specification](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/params) +## Evidence + +The `x/evidence` modules provides a mechanism for defining and submitting arbitrary +events of misbehavior and a means to execute custom business logic for such misbehavior. + +- [API Docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/evidence) +- [Specification](https://github.com/cosmos/cosmos-sdk/blob/master/docs/spec/evidence) diff --git a/simapp/app.go b/simapp/app.go index 53e671a12..e310e1749 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -20,6 +20,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/bank" "github.com/cosmos/cosmos-sdk/x/crisis" distr "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/evidence" "github.com/cosmos/cosmos-sdk/x/genutil" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/mint" @@ -54,6 +55,7 @@ var ( params.AppModuleBasic{}, crisis.AppModuleBasic{}, slashing.AppModuleBasic{}, + evidence.AppModuleBasic{}, ) // module account permissions @@ -90,6 +92,9 @@ type SimApp struct { keys map[string]*sdk.KVStoreKey tkeys map[string]*sdk.TransientStoreKey + // subspaces + subspaces map[string]params.Subspace + // keepers AccountKeeper auth.AccountKeeper BankKeeper bank.Keeper @@ -101,6 +106,7 @@ type SimApp struct { GovKeeper gov.Keeper CrisisKeeper crisis.Keeper ParamsKeeper params.Keeper + EvidenceKeeper evidence.Keeper // the module manager mm *module.Manager @@ -121,9 +127,10 @@ func NewSimApp( bApp.SetCommitMultiStoreTracer(traceStore) bApp.SetAppVersion(version.Version) - keys := sdk.NewKVStoreKeys(bam.MainStoreKey, auth.StoreKey, staking.StoreKey, - supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, - gov.StoreKey, params.StoreKey) + keys := sdk.NewKVStoreKeys( + bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, + distr.StoreKey, slashing.StoreKey, gov.StoreKey, params.StoreKey, evidence.StoreKey, + ) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) app := &SimApp{ @@ -132,39 +139,68 @@ func NewSimApp( invCheckPeriod: invCheckPeriod, keys: keys, tkeys: tkeys, + subspaces: make(map[string]params.Subspace), } // init params keeper and subspaces app.ParamsKeeper = params.NewKeeper(app.cdc, keys[params.StoreKey], tkeys[params.TStoreKey], params.DefaultCodespace) - authSubspace := app.ParamsKeeper.Subspace(auth.DefaultParamspace) - bankSubspace := app.ParamsKeeper.Subspace(bank.DefaultParamspace) - stakingSubspace := app.ParamsKeeper.Subspace(staking.DefaultParamspace) - mintSubspace := app.ParamsKeeper.Subspace(mint.DefaultParamspace) - distrSubspace := app.ParamsKeeper.Subspace(distr.DefaultParamspace) - slashingSubspace := app.ParamsKeeper.Subspace(slashing.DefaultParamspace) - govSubspace := app.ParamsKeeper.Subspace(gov.DefaultParamspace).WithKeyTable(gov.ParamKeyTable()) - crisisSubspace := app.ParamsKeeper.Subspace(crisis.DefaultParamspace) + app.subspaces[auth.ModuleName] = app.ParamsKeeper.Subspace(auth.DefaultParamspace) + app.subspaces[bank.ModuleName] = app.ParamsKeeper.Subspace(bank.DefaultParamspace) + app.subspaces[staking.ModuleName] = app.ParamsKeeper.Subspace(staking.DefaultParamspace) + app.subspaces[mint.ModuleName] = app.ParamsKeeper.Subspace(mint.DefaultParamspace) + app.subspaces[distr.ModuleName] = app.ParamsKeeper.Subspace(distr.DefaultParamspace) + app.subspaces[slashing.ModuleName] = app.ParamsKeeper.Subspace(slashing.DefaultParamspace) + app.subspaces[gov.ModuleName] = app.ParamsKeeper.Subspace(gov.DefaultParamspace).WithKeyTable(gov.ParamKeyTable()) + app.subspaces[crisis.ModuleName] = app.ParamsKeeper.Subspace(crisis.DefaultParamspace) + app.subspaces[evidence.ModuleName] = app.ParamsKeeper.Subspace(evidence.DefaultParamspace) // add keepers - app.AccountKeeper = auth.NewAccountKeeper(app.cdc, keys[auth.StoreKey], authSubspace, auth.ProtoBaseAccount) - app.BankKeeper = bank.NewBaseKeeper(app.AccountKeeper, bankSubspace, bank.DefaultCodespace, app.ModuleAccountAddrs()) - app.SupplyKeeper = supply.NewKeeper(app.cdc, keys[supply.StoreKey], app.AccountKeeper, app.BankKeeper, maccPerms) - stakingKeeper := staking.NewKeeper(app.cdc, keys[staking.StoreKey], - app.SupplyKeeper, stakingSubspace, staking.DefaultCodespace) - app.MintKeeper = mint.NewKeeper(app.cdc, keys[mint.StoreKey], mintSubspace, &stakingKeeper, app.SupplyKeeper, auth.FeeCollectorName) - app.DistrKeeper = distr.NewKeeper(app.cdc, keys[distr.StoreKey], distrSubspace, &stakingKeeper, - app.SupplyKeeper, distr.DefaultCodespace, auth.FeeCollectorName, app.ModuleAccountAddrs()) - app.SlashingKeeper = slashing.NewKeeper(app.cdc, keys[slashing.StoreKey], &stakingKeeper, - slashingSubspace, slashing.DefaultCodespace) - app.CrisisKeeper = crisis.NewKeeper(crisisSubspace, invCheckPeriod, app.SupplyKeeper, auth.FeeCollectorName) + app.AccountKeeper = auth.NewAccountKeeper( + app.cdc, keys[auth.StoreKey], app.subspaces[auth.ModuleName], auth.ProtoBaseAccount, + ) + app.BankKeeper = bank.NewBaseKeeper( + app.AccountKeeper, app.subspaces[bank.ModuleName], bank.DefaultCodespace, + app.ModuleAccountAddrs(), + ) + app.SupplyKeeper = supply.NewKeeper( + app.cdc, keys[supply.StoreKey], app.AccountKeeper, app.BankKeeper, maccPerms, + ) + stakingKeeper := staking.NewKeeper( + app.cdc, keys[staking.StoreKey], app.SupplyKeeper, app.subspaces[staking.ModuleName], + staking.DefaultCodespace) + app.MintKeeper = mint.NewKeeper( + app.cdc, keys[mint.StoreKey], app.subspaces[mint.ModuleName], &stakingKeeper, + app.SupplyKeeper, auth.FeeCollectorName, + ) + app.DistrKeeper = distr.NewKeeper( + app.cdc, keys[distr.StoreKey], app.subspaces[distr.ModuleName], &stakingKeeper, + app.SupplyKeeper, distr.DefaultCodespace, auth.FeeCollectorName, app.ModuleAccountAddrs(), + ) + app.SlashingKeeper = slashing.NewKeeper( + app.cdc, keys[slashing.StoreKey], &stakingKeeper, app.subspaces[slashing.ModuleName], slashing.DefaultCodespace, + ) + app.CrisisKeeper = crisis.NewKeeper( + app.subspaces[crisis.ModuleName], invCheckPeriod, app.SupplyKeeper, auth.FeeCollectorName, + ) + + // create evidence keeper with router + evidenceKeeper := evidence.NewKeeper( + app.cdc, keys[evidence.StoreKey], app.subspaces[evidence.ModuleName], evidence.DefaultCodespace, + ) + evidenceRouter := evidence.NewRouter() + // TODO: Register evidence routes. + evidenceKeeper.SetRouter(evidenceRouter) + app.EvidenceKeeper = *evidenceKeeper // register the proposal types govRouter := gov.NewRouter() govRouter.AddRoute(gov.RouterKey, gov.ProposalHandler). AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.ParamsKeeper)). AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.DistrKeeper)) - app.GovKeeper = gov.NewKeeper(app.cdc, keys[gov.StoreKey], govSubspace, - app.SupplyKeeper, &stakingKeeper, gov.DefaultCodespace, govRouter) + app.GovKeeper = gov.NewKeeper( + app.cdc, keys[gov.StoreKey], app.subspaces[gov.ModuleName], app.SupplyKeeper, + &stakingKeeper, gov.DefaultCodespace, govRouter, + ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -185,22 +221,21 @@ func NewSimApp( distr.NewAppModule(app.DistrKeeper, app.SupplyKeeper), slashing.NewAppModule(app.SlashingKeeper, app.StakingKeeper), staking.NewAppModule(app.StakingKeeper, app.AccountKeeper, app.SupplyKeeper), + evidence.NewAppModule(app.EvidenceKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName) - app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName) // NOTE: The genutils moodule must occur after staking so that pools are // properly initialized with tokens from genesis accounts. app.mm.SetOrderInitGenesis( - auth.ModuleName, distr.ModuleName, staking.ModuleName, - bank.ModuleName, slashing.ModuleName, gov.ModuleName, - mint.ModuleName, supply.ModuleName, crisis.ModuleName, - genutil.ModuleName, + auth.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, + slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, + crisis.ModuleName, genutil.ModuleName, evidence.ModuleName, ) app.mm.RegisterInvariants(&app.CrisisKeeper) @@ -239,6 +274,7 @@ func NewSimApp( cmn.Exit(err.Error()) } } + return app } @@ -274,21 +310,35 @@ func (app *SimApp) ModuleAccountAddrs() map[string]bool { return modAccAddrs } -// Codec returns simapp's codec +// Codec returns SimApp's codec. +// +// NOTE: This is solely to be used for testing purposes as it may be desirable +// for modules to register their own custom testing types. func (app *SimApp) Codec() *codec.Codec { return app.cdc } -// GetKey returns the KVStoreKey for the provided store key +// GetKey returns the KVStoreKey for the provided store key. +// +// NOTE: This is solely to be used for testing purposes. func (app *SimApp) GetKey(storeKey string) *sdk.KVStoreKey { return app.keys[storeKey] } -// GetTKey returns the TransientStoreKey for the provided store key +// GetTKey returns the TransientStoreKey for the provided store key. +// +// NOTE: This is solely to be used for testing purposes. func (app *SimApp) GetTKey(storeKey string) *sdk.TransientStoreKey { return app.tkeys[storeKey] } +// GetSubspace returns a param subspace for a given module name. +// +// NOTE: This is solely to be used for testing purposes. +func (app *SimApp) GetSubspace(moduleName string) params.Subspace { + return app.subspaces[moduleName] +} + // GetMaccPerms returns a copy of the module account permissions func GetMaccPerms() map[string][]string { dupMaccPerms := make(map[string][]string) diff --git a/types/errors.go b/types/errors.go index 1447a21b4..c59571bfe 100644 --- a/types/errors.go +++ b/types/errors.go @@ -309,6 +309,22 @@ func ResultFromError(err error) Result { } } +// ConvertError accepts a standard error and attempts to convert it to an sdk.Error. +// If the given error is already an sdk.Error, it'll simply be returned. Otherwise, +// it'll convert it to a types.Error. This is meant to provide a migration path +// away from sdk.Error in favor of types.Error. +func ConvertError(err error) Error { + if err == nil { + return nil + } + if sdkError, ok := err.(Error); ok { + return sdkError + } + + space, code, log := sdkerrors.ABCIInfo(err, false) + return NewError(CodespaceType(space), CodeType(code), log) +} + //---------------------------------------- // REST error utilities diff --git a/types/errors/errors.go b/types/errors/errors.go index 642c433d9..22ed2f4c8 100644 --- a/types/errors/errors.go +++ b/types/errors/errors.go @@ -65,6 +65,12 @@ var ( // ErrNoSignatures to doc ErrNoSignatures = Register(RootCodespace, 16, "no signatures supplied") + // ErrJSONMarshal defines an ABCI typed JSON marshalling error + ErrJSONMarshal = Register(RootCodespace, 17, "failed to marshal JSON bytes") + + // ErrJSONUnmarshal defines an ABCI typed JSON unmarshalling error + ErrJSONUnmarshal = Register(RootCodespace, 18, "failed to unmarshal JSON bytes") + // ErrPanic is only set when we recover from a panic, so we know to // redact potentially sensitive system info ErrPanic = Register(UndefinedCodespace, 111222, "panic") @@ -121,7 +127,7 @@ func ABCIError(codespace string, code uint32, log string) error { } // This is a unique error, will never match on .Is() // Use Wrap here to get a stack trace - return Wrap(&Error{codespace: codespace, code: code, desc: "unknown"}, log) + return Wrap(New(codespace, code, "unknown"), log) } // Error represents a root error. @@ -139,6 +145,10 @@ type Error struct { desc string } +func New(codespace string, code uint32, desc string) *Error { + return &Error{codespace: codespace, code: code, desc: desc} +} + func (e Error) Error() string { return e.desc } diff --git a/types/router.go b/types/router.go index 7b45918c3..c14255d4e 100644 --- a/types/router.go +++ b/types/router.go @@ -1,5 +1,11 @@ package types +import "regexp" + +// IsAlphaNumeric defines a regular expression for matching against alpha-numeric +// values. +var IsAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString + // Router provides handlers for each transaction type. type Router interface { AddRoute(r string, h Handler) Router diff --git a/x/evidence/alias.go b/x/evidence/alias.go new file mode 100644 index 000000000..5eca5a074 --- /dev/null +++ b/x/evidence/alias.go @@ -0,0 +1,50 @@ +package evidence + +import ( + "github.com/cosmos/cosmos-sdk/x/evidence/internal/keeper" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +// nolint + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + QueryEvidence = types.QueryEvidence + QueryAllEvidence = types.QueryAllEvidence + CodeNoEvidenceHandlerExists = types.CodeNoEvidenceHandlerExists + CodeInvalidEvidence = types.CodeInvalidEvidence + CodeNoEvidenceExists = types.CodeNoEvidenceExists + TypeMsgSubmitEvidence = types.TypeMsgSubmitEvidence + DefaultCodespace = types.DefaultCodespace + EventTypeSubmitEvidence = types.EventTypeSubmitEvidence + AttributeValueCategory = types.AttributeValueCategory + AttributeKeyEvidenceHash = types.AttributeKeyEvidenceHash +) + +var ( + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + + NewMsgSubmitEvidence = types.NewMsgSubmitEvidence + NewRouter = types.NewRouter + NewQueryEvidenceParams = types.NewQueryEvidenceParams + NewQueryAllEvidenceParams = types.NewQueryAllEvidenceParams + RegisterCodec = types.RegisterCodec + RegisterEvidenceTypeCodec = types.RegisterEvidenceTypeCodec + ModuleCdc = types.ModuleCdc + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState +) + +type ( + Keeper = keeper.Keeper + + GenesisState = types.GenesisState + MsgSubmitEvidence = types.MsgSubmitEvidence + Handler = types.Handler + Router = types.Router +) diff --git a/x/evidence/client/cli/query.go b/x/evidence/client/cli/query.go new file mode 100644 index 000000000..0529af859 --- /dev/null +++ b/x/evidence/client/cli/query.go @@ -0,0 +1,116 @@ +package cli + +import ( + "encoding/hex" + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +const ( + flagPage = "page" + flagLimit = "limit" +) + +// GetQueryCmd returns the CLI command with all evidence module query commands +// mounted. +func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Query for evidence by hash or for all (paginated) submitted evidence", + Long: strings.TrimSpace( + fmt.Sprintf(`Query for specific submitted evidence by hash or query for all (paginated) evidence: + +Example: +$ %s query %s DF0C23E8634E480F84B9D5674A7CDC9816466DEC28A3358F73260F68D28D7660 +$ %s query %s --page=2 --limit=50 +`, + version.ClientName, types.ModuleName, version.ClientName, types.ModuleName, + ), + ), + Args: cobra.MaximumNArgs(1), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: QueryEvidenceCmd(cdc), + } + + cmd.Flags().Int(flagPage, 1, "pagination page of evidence to to query for") + cmd.Flags().Int(flagLimit, 100, "pagination limit of evidence to query for") + + return cmd +} + +// QueryEvidenceCmd returns the command handler for evidence querying. Evidence +// can be queried for by hash or paginated evidence can be returned. +func QueryEvidenceCmd(cdc *codec.Codec) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + if err := client.ValidateCmd(cmd, args); err != nil { + return err + } + + cliCtx := context.NewCLIContext().WithCodec(cdc) + + if hash := args[0]; hash != "" { + return queryEvidence(cdc, cliCtx, hash) + } + + return queryAllEvidence(cdc, cliCtx) + } +} + +func queryEvidence(cdc *codec.Codec, cliCtx context.CLIContext, hash string) error { + if _, err := hex.DecodeString(hash); err != nil { + return fmt.Errorf("invalid evidence hash: %w", err) + } + + params := types.NewQueryEvidenceParams(hash) + bz, err := cdc.MarshalJSON(params) + if err != nil { + return fmt.Errorf("failed to marshal query params: %w", err) + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryEvidence) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + return err + } + + var evidence exported.Evidence + err = cdc.UnmarshalJSON(res, &evidence) + if err != nil { + return fmt.Errorf("failed to unmarshal evidence: %w", err) + } + + return cliCtx.PrintOutput(evidence) +} + +func queryAllEvidence(cdc *codec.Codec, cliCtx context.CLIContext) error { + params := types.NewQueryAllEvidenceParams(viper.GetInt(flagPage), viper.GetInt(flagLimit)) + bz, err := cdc.MarshalJSON(params) + if err != nil { + return fmt.Errorf("failed to marshal query params: %w", err) + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryAllEvidence) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + return err + } + + var evidence []exported.Evidence + err = cdc.UnmarshalJSON(res, &evidence) + if err != nil { + return fmt.Errorf("failed to unmarshal evidence: %w", err) + } + + return cliCtx.PrintOutput(evidence) +} diff --git a/x/evidence/client/cli/tx.go b/x/evidence/client/cli/tx.go new file mode 100644 index 000000000..7cf6c5f6e --- /dev/null +++ b/x/evidence/client/cli/tx.go @@ -0,0 +1,45 @@ +package cli + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + "github.com/spf13/cobra" +) + +// GetTxCmd returns a CLI command that has all the native evidence module tx +// commands mounted. In addition, it mounts all childCmds, implemented by outside +// modules, under a sub-command. This allows external modules to implement custom +// Evidence types and Handlers while having the ability to create and sign txs +// containing them all from a single root command. +func GetTxCmd(storeKey string, cdc *codec.Codec, childCmds []*cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Evidence transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + submitEvidenceCmd := SubmitEvidenceCmd(cdc) + for _, childCmd := range childCmds { + submitEvidenceCmd.AddCommand(client.PostCommands(childCmd)[0]) + } + + // TODO: Add tx commands. + + return cmd +} + +// SubmitEvidenceCmd returns the top-level evidence submission command handler. +// All concrete evidence submission child command handlers should be registered +// under this command. +func SubmitEvidenceCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "submit", + Short: "Submit arbitrary evidence of misbehavior", + } + + return cmd +} diff --git a/x/evidence/client/evidence_handler.go b/x/evidence/client/evidence_handler.go new file mode 100644 index 000000000..0b56b4c4c --- /dev/null +++ b/x/evidence/client/evidence_handler.go @@ -0,0 +1,31 @@ +package client + +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/evidence/client/rest" +) + +type ( + // RESTHandlerFn defines a REST service handler for evidence submission + RESTHandlerFn func(context.CLIContext) rest.EvidenceRESTHandler + + // CLIHandlerFn defines a CLI command handler for evidence submission + CLIHandlerFn func(*codec.Codec) *cobra.Command + + // EvidenceHandler defines a type that exposes REST and CLI client handlers for + // evidence submission. + EvidenceHandler struct { + CLIHandler CLIHandlerFn + RESTHandler RESTHandlerFn + } +) + +func NewEvidenceHandler(cliHandler CLIHandlerFn, restHandler RESTHandlerFn) EvidenceHandler { + return EvidenceHandler{ + CLIHandler: cliHandler, + RESTHandler: restHandler, + } +} diff --git a/x/evidence/client/rest/query.go b/x/evidence/client/rest/query.go new file mode 100644 index 000000000..84a894487 --- /dev/null +++ b/x/evidence/client/rest/query.go @@ -0,0 +1,91 @@ +package rest + +import ( + "fmt" + "net/http" + "strings" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + "github.com/gorilla/mux" +) + +func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc( + fmt.Sprintf("/evidence/{%s}", RestParamEvidenceHash), + queryEvidenceHandler(cliCtx), + ).Methods(MethodGet) + + r.HandleFunc( + "/evidence", + queryAllEvidenceHandler(cliCtx), + ).Methods(MethodGet) +} + +func queryEvidenceHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + evidenceHash := vars[RestParamEvidenceHash] + + if strings.TrimSpace(evidenceHash) == "" { + rest.WriteErrorResponse(w, http.StatusBadRequest, "evidence hash required but not specified") + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewQueryEvidenceParams(evidenceHash) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryEvidence) + res, height, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryAllEvidenceHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewQueryAllEvidenceParams(page, limit) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryAllEvidence) + res, height, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} diff --git a/x/evidence/client/rest/rest.go b/x/evidence/client/rest/rest.go new file mode 100644 index 000000000..bb66afbbc --- /dev/null +++ b/x/evidence/client/rest/rest.go @@ -0,0 +1,30 @@ +package rest + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + + "github.com/gorilla/mux" +) + +// REST query and parameter values +const ( + RestParamEvidenceHash = "evidence-hash" + + MethodGet = "GET" +) + +// EvidenceRESTHandler defines a REST service evidence handler implemented in +// another module. The sub-route is mounted on the evidence REST handler. +type EvidenceRESTHandler struct { + SubRoute string + Handler func(http.ResponseWriter, *http.Request) +} + +// RegisterRoutes registers all Evidence submission handlers for the evidence module's +// REST service handler. +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, handlers []EvidenceRESTHandler) { + registerQueryRoutes(cliCtx, r) + registerTxRoutes(cliCtx, r, handlers) +} diff --git a/x/evidence/client/rest/tx.go b/x/evidence/client/rest/tx.go new file mode 100644 index 000000000..b053e4fca --- /dev/null +++ b/x/evidence/client/rest/tx.go @@ -0,0 +1,11 @@ +package rest + +import ( + "github.com/cosmos/cosmos-sdk/client/context" + + "github.com/gorilla/mux" +) + +func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router, handlers []EvidenceRESTHandler) { + // TODO: Register tx handlers. +} diff --git a/x/evidence/doc.go b/x/evidence/doc.go new file mode 100644 index 000000000..23fd4c9ae --- /dev/null +++ b/x/evidence/doc.go @@ -0,0 +1,44 @@ +/* +Package evidence implements a Cosmos SDK module, per ADR 009, that allows for the +submission and handling of arbitrary evidence of misbehavior. + +All concrete evidence types must implement the Evidence interface contract. Submitted +evidence is first routed through the evidence module's Router in which it attempts +to find a corresponding Handler for that specific evidence type. Each evidence type +must have a Handler registered with the evidence module's keeper in order for it +to be successfully executed. + +Each corresponding handler must also fulfill the Handler interface contract. The +Handler for a given Evidence type can perform any arbitrary state transitions +such as slashing, jailing, and tombstoning. This provides developers with great +flexibility in designing evidence handling. + +A full setup of the evidence module may look something as follows: + + ModuleBasics = module.NewBasicManager( + // ..., + evidence.AppModuleBasic{}, + ) + + // First, create the keeper's subspace for parameters and the keeper itself. + evidenceParamspace := app.ParamsKeeper.Subspace(evidence.DefaultParamspace) + evidenceKeeper := evidence.NewKeeper( + app.cdc, keys[evidence.StoreKey], evidenceParamspace, evidence.DefaultCodespace, + ) + + + // Second, create the evidence Handler and register all desired routes. + evidenceRouter := evidence.NewRouter(). + AddRoute(evidenceRoute, evidenceHandler). + AddRoute(..., ...) + + evidenceKeeper.SetRouter(evidenceRouter) + + app.mm = module.NewManager( + // ... + evidence.NewAppModule(evidenceKeeper), + ) + + // Remaining application bootstrapping... +*/ +package evidence diff --git a/x/evidence/exported/evidence.go b/x/evidence/exported/evidence.go new file mode 100644 index 000000000..1c6adfdf8 --- /dev/null +++ b/x/evidence/exported/evidence.go @@ -0,0 +1,29 @@ +package exported + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + cmn "github.com/tendermint/tendermint/libs/common" +) + +// Evidence defines the contract which concrete evidence types of misbehavior +// must implement. +type Evidence interface { + Route() string + Type() string + String() string + Hash() cmn.HexBytes + ValidateBasic() error + + // The consensus address of the malicious validator at time of infraction + GetConsensusAddress() sdk.ConsAddress + + // Height at which the infraction occurred + GetHeight() int64 + + // The total power of the malicious validator at time of infraction + GetValidatorPower() int64 + + // The total validator set power at time of infraction + GetTotalPower() int64 +} diff --git a/x/evidence/genesis.go b/x/evidence/genesis.go new file mode 100644 index 000000000..b06c7ce96 --- /dev/null +++ b/x/evidence/genesis.go @@ -0,0 +1,30 @@ +package evidence + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// InitGenesis initializes the evidence module's state from a provided genesis +// state. +func InitGenesis(ctx sdk.Context, k Keeper, gs GenesisState) { + if err := gs.Validate(); err != nil { + panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) + } + + for _, e := range gs.Evidence { + if _, ok := k.GetEvidence(ctx, e.Hash()); ok { + panic(fmt.Sprintf("evidence with hash %s already exists", e.Hash())) + } + + k.SetEvidence(ctx, e) + } +} + +// ExportGenesis returns the evidence module's exported genesis. +func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { + return GenesisState{ + Evidence: k.GetAllEvidence(ctx), + } +} diff --git a/x/evidence/genesis_test.go b/x/evidence/genesis_test.go new file mode 100644 index 000000000..3f3e3abac --- /dev/null +++ b/x/evidence/genesis_test.go @@ -0,0 +1,111 @@ +package evidence_test + +import ( + "testing" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + "github.com/stretchr/testify/suite" +) + +type GenesisTestSuite struct { + suite.Suite + + ctx sdk.Context + keeper evidence.Keeper +} + +func (suite *GenesisTestSuite) SetupTest() { + checkTx := false + app := simapp.Setup(checkTx) + + // get the app's codec and register custom testing types + cdc := app.Codec() + cdc.RegisterConcrete(types.TestEquivocationEvidence{}, "test/TestEquivocationEvidence", nil) + + // recreate keeper in order to use custom testing types + evidenceKeeper := evidence.NewKeeper( + cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), + evidence.DefaultCodespace, + ) + router := evidence.NewRouter() + router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) + evidenceKeeper.SetRouter(router) + + suite.ctx = app.BaseApp.NewContext(checkTx, abci.Header{Height: 1}) + suite.keeper = *evidenceKeeper +} + +func (suite *GenesisTestSuite) TestInitGenesis_Valid() { + pk := ed25519.GenPrivKey() + + testEvidence := make([]exported.Evidence, 100) + for i := 0; i < 100; i++ { + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: int64(i), + Round: 0, + } + sig, err := pk.Sign(sv.SignBytes("test-chain")) + suite.NoError(err) + sv.Signature = sig + + testEvidence[i] = types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: sv, + } + } + + suite.NotPanics(func() { + evidence.InitGenesis(suite.ctx, suite.keeper, evidence.NewGenesisState(testEvidence)) + }) + + for _, e := range testEvidence { + _, ok := suite.keeper.GetEvidence(suite.ctx, e.Hash()) + suite.True(ok) + } +} + +func (suite *GenesisTestSuite) TestInitGenesis_Invalid() { + pk := ed25519.GenPrivKey() + + testEvidence := make([]exported.Evidence, 100) + for i := 0; i < 100; i++ { + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: int64(i), + Round: 0, + } + sig, err := pk.Sign(sv.SignBytes("test-chain")) + suite.NoError(err) + sv.Signature = sig + + testEvidence[i] = types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: types.TestVote{Height: 10, Round: 1}, + } + } + + suite.Panics(func() { + evidence.InitGenesis(suite.ctx, suite.keeper, evidence.NewGenesisState(testEvidence)) + }) + + suite.Empty(suite.keeper.GetAllEvidence(suite.ctx)) +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} diff --git a/x/evidence/handler.go b/x/evidence/handler.go new file mode 100644 index 000000000..714da444c --- /dev/null +++ b/x/evidence/handler.go @@ -0,0 +1,40 @@ +package evidence + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + switch msg := msg.(type) { + case MsgSubmitEvidence: + return handleMsgSubmitEvidence(ctx, k, msg) + + default: + return sdk.ErrUnknownRequest(fmt.Sprintf("unrecognized %s message type: %T", ModuleName, msg)).Result() + } + } +} + +func handleMsgSubmitEvidence(ctx sdk.Context, k Keeper, msg MsgSubmitEvidence) sdk.Result { + if err := k.SubmitEvidence(ctx, msg.Evidence); err != nil { + return sdk.ConvertError(err).Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Submitter.String()), + ), + ) + + return sdk.Result{ + Data: msg.Evidence.Hash(), + Events: ctx.EventManager().Events(), + } +} diff --git a/x/evidence/handler_test.go b/x/evidence/handler_test.go new file mode 100644 index 000000000..0b286817e --- /dev/null +++ b/x/evidence/handler_test.go @@ -0,0 +1,103 @@ +package evidence_test + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +type HandlerTestSuite struct { + suite.Suite + + ctx sdk.Context + handler sdk.Handler + keeper evidence.Keeper +} + +func (suite *HandlerTestSuite) SetupTest() { + checkTx := false + app := simapp.Setup(checkTx) + + // get the app's codec and register custom testing types + cdc := app.Codec() + cdc.RegisterConcrete(types.TestEquivocationEvidence{}, "test/TestEquivocationEvidence", nil) + + // recreate keeper in order to use custom testing types + evidenceKeeper := evidence.NewKeeper( + cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), + evidence.DefaultCodespace, + ) + router := evidence.NewRouter() + router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) + evidenceKeeper.SetRouter(router) + + suite.ctx = app.BaseApp.NewContext(checkTx, abci.Header{Height: 1}) + suite.handler = evidence.NewHandler(*evidenceKeeper) + suite.keeper = *evidenceKeeper +} + +func (suite *HandlerTestSuite) TestMsgSubmitEvidence_Valid() { + pk := ed25519.GenPrivKey() + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: 11, + Round: 0, + } + + sig, err := pk.Sign(sv.SignBytes(suite.ctx.ChainID())) + suite.NoError(err) + sv.Signature = sig + + s := sdk.AccAddress("test") + e := types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: sv, + } + + ctx := suite.ctx.WithIsCheckTx(false) + msg := evidence.NewMsgSubmitEvidence(e, s) + res := suite.handler(ctx, msg) + suite.True(res.IsOK()) + suite.Equal(e.Hash().Bytes(), res.Data) +} + +func (suite *HandlerTestSuite) TestMsgSubmitEvidence_Invalid() { + pk := ed25519.GenPrivKey() + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: 11, + Round: 0, + } + + sig, err := pk.Sign(sv.SignBytes(suite.ctx.ChainID())) + suite.NoError(err) + sv.Signature = sig + + s := sdk.AccAddress("test") + e := types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: types.TestVote{Height: 10, Round: 1}, + } + + ctx := suite.ctx.WithIsCheckTx(false) + msg := evidence.NewMsgSubmitEvidence(e, s) + res := suite.handler(ctx, msg) + suite.False(res.IsOK()) +} + +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} diff --git a/x/evidence/internal/keeper/keeper.go b/x/evidence/internal/keeper/keeper.go new file mode 100644 index 000000000..fa79aac7b --- /dev/null +++ b/x/evidence/internal/keeper/keeper.go @@ -0,0 +1,147 @@ +package keeper + +import ( + "fmt" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// Keeper defines the evidence module's keeper. The keeper is responsible for +// managing persistence, state transitions and query handling for the evidence +// module. +type Keeper struct { + cdc *codec.Codec + storeKey sdk.StoreKey + paramSpace params.Subspace + router types.Router + codespace sdk.CodespaceType +} + +func NewKeeper( + cdc *codec.Codec, storeKey sdk.StoreKey, paramSpace params.Subspace, codespace sdk.CodespaceType, +) *Keeper { + + return &Keeper{ + cdc: cdc, + storeKey: storeKey, + paramSpace: paramSpace, + codespace: codespace, + } +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +// SetRouter sets the Evidence Handler router for the x/evidence module. Note, +// we allow the ability to set the router after the Keeper is constructed as a +// given Handler may need access the Keeper before being constructed. The router +// may only be set once and will be sealed if it's not already sealed. +func (k *Keeper) SetRouter(rtr types.Router) { + // It is vital to seal the Evidence Handler router as to not allow further + // handlers to be registered after the keeper is created since this + // could create invalid or non-deterministic behavior. + if !rtr.Sealed() { + rtr.Seal() + } + if k.router != nil { + panic(fmt.Sprintf("attempting to reset router on x/%s", types.ModuleName)) + } + + k.router = rtr +} + +// GetEvidenceHandler returns a registered Handler for a given Evidence type. If +// no handler exists, an error is returned. +func (k Keeper) GetEvidenceHandler(evidenceRoute string) (types.Handler, error) { + if !k.router.HasRoute(evidenceRoute) { + return nil, types.ErrNoEvidenceHandlerExists(k.codespace, evidenceRoute) + } + + return k.router.GetRoute(evidenceRoute), nil +} + +// SubmitEvidence attempts to match evidence against the keepers router and execute +// the corresponding registered Evidence Handler. An error is returned if no +// registered Handler exists or if the Handler fails. Otherwise, the evidence is +// persisted. +func (k Keeper) SubmitEvidence(ctx sdk.Context, evidence exported.Evidence) error { + if _, ok := k.GetEvidence(ctx, evidence.Hash()); ok { + return types.ErrEvidenceExists(k.codespace, evidence.Hash().String()) + } + if !k.router.HasRoute(evidence.Route()) { + return types.ErrNoEvidenceHandlerExists(k.codespace, evidence.Route()) + } + + handler := k.router.GetRoute(evidence.Route()) + if err := handler(ctx, evidence); err != nil { + return types.ErrInvalidEvidence(k.codespace, err.Error()) + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeSubmitEvidence, + sdk.NewAttribute(types.AttributeKeyEvidenceHash, evidence.Hash().String()), + ), + ) + + k.SetEvidence(ctx, evidence) + return nil +} + +// SetEvidence sets Evidence by hash in the module's KVStore. +func (k Keeper) SetEvidence(ctx sdk.Context, evidence exported.Evidence) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixEvidence) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(evidence) + store.Set(evidence.Hash(), bz) +} + +// GetEvidence retrieves Evidence by hash if it exists. If no Evidence exists for +// the given hash, (nil, false) is returned. +func (k Keeper) GetEvidence(ctx sdk.Context, hash cmn.HexBytes) (evidence exported.Evidence, found bool) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixEvidence) + + bz := store.Get(hash) + if len(bz) == 0 { + return nil, false + } + + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &evidence) + return evidence, true +} + +// IterateEvidence provides an interator over all stored Evidence objects. For +// each Evidence object, cb will be called. If the cb returns true, the iterator +// will close and stop. +func (k Keeper) IterateEvidence(ctx sdk.Context, cb func(exported.Evidence) bool) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixEvidence) + iterator := sdk.KVStorePrefixIterator(store, nil) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var evidence exported.Evidence + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &evidence) + + if cb(evidence) { + break + } + } +} + +// GetAllEvidence returns all stored Evidence objects. +func (k Keeper) GetAllEvidence(ctx sdk.Context) (evidence []exported.Evidence) { + k.IterateEvidence(ctx, func(e exported.Evidence) bool { + evidence = append(evidence, e) + return false + }) + return evidence +} diff --git a/x/evidence/internal/keeper/keeper_test.go b/x/evidence/internal/keeper/keeper_test.go new file mode 100644 index 000000000..2f7862283 --- /dev/null +++ b/x/evidence/internal/keeper/keeper_test.go @@ -0,0 +1,181 @@ +package keeper_test + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/keeper" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +type KeeperTestSuite struct { + suite.Suite + + ctx sdk.Context + querier sdk.Querier + keeper keeper.Keeper +} + +func (suite *KeeperTestSuite) SetupTest() { + checkTx := false + app := simapp.Setup(checkTx) + + // get the app's codec and register custom testing types + cdc := app.Codec() + cdc.RegisterConcrete(types.TestEquivocationEvidence{}, "test/TestEquivocationEvidence", nil) + + // recreate keeper in order to use custom testing types + evidenceKeeper := evidence.NewKeeper( + cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), + evidence.DefaultCodespace, + ) + router := evidence.NewRouter() + router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) + evidenceKeeper.SetRouter(router) + + suite.ctx = app.BaseApp.NewContext(checkTx, abci.Header{Height: 1}) + suite.querier = keeper.NewQuerier(*evidenceKeeper) + suite.keeper = *evidenceKeeper +} + +func (suite *KeeperTestSuite) populateEvidence(ctx sdk.Context, numEvidence int) []exported.Evidence { + evidence := make([]exported.Evidence, numEvidence) + + for i := 0; i < numEvidence; i++ { + pk := ed25519.GenPrivKey() + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: int64(i), + Round: 0, + } + + sig, err := pk.Sign(sv.SignBytes(ctx.ChainID())) + suite.NoError(err) + sv.Signature = sig + + evidence[i] = types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: sv, + } + + suite.Nil(suite.keeper.SubmitEvidence(ctx, evidence[i])) + } + + return evidence +} + +func (suite *KeeperTestSuite) TestSubmitValidEvidence() { + ctx := suite.ctx.WithIsCheckTx(false) + pk := ed25519.GenPrivKey() + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: 11, + Round: 0, + } + + sig, err := pk.Sign(sv.SignBytes(ctx.ChainID())) + suite.NoError(err) + sv.Signature = sig + + e := types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: sv, + } + + suite.Nil(suite.keeper.SubmitEvidence(ctx, e)) + + res, ok := suite.keeper.GetEvidence(ctx, e.Hash()) + suite.True(ok) + suite.Equal(e, res) +} + +func (suite *KeeperTestSuite) TestSubmitValidEvidence_Duplicate() { + ctx := suite.ctx.WithIsCheckTx(false) + pk := ed25519.GenPrivKey() + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: 11, + Round: 0, + } + + sig, err := pk.Sign(sv.SignBytes(ctx.ChainID())) + suite.NoError(err) + sv.Signature = sig + + e := types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: sv, + } + + suite.Nil(suite.keeper.SubmitEvidence(ctx, e)) + suite.Error(suite.keeper.SubmitEvidence(ctx, e)) + + res, ok := suite.keeper.GetEvidence(ctx, e.Hash()) + suite.True(ok) + suite.Equal(e, res) +} + +func (suite *KeeperTestSuite) TestSubmitInvalidEvidence() { + ctx := suite.ctx.WithIsCheckTx(false) + pk := ed25519.GenPrivKey() + e := types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: 10, + Round: 0, + }, + VoteB: types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: 11, + Round: 0, + }, + } + + suite.Error(suite.keeper.SubmitEvidence(ctx, e)) + + res, ok := suite.keeper.GetEvidence(ctx, e.Hash()) + suite.False(ok) + suite.Nil(res) +} + +func (suite *KeeperTestSuite) TestIterateEvidence() { + ctx := suite.ctx.WithIsCheckTx(false) + numEvidence := 100 + suite.populateEvidence(ctx, numEvidence) + + evidence := suite.keeper.GetAllEvidence(ctx) + suite.Len(evidence, numEvidence) +} + +func (suite *KeeperTestSuite) TestGetEvidenceHandler() { + handler, err := suite.keeper.GetEvidenceHandler(types.TestEquivocationEvidence{}.Route()) + suite.NoError(err) + suite.NotNil(handler) + + handler, err = suite.keeper.GetEvidenceHandler("invalidHandler") + suite.Error(err) + suite.Nil(handler) +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} diff --git a/x/evidence/internal/keeper/querier.go b/x/evidence/internal/keeper/querier.go new file mode 100644 index 000000000..fa6ef17c0 --- /dev/null +++ b/x/evidence/internal/keeper/querier.go @@ -0,0 +1,87 @@ +package keeper + +import ( + "encoding/hex" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + abci "github.com/tendermint/tendermint/abci/types" +) + +func NewQuerier(k Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, sdk.Error) { + var ( + res []byte + err error + ) + + switch path[0] { + case types.QueryEvidence: + res, err = queryEvidence(ctx, path[1:], req, k) + + case types.QueryAllEvidence: + res, err = queryAllEvidence(ctx, path[1:], req, k) + + default: + err = sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) + } + + return res, sdk.ConvertError(err) + } +} + +func queryEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) ([]byte, error) { + var params types.QueryEvidenceParams + + err := k.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + hash, err := hex.DecodeString(params.EvidenceHash) + if err != nil { + return nil, sdkerrors.Wrap(err, "failed to decode evidence hash string query") + } + + evidence, ok := k.GetEvidence(ctx, hash) + if !ok { + return nil, types.ErrNoEvidenceExists(k.codespace, params.EvidenceHash) + } + + res, err := codec.MarshalJSONIndent(k.cdc, evidence) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return res, nil +} + +func queryAllEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) ([]byte, error) { + var params types.QueryAllEvidenceParams + + err := k.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + evidence := k.GetAllEvidence(ctx) + + start, end := client.Paginate(len(evidence), params.Page, params.Limit, 100) + if start < 0 || end < 0 { + evidence = []exported.Evidence{} + } else { + evidence = evidence[start:end] + } + + res, err := codec.MarshalJSONIndent(k.cdc, evidence) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return res, nil +} diff --git a/x/evidence/internal/keeper/querier_test.go b/x/evidence/internal/keeper/querier_test.go new file mode 100644 index 000000000..b656019ed --- /dev/null +++ b/x/evidence/internal/keeper/querier_test.go @@ -0,0 +1,86 @@ +package keeper_test + +import ( + "strings" + + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + abci "github.com/tendermint/tendermint/abci/types" +) + +const ( + custom = "custom" +) + +func (suite *KeeperTestSuite) TestQueryEvidence_Existing() { + ctx := suite.ctx.WithIsCheckTx(false) + numEvidence := 100 + + evidence := suite.populateEvidence(ctx, numEvidence) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryEvidence}, "/"), + Data: types.TestingCdc.MustMarshalJSON(types.NewQueryEvidenceParams(evidence[0].Hash().String())), + } + + bz, err := suite.querier(ctx, []string{types.QueryEvidence}, query) + suite.Nil(err) + suite.NotNil(bz) + + var e exported.Evidence + suite.Nil(types.TestingCdc.UnmarshalJSON(bz, &e)) + suite.Equal(evidence[0], e) +} + +func (suite *KeeperTestSuite) TestQueryEvidence_NonExisting() { + ctx := suite.ctx.WithIsCheckTx(false) + numEvidence := 100 + + suite.populateEvidence(ctx, numEvidence) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryEvidence}, "/"), + Data: types.TestingCdc.MustMarshalJSON(types.NewQueryEvidenceParams("0000000000000000000000000000000000000000000000000000000000000000")), + } + + bz, err := suite.querier(ctx, []string{types.QueryEvidence}, query) + suite.NotNil(err) + suite.Nil(bz) +} + +func (suite *KeeperTestSuite) TestQueryAllEvidence() { + ctx := suite.ctx.WithIsCheckTx(false) + numEvidence := 100 + + suite.populateEvidence(ctx, numEvidence) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryAllEvidence}, "/"), + Data: types.TestingCdc.MustMarshalJSON(types.NewQueryAllEvidenceParams(1, numEvidence)), + } + + bz, err := suite.querier(ctx, []string{types.QueryAllEvidence}, query) + suite.Nil(err) + suite.NotNil(bz) + + var e []exported.Evidence + suite.Nil(types.TestingCdc.UnmarshalJSON(bz, &e)) + suite.Len(e, numEvidence) +} + +func (suite *KeeperTestSuite) TestQueryAllEvidence_InvalidPagination() { + ctx := suite.ctx.WithIsCheckTx(false) + numEvidence := 100 + + suite.populateEvidence(ctx, numEvidence) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryAllEvidence}, "/"), + Data: types.TestingCdc.MustMarshalJSON(types.NewQueryAllEvidenceParams(0, numEvidence)), + } + + bz, err := suite.querier(ctx, []string{types.QueryAllEvidence}, query) + suite.Nil(err) + suite.NotNil(bz) + + var e []exported.Evidence + suite.Nil(types.TestingCdc.UnmarshalJSON(bz, &e)) + suite.Len(e, 0) +} diff --git a/x/evidence/internal/types/codec.go b/x/evidence/internal/types/codec.go new file mode 100644 index 000000000..72fb044e9 --- /dev/null +++ b/x/evidence/internal/types/codec.go @@ -0,0 +1,28 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" +) + +// ModuleCdc defines the evidence module's codec. The codec is not sealed as to +// allow other modules to register their concrete Evidence types. +var ModuleCdc = codec.New() + +// RegisterCodec registers all the necessary types and interfaces for the +// evidence module. +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*exported.Evidence)(nil), nil) + cdc.RegisterConcrete(MsgSubmitEvidence{}, "cosmos-sdk/MsgSubmitEvidence", nil) +} + +// RegisterEvidenceTypeCodec registers an external concrete Evidence type defined +// in another module for the internal ModuleCdc. This allows the MsgSubmitEvidence +// to be correctly Amino encoded and decoded. +func RegisterEvidenceTypeCodec(o interface{}, name string) { + ModuleCdc.RegisterConcrete(o, name, nil) +} + +func init() { + RegisterCodec(ModuleCdc) +} diff --git a/x/evidence/internal/types/codec_test.go b/x/evidence/internal/types/codec_test.go new file mode 100644 index 000000000..1edf6d97c --- /dev/null +++ b/x/evidence/internal/types/codec_test.go @@ -0,0 +1,42 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + cmn "github.com/tendermint/tendermint/libs/common" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +var _ exported.Evidence = (*testEvidence)(nil) + +type testEvidence struct{} + +func (te testEvidence) Route() string { return "" } +func (te testEvidence) Type() string { return "" } +func (te testEvidence) String() string { return "" } +func (te testEvidence) ValidateBasic() error { return nil } +func (te testEvidence) GetConsensusAddress() sdk.ConsAddress { return nil } +func (te testEvidence) Hash() cmn.HexBytes { return nil } +func (te testEvidence) GetHeight() int64 { return 0 } +func (te testEvidence) GetValidatorPower() int64 { return 0 } +func (te testEvidence) GetTotalPower() int64 { return 0 } + +func TestCodec(t *testing.T) { + cdc := codec.New() + types.RegisterCodec(cdc) + types.RegisterEvidenceTypeCodec(testEvidence{}, "cosmos-sdk/testEvidence") + + var e exported.Evidence = testEvidence{} + bz, err := cdc.MarshalBinaryBare(e) + require.NoError(t, err) + + var te testEvidence + require.NoError(t, cdc.UnmarshalBinaryBare(bz, &te)) + + require.Panics(t, func() { types.RegisterEvidenceTypeCodec(testEvidence{}, "cosmos-sdk/testEvidence") }) +} diff --git a/x/evidence/internal/types/errors.go b/x/evidence/internal/types/errors.go new file mode 100644 index 000000000..2054cee9f --- /dev/null +++ b/x/evidence/internal/types/errors.go @@ -0,0 +1,58 @@ +// DONTCOVER +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// Error codes specific to the evidence module +const ( + DefaultCodespace sdk.CodespaceType = ModuleName + + CodeNoEvidenceHandlerExists sdk.CodeType = 1 + CodeInvalidEvidence sdk.CodeType = 2 + CodeNoEvidenceExists sdk.CodeType = 3 + CodeEvidenceExists sdk.CodeType = 4 +) + +// ErrNoEvidenceHandlerExists returns a typed ABCI error for an invalid evidence +// handler route. +func ErrNoEvidenceHandlerExists(codespace sdk.CodespaceType, route string) error { + return sdkerrors.New( + string(codespace), + uint32(CodeNoEvidenceHandlerExists), + fmt.Sprintf("route '%s' does not have a registered evidence handler", route), + ) +} + +// ErrInvalidEvidence returns a typed ABCI error for invalid evidence. +func ErrInvalidEvidence(codespace sdk.CodespaceType, msg string) error { + return sdkerrors.New( + string(codespace), + uint32(CodeInvalidEvidence), + fmt.Sprintf("invalid evidence: %s", msg), + ) +} + +// ErrNoEvidenceExists returns a typed ABCI error for Evidence that does not exist +// for a given hash. +func ErrNoEvidenceExists(codespace sdk.CodespaceType, hash string) error { + return sdkerrors.New( + string(codespace), + uint32(CodeNoEvidenceExists), + fmt.Sprintf("evidence with hash %s does not exist", hash), + ) +} + +// ErrEvidenceExists returns a typed ABCI error for Evidence that already exists +// by hash in state. +func ErrEvidenceExists(codespace sdk.CodespaceType, hash string) error { + return sdkerrors.New( + string(codespace), + uint32(CodeEvidenceExists), + fmt.Sprintf("evidence with hash %s already exists", hash), + ) +} diff --git a/x/evidence/internal/types/events.go b/x/evidence/internal/types/events.go new file mode 100644 index 000000000..fe468c43e --- /dev/null +++ b/x/evidence/internal/types/events.go @@ -0,0 +1,9 @@ +package types + +// evidence module events +const ( + EventTypeSubmitEvidence = "submit_evidence" + + AttributeValueCategory = "evidence" + AttributeKeyEvidenceHash = "evidence_hash" +) diff --git a/x/evidence/internal/types/genesis.go b/x/evidence/internal/types/genesis.go new file mode 100644 index 000000000..4013b573f --- /dev/null +++ b/x/evidence/internal/types/genesis.go @@ -0,0 +1,31 @@ +package types + +import "github.com/cosmos/cosmos-sdk/x/evidence/exported" + +// DONTCOVER + +// GenesisState defines the evidence module's genesis state. +type GenesisState struct { + Evidence []exported.Evidence `json:"evidence" yaml:"evidence"` +} + +func NewGenesisState(e []exported.Evidence) GenesisState { + return GenesisState{Evidence: e} +} + +// DefaultGenesisState returns the evidence module's default genesis state. +func DefaultGenesisState() GenesisState { + return GenesisState{Evidence: []exported.Evidence{}} +} + +// Validate performs basic gensis state validation returning an error upon any +// failure. +func (gs GenesisState) Validate() error { + for _, e := range gs.Evidence { + if err := e.ValidateBasic(); err != nil { + return err + } + } + + return nil +} diff --git a/x/evidence/internal/types/genesis_test.go b/x/evidence/internal/types/genesis_test.go new file mode 100644 index 000000000..936877ad3 --- /dev/null +++ b/x/evidence/internal/types/genesis_test.go @@ -0,0 +1,71 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/ed25519" + + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +func TestDefaultGenesisState(t *testing.T) { + gs := types.DefaultGenesisState() + require.NotNil(t, gs.Evidence) + require.Len(t, gs.Evidence, 0) +} + +func TestGenesisStateValidate_Valid(t *testing.T) { + pk := ed25519.GenPrivKey() + + evidence := make([]exported.Evidence, 100) + for i := 0; i < 100; i++ { + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: int64(i), + Round: 0, + } + sig, err := pk.Sign(sv.SignBytes("test-chain")) + require.NoError(t, err) + sv.Signature = sig + + evidence[i] = types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: sv, + } + } + + gs := types.NewGenesisState(evidence) + require.NoError(t, gs.Validate()) +} + +func TestGenesisStateValidate_Invalid(t *testing.T) { + pk := ed25519.GenPrivKey() + + evidence := make([]exported.Evidence, 100) + for i := 0; i < 100; i++ { + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: int64(i), + Round: 0, + } + sig, err := pk.Sign(sv.SignBytes("test-chain")) + require.NoError(t, err) + sv.Signature = sig + + evidence[i] = types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: types.TestVote{Height: 10, Round: 1}, + } + } + + gs := types.NewGenesisState(evidence) + require.Error(t, gs.Validate()) +} diff --git a/x/evidence/internal/types/keys.go b/x/evidence/internal/types/keys.go new file mode 100644 index 000000000..702f76d9b --- /dev/null +++ b/x/evidence/internal/types/keys.go @@ -0,0 +1,23 @@ +package types + +const ( + // ModuleName defines the module name + ModuleName = "evidence" + + // StoreKey defines the primary module store key + StoreKey = ModuleName + + // RouterKey defines the module's message routing key + RouterKey = ModuleName + + // QuerierRoute defines the module's query routing key + QuerierRoute = ModuleName + + // DefaultParamspace defines the module's default paramspace name + DefaultParamspace = ModuleName +) + +// KVStore key prefixes +var ( + KeyPrefixEvidence = []byte{0x00} +) diff --git a/x/evidence/internal/types/msgs.go b/x/evidence/internal/types/msgs.go new file mode 100644 index 000000000..b09f120da --- /dev/null +++ b/x/evidence/internal/types/msgs.go @@ -0,0 +1,59 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" +) + +// Message types for the evidence module +const ( + TypeMsgSubmitEvidence = "submit_evidence" +) + +var ( + _ sdk.Msg = MsgSubmitEvidence{} +) + +// MsgSubmitEvidence defines an sdk.Msg type that supports submitting arbitrary +// Evidence. +type MsgSubmitEvidence struct { + Evidence exported.Evidence `json:"evidence" yaml:"evidence"` + Submitter sdk.AccAddress `json:"submitter" yaml:"submitter"` +} + +func NewMsgSubmitEvidence(e exported.Evidence, s sdk.AccAddress) MsgSubmitEvidence { + return MsgSubmitEvidence{Evidence: e, Submitter: s} +} + +// Route returns the MsgSubmitEvidence's route. +func (m MsgSubmitEvidence) Route() string { return RouterKey } + +// Type returns the MsgSubmitEvidence's type. +func (m MsgSubmitEvidence) Type() string { return TypeMsgSubmitEvidence } + +// ValidateBasic performs basic (non-state-dependant) validation on a MsgSubmitEvidence. +func (m MsgSubmitEvidence) ValidateBasic() sdk.Error { + if m.Evidence == nil { + return sdk.ConvertError(ErrInvalidEvidence(DefaultCodespace, "missing evidence")) + } + if err := m.Evidence.ValidateBasic(); err != nil { + return sdk.ConvertError(err) + } + if m.Submitter.Empty() { + return sdk.ConvertError(sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, m.Submitter.String())) + } + + return nil +} + +// GetSignBytes returns the raw bytes a signer is expected to sign when submitting +// a MsgSubmitEvidence message. +func (m MsgSubmitEvidence) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(m)) +} + +// GetSigners returns the single expected signer for a MsgSubmitEvidence. +func (m MsgSubmitEvidence) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{m.Submitter} +} diff --git a/x/evidence/internal/types/msgs_test.go b/x/evidence/internal/types/msgs_test.go new file mode 100644 index 000000000..5a50ba6e7 --- /dev/null +++ b/x/evidence/internal/types/msgs_test.go @@ -0,0 +1,66 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +func TestMsgSubmitEvidence(t *testing.T) { + pk := ed25519.GenPrivKey() + sv := types.TestVote{ + ValidatorAddress: pk.PubKey().Address(), + Height: 11, + Round: 0, + } + sig, err := pk.Sign(sv.SignBytes("test-chain")) + require.NoError(t, err) + sv.Signature = sig + + submitter := sdk.AccAddress("test") + testCases := []struct { + evidence exported.Evidence + submitter sdk.AccAddress + expectErr bool + }{ + {nil, submitter, true}, + { + types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: sv, + }, + submitter, + false, + }, + { + types.TestEquivocationEvidence{ + Power: 100, + TotalPower: 100000, + PubKey: pk.PubKey(), + VoteA: sv, + VoteB: types.TestVote{Height: 10, Round: 1}, + }, + submitter, + true, + }, + } + + for i, tc := range testCases { + msg := types.NewMsgSubmitEvidence(tc.evidence, tc.submitter) + require.Equal(t, msg.Route(), types.RouterKey, "unexpected result for tc #%d", i) + require.Equal(t, msg.Type(), types.TypeMsgSubmitEvidence, "unexpected result for tc #%d", i) + require.Equal(t, tc.expectErr, msg.ValidateBasic() != nil, "unexpected result for tc #%d", i) + + if !tc.expectErr { + require.Equal(t, msg.GetSigners(), []sdk.AccAddress{tc.submitter}, "unexpected result for tc #%d", i) + } + } +} diff --git a/x/evidence/internal/types/querier.go b/x/evidence/internal/types/querier.go new file mode 100644 index 000000000..af643ee7c --- /dev/null +++ b/x/evidence/internal/types/querier.go @@ -0,0 +1,26 @@ +package types + +// Querier routes for the evidence module +const ( + QueryEvidence = "evidence" + QueryAllEvidence = "all_evidence" +) + +// QueryEvidenceParams defines the parameters necessary for querying Evidence. +type QueryEvidenceParams struct { + EvidenceHash string `json:"evidence_hash" yaml:"evidence_hash"` +} + +func NewQueryEvidenceParams(hash string) QueryEvidenceParams { + return QueryEvidenceParams{EvidenceHash: hash} +} + +// QueryAllEvidenceParams defines the parameters necessary for querying for all Evidence. +type QueryAllEvidenceParams struct { + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` +} + +func NewQueryAllEvidenceParams(page, limit int) QueryAllEvidenceParams { + return QueryAllEvidenceParams{Page: page, Limit: limit} +} diff --git a/x/evidence/internal/types/router.go b/x/evidence/internal/types/router.go new file mode 100644 index 000000000..343c72e41 --- /dev/null +++ b/x/evidence/internal/types/router.go @@ -0,0 +1,81 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" +) + +type ( + // Handler defines an agnostic Evidence handler. The handler is responsible + // for executing all corresponding business logic necessary for verifying the + // evidence as valid. In addition, the Handler may execute any necessary + // slashing and potential jailing. + Handler func(sdk.Context, exported.Evidence) error + + // Router defines a contract for which any Evidence handling module must + // implement in order to route Evidence to registered Handlers. + Router interface { + AddRoute(r string, h Handler) Router + HasRoute(r string) bool + GetRoute(path string) Handler + Seal() + Sealed() bool + } + + router struct { + routes map[string]Handler + sealed bool + } +) + +func NewRouter() Router { + return &router{ + routes: make(map[string]Handler), + } +} + +// Seal prevents the router from any subsequent route handlers to be registered. +// Seal will panic if called more than once. +func (rtr *router) Seal() { + if rtr.sealed { + panic("router already sealed") + } + rtr.sealed = true +} + +// Sealed returns a boolean signifying if the Router is sealed or not. +func (rtr router) Sealed() bool { + return rtr.sealed +} + +// AddRoute adds a governance handler for a given path. It returns the Router +// so AddRoute calls can be linked. It will panic if the router is sealed. +func (rtr *router) AddRoute(path string, h Handler) Router { + if rtr.sealed { + panic(fmt.Sprintf("router sealed; cannot register %s route handler", path)) + } + if !sdk.IsAlphaNumeric(path) { + panic("route expressions can only contain alphanumeric characters") + } + if rtr.HasRoute(path) { + panic(fmt.Sprintf("route %s has already been registered", path)) + } + + rtr.routes[path] = h + return rtr +} + +// HasRoute returns true if the router has a path registered or false otherwise. +func (rtr *router) HasRoute(path string) bool { + return rtr.routes[path] != nil +} + +// GetRoute returns a Handler for a given path. +func (rtr *router) GetRoute(path string) Handler { + if !rtr.HasRoute(path) { + panic(fmt.Sprintf("route does not exist for path %s", path)) + } + return rtr.routes[path] +} diff --git a/x/evidence/internal/types/router_test.go b/x/evidence/internal/types/router_test.go new file mode 100644 index 000000000..b8cb42d2d --- /dev/null +++ b/x/evidence/internal/types/router_test.go @@ -0,0 +1,28 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +func testHandler(sdk.Context, exported.Evidence) error { return nil } + +func TestRouterSeal(t *testing.T) { + r := types.NewRouter() + r.Seal() + require.Panics(t, func() { r.AddRoute("test", nil) }) + require.Panics(t, func() { r.Seal() }) +} + +func TestRouter(t *testing.T) { + r := types.NewRouter() + r.AddRoute("test", testHandler) + require.True(t, r.HasRoute("test")) + require.Panics(t, func() { r.AddRoute("test", testHandler) }) + require.Panics(t, func() { r.AddRoute(" ", testHandler) }) +} diff --git a/x/evidence/internal/types/test_util.go b/x/evidence/internal/types/test_util.go new file mode 100644 index 000000000..b138a352b --- /dev/null +++ b/x/evidence/internal/types/test_util.go @@ -0,0 +1,135 @@ +/* +Common testing types and utility functions and methods to be used in unit and +integration testing of the evidence module. +*/ +// DONTCOVER +package types + +import ( + "bytes" + "errors" + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + + "gopkg.in/yaml.v2" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/tmhash" + cmn "github.com/tendermint/tendermint/libs/common" +) + +var ( + _ exported.Evidence = (*TestEquivocationEvidence)(nil) + + TestingCdc = codec.New() +) + +const ( + TestEvidenceRouteEquivocation = "TestEquivocationEvidence" + TestEvidenceTypeEquivocation = "equivocation" +) + +type ( + TestVote struct { + Height int64 + Round int64 + Timestamp time.Time + ValidatorAddress cmn.HexBytes + Signature []byte + } + + TestCanonicalVote struct { + Height int64 + Round int64 + Timestamp time.Time + ChainID string + } + + TestEquivocationEvidence struct { + Power int64 + TotalPower int64 + PubKey crypto.PubKey + VoteA TestVote + VoteB TestVote + } +) + +func init() { + RegisterCodec(TestingCdc) + codec.RegisterCrypto(TestingCdc) + TestingCdc.RegisterConcrete(TestEquivocationEvidence{}, "test/TestEquivocationEvidence", nil) +} + +func (e TestEquivocationEvidence) Route() string { return TestEvidenceRouteEquivocation } +func (e TestEquivocationEvidence) Type() string { return TestEvidenceTypeEquivocation } +func (e TestEquivocationEvidence) GetHeight() int64 { return e.VoteA.Height } +func (e TestEquivocationEvidence) GetValidatorPower() int64 { return e.Power } +func (e TestEquivocationEvidence) GetTotalPower() int64 { return e.TotalPower } + +func (e TestEquivocationEvidence) String() string { + bz, _ := yaml.Marshal(e) + return string(bz) +} + +func (e TestEquivocationEvidence) GetConsensusAddress() sdk.ConsAddress { + return sdk.ConsAddress(e.PubKey.Address()) +} + +func (e TestEquivocationEvidence) ValidateBasic() error { + if e.VoteA.Height != e.VoteB.Height || + e.VoteA.Round != e.VoteB.Round { + return fmt.Errorf("H/R/S does not match (got %v and %v)", e.VoteA, e.VoteB) + } + + if !bytes.Equal(e.VoteA.ValidatorAddress, e.VoteB.ValidatorAddress) { + return fmt.Errorf( + "validator addresses do not match (got %X and %X)", + e.VoteA.ValidatorAddress, + e.VoteB.ValidatorAddress, + ) + } + + return nil +} + +func (e TestEquivocationEvidence) Hash() cmn.HexBytes { + return tmhash.Sum(TestingCdc.MustMarshalBinaryBare(e)) +} + +func (v TestVote) SignBytes(chainID string) []byte { + scv := TestCanonicalVote{ + Height: v.Height, + Round: v.Round, + Timestamp: v.Timestamp, + ChainID: chainID, + } + bz, _ := TestingCdc.MarshalBinaryLengthPrefixed(scv) + return bz +} + +func TestEquivocationHandler(k interface{}) Handler { + return func(ctx sdk.Context, e exported.Evidence) error { + if err := e.ValidateBasic(); err != nil { + return err + } + + ee, ok := e.(TestEquivocationEvidence) + if !ok { + return fmt.Errorf("unexpected evidence type: %T", e) + } + if !ee.PubKey.VerifyBytes(ee.VoteA.SignBytes(ctx.ChainID()), ee.VoteA.Signature) { + return errors.New("failed to verify vote A signature") + } + if !ee.PubKey.VerifyBytes(ee.VoteB.SignBytes(ctx.ChainID()), ee.VoteB.Signature) { + return errors.New("failed to verify vote B signature") + } + + // TODO: Slashing! + + return nil + } +} diff --git a/x/evidence/module.go b/x/evidence/module.go new file mode 100644 index 000000000..a66a35adf --- /dev/null +++ b/x/evidence/module.go @@ -0,0 +1,167 @@ +package evidence + +import ( + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/evidence/client" + "github.com/cosmos/cosmos-sdk/x/evidence/client/cli" + "github.com/cosmos/cosmos-sdk/x/evidence/client/rest" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + + // TODO: Enable simulation once concrete types are defined. + // _ module.AppModuleSimulation = AppModuleSimulation{} +) + +// ---------------------------------------------------------------------------- +// AppModuleBasic +// ---------------------------------------------------------------------------- + +// AppModuleBasic implements the AppModuleBasic interface for the evidence module. +type AppModuleBasic struct { + evidenceHandlers []client.EvidenceHandler // client evidence submission handlers +} + +func NewAppModuleBasic(evidenceHandlers ...client.EvidenceHandler) AppModuleBasic { + return AppModuleBasic{ + evidenceHandlers: evidenceHandlers, + } +} + +// Name returns the evidence module's name. +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec registers the evidence module's types to the provided codec. +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// DefaultGenesis returns the evidence module's default genesis state. +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// ValidateGenesis performs genesis state validation for the evidence module. +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) + if err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", ModuleName, err) + } + + return gs.Validate() +} + +// RegisterRESTRoutes registers the evidence module's REST service handlers. +func (a AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + evidenceRESTHandlers := make([]rest.EvidenceRESTHandler, len(a.evidenceHandlers)) + + for i, evidenceHandler := range a.evidenceHandlers { + evidenceRESTHandlers[i] = evidenceHandler.RESTHandler(ctx) + } + + rest.RegisterRoutes(ctx, rtr, evidenceRESTHandlers) +} + +// GetTxCmd returns the evidence module's root tx command. +func (a AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + evidenceCLIHandlers := make([]*cobra.Command, len(a.evidenceHandlers)) + + for i, evidenceHandler := range a.evidenceHandlers { + evidenceCLIHandlers[i] = evidenceHandler.CLIHandler(cdc) + } + + return cli.GetTxCmd(StoreKey, cdc, evidenceCLIHandlers) +} + +// GetTxCmd returns the evidence module's root query command. +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetQueryCmd(StoreKey, cdc) +} + +// ---------------------------------------------------------------------------- +// AppModule +// ---------------------------------------------------------------------------- + +// AppModule implements the AppModule interface for the evidence module. +type AppModule struct { + AppModuleBasic + + keeper Keeper +} + +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: NewAppModuleBasic(), + keeper: keeper, + } +} + +// Name returns the evidence module's name. +func (am AppModule) Name() string { + return am.AppModuleBasic.Name() +} + +// Route returns the evidence module's message routing key. +func (AppModule) Route() string { + return RouterKey +} + +// QuerierRoute returns the evidence module's query routing key. +func (AppModule) QuerierRoute() string { + return QuerierRoute +} + +// NewHandler returns the evidence module's message Handler. +func (am AppModule) NewHandler() sdk.Handler { + return NewHandler(am.keeper) +} + +// NewQuerierHandler returns the evidence module's Querier. +func (am AppModule) NewQuerierHandler() sdk.Querier { + return NewQuerier(am.keeper) +} + +// RegisterInvariants registers the evidence module's invariants. +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} + +// InitGenesis performs the evidence module's genesis initialization It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, bz json.RawMessage) []abci.ValidatorUpdate { + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) + if err != nil { + panic(fmt.Sprintf("failed to unmarshal %s genesis state: %s", ModuleName, err)) + } + + InitGenesis(ctx, am.keeper, gs) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the evidence module's exported genesis state as raw JSON bytes. +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + return ModuleCdc.MustMarshalJSON(ExportGenesis(ctx, am.keeper)) +} + +// BeginBlock executes all ABCI BeginBlock logic respective to the evidence module. +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock executes all ABCI EndBlock logic respective to the evidence module. It +// returns no validator updates. +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/evidence/spec/01_concepts.md b/x/evidence/spec/01_concepts.md new file mode 100644 index 000000000..b71f493f2 --- /dev/null +++ b/x/evidence/spec/01_concepts.md @@ -0,0 +1,58 @@ +# Concepts + +## Evidence + +Any concrete type of evidence submitted to the `x/evidence` module must fulfill the +`Evidence` contract outlined below. Not all concrete types of evidence will fulfill +this contract in the same way and some data may be entirely irrelevant to certain +types of evidence. + +```go +type Evidence interface { + Route() string + Type() string + String() string + Hash() HexBytes + ValidateBasic() error + + // The consensus address of the malicious validator at time of infraction + GetConsensusAddress() ConsAddress + + // Height at which the infraction occurred + GetHeight() int64 + + // The total power of the malicious validator at time of infraction + GetValidatorPower() int64 + + // The total validator set power at time of infraction + GetTotalPower() int64 +} +``` + +## Registration & Handling + +The `x/evidence` module must first know about all types of evidence it is expected +to handle. This is accomplished by registering the `Route` method in the `Evidence` +contract with what is known as a `Router` (defined below). The `Router` accepts +`Evidence` and attempts to find the corresponding `Handler` for the `Evidence` +via the `Route` method. + +```go +type Router interface { + AddRoute(r string, h Handler) Router + HasRoute(r string) bool + GetRoute(path string) Handler + Seal() + Sealed() bool +} +``` + +The `Handler` (defined below) is responsible for executing the entirety of the +business logic for handling `Evidence`. This typically includes validating the +evidence, both stateless checks via `ValidateBasic` and stateful checks via any +keepers provided to the `Handler`. In addition, the `Handler` may also perform +capabilities such as slashing and jailing a validator. + +```go +type Handler func(Context, Evidence) error +``` diff --git a/x/evidence/spec/02_state.md b/x/evidence/spec/02_state.md new file mode 100644 index 000000000..163743a7c --- /dev/null +++ b/x/evidence/spec/02_state.md @@ -0,0 +1,12 @@ +# State + +Currently the `x/evidence` module only stores valid submitted `Evidence` in state. +The evidence state is also stored and exported in the `x/evidence` module's `GenesisState`. + +```go +type GenesisState struct { + Evidence []Evidence `json:"evidence" yaml:"evidence"` +} +``` + +All `Evidence` is retrieved and stored via a prefix `KVStore` using prefix `0x00` (`KeyPrefixEvidence`). diff --git a/x/evidence/spec/03_messages.md b/x/evidence/spec/03_messages.md new file mode 100644 index 000000000..84b8a844e --- /dev/null +++ b/x/evidence/spec/03_messages.md @@ -0,0 +1,42 @@ +# Messages + +## MsgSubmitEvidence + +Evidence is submitted through a `MsgSubmitEvidence` message: + +```go +type MsgSubmitEvidence struct { + Evidence Evidence + Submitter AccAddress +} +``` + +Note, the `Evidence` of a `MsgSubmitEvidence` message must have a corresponding +`Handler` registered with the `x/evidence` module's `Router` in order to be processed +and routed correctly. + +Given the `Evidence` is registered with a corresponding `Handler`, it is processed +as follows: + +```go +func SubmitEvidence(ctx Context, evidence Evidence) error { + if _, ok := GetEvidence(ctx, evidence.Hash()); ok { + return ErrEvidenceExists(codespace, evidence.Hash().String()) + } + if !router.HasRoute(evidence.Route()) { + return ErrNoEvidenceHandlerExists(codespace, evidence.Route()) + } + + handler := router.GetRoute(evidence.Route()) + if err := handler(ctx, evidence); err != nil { + return ErrInvalidEvidence(codespace, err.Error()) + } + + SetEvidence(ctx, evidence) + return nil +} +``` + +First, there must not already exist valid submitted `Evidence` of the exact same +type. Secondly, the `Evidence` is routed to the `Handler` and executed. Finally, +if there is no error in handling the `Evidence`, it is persisted to state. diff --git a/x/evidence/spec/04_events.md b/x/evidence/spec/04_events.md new file mode 100644 index 000000000..1994f2398 --- /dev/null +++ b/x/evidence/spec/04_events.md @@ -0,0 +1,14 @@ +# Events + +The `x/evidence` module emits the following events: + +## Handlers + +### MsgSubmitEvidence + +| Type | Attribute Key | Attribute Value | +| --------------- | ------------- | --------------- | +| submit_evidence | evidence_hash | {evidenceHash} | +| message | module | evidence | +| message | sender | {senderAddress} | +| message | action | submit_evidence | diff --git a/x/evidence/spec/README.md b/x/evidence/spec/README.md new file mode 100644 index 000000000..1f8667b1b --- /dev/null +++ b/x/evidence/spec/README.md @@ -0,0 +1,28 @@ +# Evidence Module Specification + +## Abstract + +`x/evidence` is an implementation of a Cosmos SDK module, per [ADR 009](./../../../docs/architecture/adr-009-evidence-module.md), +that allows for the submission and handling of arbitrary evidence of misbehavior such +as equivocation and counterfactual signing. + +The evidence module differs from standard evidence handling which typically expects the +underlying consensus engine, e.g. Tendermint, to automatically submit evidence when +it is discovered by allowing clients and foreign chains to submit more complex evidence +directly. + +All concrete evidence types must implement the `Evidence` interface contract. Submitted +`Evidence` is first routed through the evidence module's `Router` in which it attempts +to find a corresponding registered `Handler` for that specific `Evidence` type. +Each `Evidence` type must have a `Handler` registered with the evidence module's +keeper in order for it to be successfully routed and executed. + +Each corresponding handler must also fulfill the `Handler` interface contract. The +`Handler` for a given `Evidence` type can perform any arbitrary state transitions +such as slashing, jailing, and tombstoning. + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** diff --git a/x/gov/types/router.go b/x/gov/types/router.go index 3901d8869..651d20dc9 100644 --- a/x/gov/types/router.go +++ b/x/gov/types/router.go @@ -3,6 +3,8 @@ package types import ( "fmt" "regexp" + + sdk "github.com/cosmos/cosmos-sdk/types" ) var ( @@ -49,7 +51,7 @@ func (rtr *router) AddRoute(path string, h Handler) Router { panic("router sealed; cannot add route handler") } - if !isAlphaNumeric(path) { + if !sdk.IsAlphaNumeric(path) { panic("route expressions can only contain alphanumeric characters") } if rtr.HasRoute(path) {