Skip to content

Commit

Permalink
feat!: prototype for gatekeeping messages based on their version (cel…
Browse files Browse the repository at this point in the history
…estiaorg#3162)

Ref: celestiaorg#3134

This PR solves the problem of ensuring the messages belonging to modules
not part of the current app version aren't executed.

It does this in two ways:
- Introducing an antehandler decorator to be predominantly used in
CheckTx to immediately reject transactions giving users a useful error
message (instead of just failing to execute)
- Introduces a `CircuitBreaker` implementation to the `MsgServiceRouter`
which prevents the execution of messages not belonging to the current
app version. We need this because another module may call a message that
is not current supported (think a governance proposal)

I had several complications with the wiring of this given the structure
of the SDK and tried a few different variants - this one I think being
the better.

It uses the configurator which is reponsible for registering services to
read all the methods a modules grpc Server supports and extracting out
the message names and mapping them to one or more versions that they are
supported for.

---------

Co-authored-by: Rootul P <[email protected]>
  • Loading branch information
cmwaters and rootulp authored Mar 14, 2024
1 parent 2660108 commit 91603c4
Show file tree
Hide file tree
Showing 22 changed files with 596 additions and 158 deletions.
4 changes: 4 additions & 0 deletions app/ante/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ func NewAnteHandler(
signModeHandler signing.SignModeHandler,
sigGasConsumer ante.SignatureVerificationGasConsumer,
channelKeeper *ibckeeper.Keeper,
msgVersioningGateKeeper *MsgVersioningGateKeeper,
) sdk.AnteHandler {
return sdk.ChainAnteDecorators(
// Wraps the panic with the string format of the transaction
NewHandlePanicDecorator(),
// Prevents messages that don't belong to the correct app version
// from being executed
msgVersioningGateKeeper,
// Set up the context with a gas meter.
// Must be called before gas consumption occurs in any other decorator.
ante.NewSetUpContextDecorator(),
Expand Down
57 changes: 57 additions & 0 deletions app/ante/msg_gatekeeper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package ante

import (
"context"

"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

var (
_ sdk.AnteDecorator = MsgVersioningGateKeeper{}
_ baseapp.CircuitBreaker = MsgVersioningGateKeeper{}
)

// MsgVersioningGateKeeper dictates which transactions are accepted for an app version
type MsgVersioningGateKeeper struct {
// acceptedMsgs is a map from appVersion -> msgTypeURL -> struct{}.
// If a msgTypeURL is present in the map it should be accepted for that appVersion.
acceptedMsgs map[uint64]map[string]struct{}
}

func NewMsgVersioningGateKeeper(acceptedList map[uint64]map[string]struct{}) *MsgVersioningGateKeeper {
return &MsgVersioningGateKeeper{
acceptedMsgs: acceptedList,
}
}

// AnteHandle implements the ante.Decorator interface
func (mgk MsgVersioningGateKeeper) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
acceptedMsgs, exists := mgk.acceptedMsgs[ctx.BlockHeader().Version.App]
if !exists {
return ctx, sdkerrors.ErrNotSupported.Wrapf("app version %d is not supported", ctx.BlockHeader().Version.App)
}
for _, msg := range tx.GetMsgs() {
msgTypeURL := sdk.MsgTypeURL(msg)
_, exists := acceptedMsgs[msgTypeURL]
if !exists {
return ctx, sdkerrors.ErrNotSupported.Wrapf("message type %s is not supported in version %d", msgTypeURL, ctx.BlockHeader().Version.App)
}
}

return next(ctx, tx, simulate)
}

func (mgk MsgVersioningGateKeeper) IsAllowed(ctx context.Context, msgName string) (bool, error) {
appVersion := sdk.UnwrapSDKContext(ctx).BlockHeader().Version.App
acceptedMsgs, exists := mgk.acceptedMsgs[appVersion]
if !exists {
return false, sdkerrors.ErrNotSupported.Wrapf("app version %d is not supported", appVersion)
}
_, exists = acceptedMsgs[msgName]
if !exists {
return false, nil
}
return true, nil
}
70 changes: 70 additions & 0 deletions app/ante/msg_gatekeeper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ante_test

import (
"testing"

"github.com/celestiaorg/celestia-app/app"
"github.com/celestiaorg/celestia-app/app/ante"
"github.com/celestiaorg/celestia-app/app/encoding"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
version "github.com/tendermint/tendermint/proto/tendermint/version"
)

func TestMsgGateKeeperAnteHandler(t *testing.T) {
// Define test cases
tests := []struct {
name string
msg sdk.Msg
acceptMsg bool
version uint64
}{
{
name: "Accept MsgSend",
msg: &banktypes.MsgSend{},
acceptMsg: true,
version: 1,
},
{
name: "Reject MsgMultiSend",
msg: &banktypes.MsgMultiSend{},
acceptMsg: false,
version: 1,
},
{
name: "Reject MsgSend with version 2",
msg: &banktypes.MsgSend{},
acceptMsg: false,
version: 2,
},
}

msgGateKeeper := ante.NewMsgVersioningGateKeeper(map[uint64]map[string]struct{}{
1: {
"/cosmos.bank.v1beta1.MsgSend": {},
},
2: {},
})
cdc := encoding.MakeConfig(app.ModuleEncodingRegisters...)
anteHandler := sdk.ChainAnteDecorators(msgGateKeeper)

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := sdk.NewContext(nil, tmproto.Header{Version: version.Consensus{App: tc.version}}, false, nil)
txBuilder := cdc.TxConfig.NewTxBuilder()
require.NoError(t, txBuilder.SetMsgs(tc.msg))
_, err := anteHandler(ctx, txBuilder.GetTx(), false)
allowed, err2 := msgGateKeeper.IsAllowed(ctx, sdk.MsgTypeURL(tc.msg))
require.NoError(t, err2)
if tc.acceptMsg {
require.NoError(t, err, "expected message to be accepted")
require.True(t, allowed)
} else {
require.Error(t, err, "expected message to be rejected")
require.False(t, allowed)
}
})
}
}
173 changes: 130 additions & 43 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ import (

"github.com/celestiaorg/celestia-app/app/ante"
"github.com/celestiaorg/celestia-app/app/encoding"
v1 "github.com/celestiaorg/celestia-app/pkg/appconsts/v1"
v2 "github.com/celestiaorg/celestia-app/pkg/appconsts/v2"
appv1 "github.com/celestiaorg/celestia-app/pkg/appconsts/v1"
appv2 "github.com/celestiaorg/celestia-app/pkg/appconsts/v2"
"github.com/celestiaorg/celestia-app/pkg/proof"
"github.com/celestiaorg/celestia-app/x/blob"
blobkeeper "github.com/celestiaorg/celestia-app/x/blob/keeper"
Expand Down Expand Up @@ -142,7 +142,11 @@ var (
}
)

const DefaultInitialVersion = v1.Version
const (
v1 = appv1.Version
v2 = appv2.Version
DefaultInitialVersion = v1
)

var _ servertypes.Application = (*App)(nil)

Expand Down Expand Up @@ -189,9 +193,12 @@ type App struct {
BlobKeeper blobkeeper.Keeper
BlobstreamKeeper blobstreamkeeper.Keeper

mm *module.Manager

configurator sdkmodule.Configurator
mm *module.Manager
configurator module.VersionedConfigurator
// used as a coordination mechanism for height based upgrades
upgradeHeight int64
// used to define what messages are accepted for a given app version
MsgGateKeeper *ante.MsgVersioningGateKeeper
}

// New returns a reference to an initialized celestia app.
Expand Down Expand Up @@ -287,7 +294,8 @@ func New(
)

app.FeeGrantKeeper = feegrantkeeper.NewKeeper(appCodec, keys[feegrant.StoreKey], app.AccountKeeper)
app.UpgradeKeeper = upgrade.NewKeeper(keys[upgradetypes.StoreKey], upgradeHeight, stakingKeeper)
app.UpgradeKeeper = upgrade.NewKeeper(keys[upgradetypes.StoreKey], stakingKeeper)
app.upgradeHeight = upgradeHeight

app.BlobstreamKeeper = *blobstreamkeeper.NewKeeper(
appCodec,
Expand Down Expand Up @@ -367,31 +375,88 @@ func New(
// NOTE: Any module instantiated in the module manager that is later modified
// must be passed by reference here.
var err error
app.mm, err = module.NewManager(
module.NewVersionedModule(genutil.NewAppModule(
app.AccountKeeper, app.StakingKeeper, app.BaseApp.DeliverTx,
encodingConfig.TxConfig,
), v1.Version, v2.Version),
module.NewVersionedModule(auth.NewAppModule(appCodec, app.AccountKeeper, nil), v1.Version, v2.Version),
module.NewVersionedModule(vesting.NewAppModule(app.AccountKeeper, app.BankKeeper), v1.Version, v2.Version),
module.NewVersionedModule(bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), v1.Version, v2.Version),
module.NewVersionedModule(capability.NewAppModule(appCodec, *app.CapabilityKeeper), v1.Version, v2.Version),
module.NewVersionedModule(feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), v1.Version, v2.Version),
module.NewVersionedModule(crisis.NewAppModule(&app.CrisisKeeper, skipGenesisInvariants), v1.Version, v2.Version),
module.NewVersionedModule(gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper), v1.Version, v2.Version),
module.NewVersionedModule(mint.NewAppModule(appCodec, app.MintKeeper, app.AccountKeeper), v1.Version, v2.Version),
module.NewVersionedModule(slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper), v1.Version, v2.Version),
module.NewVersionedModule(distr.NewAppModule(appCodec, app.DistrKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper), v1.Version, v2.Version),
module.NewVersionedModule(staking.NewAppModule(appCodec, app.StakingKeeper, app.AccountKeeper, app.BankKeeper), v1.Version, v2.Version),
module.NewVersionedModule(evidence.NewAppModule(app.EvidenceKeeper), v1.Version, v2.Version),
module.NewVersionedModule(authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), v1.Version, v2.Version),
module.NewVersionedModule(ibc.NewAppModule(app.IBCKeeper), v1.Version, v2.Version),
module.NewVersionedModule(params.NewAppModule(app.ParamsKeeper), v1.Version, v2.Version),
module.NewVersionedModule(transfer.NewAppModule(app.TransferKeeper), v1.Version, v2.Version),
module.NewVersionedModule(blob.NewAppModule(appCodec, app.BlobKeeper), v1.Version, v2.Version),
module.NewVersionedModule(blobstream.NewAppModule(appCodec, app.BlobstreamKeeper), v1.Version, v2.Version),
module.NewVersionedModule(upgrade.NewAppModule(app.UpgradeKeeper), v2.Version, v2.Version),
)
app.mm, err = module.NewManager([]module.VersionedModule{
{
Module: genutil.NewAppModule(app.AccountKeeper, app.StakingKeeper, app.BaseApp.DeliverTx, encodingConfig.TxConfig),
FromVersion: v1, ToVersion: v2,
},
{
Module: auth.NewAppModule(appCodec, app.AccountKeeper, nil),
FromVersion: v1, ToVersion: v2,
},
{
Module: vesting.NewAppModule(app.AccountKeeper, app.BankKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: capability.NewAppModule(appCodec, *app.CapabilityKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry),
FromVersion: v1, ToVersion: v2,
},
{
Module: crisis.NewAppModule(&app.CrisisKeeper, skipGenesisInvariants),
FromVersion: v1, ToVersion: v2,
},
{
Module: gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: mint.NewAppModule(appCodec, app.MintKeeper, app.AccountKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: distr.NewAppModule(appCodec, app.DistrKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: staking.NewAppModule(appCodec, app.StakingKeeper, app.AccountKeeper, app.BankKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: evidence.NewAppModule(app.EvidenceKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
FromVersion: v1, ToVersion: v2,
},
{
Module: ibc.NewAppModule(app.IBCKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: params.NewAppModule(app.ParamsKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: transfer.NewAppModule(app.TransferKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: blob.NewAppModule(appCodec, app.BlobKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: blobstream.NewAppModule(appCodec, app.BlobstreamKeeper),
FromVersion: v1, ToVersion: v2,
},
{
Module: upgrade.NewAppModule(app.UpgradeKeeper),
FromVersion: v2, ToVersion: v2,
},
})
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -481,6 +546,12 @@ func New(
app.configurator = module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter())
app.mm.RegisterServices(app.configurator)

// extract the accepted message list from the configurator and create a gatekeeper
// which will be used both as the antehandler and as part of the circuit breaker in
// the msg service router
app.MsgGateKeeper = ante.NewMsgVersioningGateKeeper(app.configurator.GetAcceptedMessages())
app.MsgServiceRouter().SetCircuit(app.MsgGateKeeper)

// initialize stores
app.MountKVStores(keys)
app.MountTransientStores(tkeys)
Expand All @@ -498,6 +569,7 @@ func New(
encodingConfig.TxConfig.SignModeHandler(),
ante.DefaultSigVerificationGasConsumer,
app.IBCKeeper,
app.MsgGateKeeper,
))
app.SetPostHandler(posthandler.New())

Expand All @@ -521,26 +593,37 @@ func (app *App) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.R
// EndBlocker application updates every end block
func (app *App) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
res := app.mm.EndBlock(ctx, req)
// NOTE: this is a specific feature for upgrading from v1 to v2. It will be deprecated in v3
if app.UpgradeKeeper.ShouldUpgradeToV2(req.Height) && app.AppVersion(ctx) == v1.Version {
if err := app.Upgrade(ctx, v2.Version); err != nil {
panic(err)
currentVersion := app.AppVersion()
// For v1 only we upgrade using a agreed upon height known ahead of time
if currentVersion == v1 {
// check that we are at the height before the upgrade
if req.Height == app.upgradeHeight-1 {
if err := app.Upgrade(ctx, currentVersion, currentVersion+1); err != nil {
panic(err)
}
}
// from v2 to v3 and onwards we use a signalling mechanism
} else if shouldUpgrade, version := app.UpgradeKeeper.ShouldUpgrade(); shouldUpgrade {
} else if shouldUpgrade, newVersion := app.UpgradeKeeper.ShouldUpgrade(); shouldUpgrade {
// Version changes must be increasing. Downgrades are not permitted
if version > app.AppVersion(ctx) {
if err := app.Upgrade(ctx, version); err != nil {
if newVersion > currentVersion {
if err := app.Upgrade(ctx, currentVersion, newVersion); err != nil {
panic(err)
}
}
}
return res
}

func (app *App) Upgrade(ctx sdk.Context, version uint64) error {
app.SetAppVersion(ctx, version)
return app.mm.RunMigrations(ctx, app.configurator, app.AppVersion(ctx), version)
func (app *App) Upgrade(ctx sdk.Context, fromVersion, toVersion uint64) error {
if err := app.mm.RunMigrations(ctx, app.configurator, fromVersion, toVersion); err != nil {
return err
}
if toVersion == v2 {
// we need to set the app version in the param store for the first time
app.SetInitialAppVersionInConsensusParams(ctx, toVersion)
}
app.SetAppVersion(ctx, toVersion)
return nil
}

// InitChainer application update at chain initialization
Expand All @@ -550,10 +633,14 @@ func (app *App) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.Res
panic(err)
}
// genesis must always contain the consensus params. The validator set howerver is derived from the
// initial genesis state
// initial genesis state. The genesis must always contain a non zero app version which is the initial
// version that the chain starts on
if req.ConsensusParams == nil || req.ConsensusParams.Version == nil {
panic("no consensus params set")
}
if req.ConsensusParams.Version.AppVersion == 0 {
panic("app version 0 is not accepted. Please set an app version in the genesis")
}
return app.mm.InitGenesis(ctx, app.appCodec, genesisState, req.ConsensusParams.Version.AppVersion)
}

Expand Down
Loading

0 comments on commit 91603c4

Please sign in to comment.