Skip to content

Commit

Permalink
feat!: Implement the version module (#1640)
Browse files Browse the repository at this point in the history
## Overview

This PR adds a very minimal version module, which will update the app
version in the header using end block based on a config. The configs are
just hardcoded maps atm, but its possible for a user to pass their own
via app options. This PR also isn't meant to be a final solution for
#1014 or #1568, but more of an exploratory demo of one way they could
done.

the majority of #1594 

## Checklist

- [x] New and updated code has appropriate documentation
- [x] New and updated code has new and/or updated testing
- [x] Required CI checks are passing
- [x] Visual proof for any user facing features like CLI or
documentation updates
- [x] Linked issues closed with keywords

---------

Co-authored-by: Rootul P <[email protected]>
Co-authored-by: rene <[email protected]>
  • Loading branch information
3 people authored Apr 27, 2023
1 parent 8439399 commit f2a18f4
Show file tree
Hide file tree
Showing 16 changed files with 543 additions and 13 deletions.
10 changes: 8 additions & 2 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import (
blobmodulekeeper "github.com/celestiaorg/celestia-app/x/blob/keeper"
blobmoduletypes "github.com/celestiaorg/celestia-app/x/blob/types"
"github.com/celestiaorg/celestia-app/x/tokenfilter"
appversion "github.com/celestiaorg/celestia-app/x/version"

qgbmodule "github.com/celestiaorg/celestia-app/x/qgb"
qgbmodulekeeper "github.com/celestiaorg/celestia-app/x/qgb/keeper"
Expand Down Expand Up @@ -223,8 +224,9 @@ type App struct {
ScopedIBCKeeper capabilitykeeper.ScopedKeeper
ScopedTransferKeeper capabilitykeeper.ScopedKeeper

BlobKeeper blobmodulekeeper.Keeper
QgbKeeper qgbmodulekeeper.Keeper
BlobKeeper blobmodulekeeper.Keeper
QgbKeeper qgbmodulekeeper.Keeper
VersionKeeper appversion.Keeper

// the module manager
mm *module.Manager
Expand Down Expand Up @@ -324,6 +326,10 @@ func New(
)
qgbmod := qgbmodule.NewAppModule(appCodec, app.QgbKeeper)

customVersions := appOpts.Get(appversion.CustomVersionConfigKey)
cv, _ := customVersions.(map[string]appversion.ChainVersionConfig)
app.VersionKeeper = appversion.NewKeeper(app.BaseApp, cv)

// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
app.StakingKeeper = *stakingKeeper.SetHooks(
Expand Down
14 changes: 14 additions & 0 deletions app/end_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app

import (
appversion "github.com/celestiaorg/celestia-app/x/version"
abci "github.com/tendermint/tendermint/abci/types"
)

// EndBlock wraps the BaseApp's end block method. This is done to modify the app
// version if necessary.
func (app *App) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) {
res = app.BaseApp.EndBlock(req)
ctx := app.GetContextForDeliverTx([]byte{1})
return appversion.EndBlocker(ctx, app.VersionKeeper, res)
}
1 change: 0 additions & 1 deletion cmd/celestia-appd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,6 @@ func NewAppServer(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts se
appOpts,
baseapp.SetPruning(pruningOpts),
baseapp.SetMinGasPrices(cast.ToString(appOpts.Get(server.FlagMinGasPrices))),
baseapp.SetMinRetainBlocks(cast.ToUint64(appOpts.Get(server.FlagMinRetainBlocks))),
baseapp.SetHaltHeight(cast.ToUint64(appOpts.Get(server.FlagHaltHeight))),
baseapp.SetHaltTime(cast.ToUint64(appOpts.Get(server.FlagHaltTime))),
baseapp.SetMinRetainBlocks(cast.ToUint64(appOpts.Get(server.FlagMinRetainBlocks))),
Expand Down
1 change: 1 addition & 0 deletions pkg/wrapper/nmt_wrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func TestErasuredNamespacedMerkleTree(t *testing.T) {
privateRoot, err := tree.tree.Root()
assert.NoError(t, err)
assert.Equal(t, tree.Tree(), tree.tree)

assert.Equal(t, publicRoot, privateRoot)
}

Expand Down
28 changes: 20 additions & 8 deletions testutil/testnode/full_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import (
"testing"
"time"

"github.com/celestiaorg/celestia-app/app"
"github.com/celestiaorg/celestia-app/app/encoding"
"github.com/celestiaorg/celestia-app/cmd/celestia-appd/cmd"
"github.com/celestiaorg/celestia-app/testutil/testfactory"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
pruningtypes "github.com/cosmos/cosmos-sdk/pruning/types"
Expand All @@ -29,11 +25,18 @@ import (
"github.com/tendermint/tendermint/proxy"
"github.com/tendermint/tendermint/types"
dbm "github.com/tendermint/tm-db"

"github.com/celestiaorg/celestia-app/app"
"github.com/celestiaorg/celestia-app/app/encoding"
"github.com/celestiaorg/celestia-app/cmd/celestia-appd/cmd"
"github.com/celestiaorg/celestia-app/testutil/testfactory"
"github.com/celestiaorg/celestia-app/x/version"
)

// New creates a ready to use tendermint node that operates a single validator
// celestia-app network using the provided genesis state. The provided keyring
// is stored in the client.Context that is returned.
// is stored in the client.Context that is returned. versionMap is used to
// specify the version of the application at a given height.
//
// NOTE: the forced delay between blocks, TimeIotaMs in the consensus
// parameters, is set to the lowest possible value (1ms).
Expand All @@ -44,6 +47,8 @@ func New(
supressLog bool,
genState map[string]json.RawMessage,
kr keyring.Keyring,
chainID string,
versionMap map[uint64]int64,
) (*node.Node, srvtypes.Application, Context, error) {
var logger log.Logger
if supressLog {
Expand All @@ -58,8 +63,6 @@ func New(
return nil, nil, Context{}, err
}

chainID := tmrand.Str(6)

encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...)

nodeKey, err := p2p.LoadOrGenNodeKey(tmCfg.NodeKeyFile())
Expand Down Expand Up @@ -89,10 +92,19 @@ func New(

db := dbm.NewMemDB()

if versionMap == nil {
versionMap = map[uint64]int64{
1: 0,
}
}

appOpts := appOptions{
options: map[string]interface{}{
server.FlagPruning: pruningtypes.PruningOptionNothing,
flags.FlagHome: baseDir,
version.CustomVersionConfigKey: map[string]version.ChainVersionConfig{
chainID: version.NewChainVersionConfig(versionMap),
},
},
}

Expand Down Expand Up @@ -192,7 +204,7 @@ func DefaultNetwork(t *testing.T, blockTime time.Duration) (cleanup func() error
genState, kr, err := DefaultGenesisState(accounts...)
require.NoError(t, err)

tmNode, app, cctx, err := New(t, DefaultParams(), tmCfg, false, genState, kr)
tmNode, app, cctx, err := New(t, DefaultParams(), tmCfg, false, genState, kr, tmrand.Str(6), nil)
require.NoError(t, err)

cctx, stopNode, err := StartNode(tmNode, cctx)
Expand Down
2 changes: 1 addition & 1 deletion testutil/testnode/full_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
genState, kr, err := DefaultGenesisState(s.accounts...)
require.NoError(err)

tmNode, app, cctx, err := New(s.T(), DefaultParams(), DefaultTendermintConfig(), false, genState, kr)
tmNode, app, cctx, err := New(s.T(), DefaultParams(), DefaultTendermintConfig(), false, genState, kr, tmrand.Str(6), nil)
require.NoError(err)

cctx, stopNode, err := StartNode(tmNode, cctx)
Expand Down
2 changes: 1 addition & 1 deletion x/blob/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The blob module emits the following events:
celestia-app tx blob PayForBlobs <hex encoded namespace> <hex encoded data> [flags]
```

For submitting PFB transaction via a light client's rpc, see [celestia-node's documention](https://docs.celestia.org/developers/node-api/#post-submit_pfd).
For submitting PFB transaction via a light client's rpc, see [celestia-node's documention](https://docs.celestia.org/developers/node-tutorial/#submit-a-pfb-transaction).

While not directly supported, the steps in the [`SubmitPayForBlob`](https://github.com/celestiaorg/celestia-app/blob/a82110a281bf9ee95a9bf9f0492e5d091371ff0b/x/blob/payforblob.go) function can be reverse engineered to submit blobs programmatically.

Expand Down
106 changes: 106 additions & 0 deletions x/version/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# `x/version`

## Abstract

The version module will increment the app version in the header per a predefined
mapping of app versions to heights. This allows for application logic to be
routed in the same binary, which enables single binary syncs and upgrades.

It accomplishes this by wrapping the `BaseApp`'s `EndBlock` method and checking
if the current height is equal to the height at which the app version should be
incremented. If it is, then the app version is incremented and the new version
is set in the header.

```go
func (app *App) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) {
res = app.BaseApp.EndBlock(req)
ctx := app.GetContextForDeliverTx([]byte{1})
return appversion.EndBlocker(ctx, app.VersionKeeper, res)
}

// EndBlocker will modify the app version if the current height is equal to
// a predefined height at which the app version should be changed.
func EndBlocker(ctx sdk.Context, keeper Keeper, resp abci.ResponseEndBlock) abci.ResponseEndBlock {
newAppVersion := keeper.GetVersion(ctx)
if ctx.BlockHeader().Version.App != newAppVersion {
resp.ConsensusParamUpdates.Version = &coretypes.VersionParams{
AppVersion: newAppVersion,
}
// set the version in the application to ensure that tendermint is
// passed the correct value upon rebooting
keeper.versionSetter.SetProtocolVersion(newAppVersion)
}
return resp
}
```

## State

The state in the version module is abnormal because it can be modified by the
party building the binary without breaking the app hash. This is because the
state is not stored in the iavl tree, but rather as a simple predefined golang
map. It's important to note that even though this state is not in the state
store, that it can still cause consensus breaking changes. This is because that
state controls at which height the application will increment the app version.

```go
type Keeper struct {
chainAppVersions map[string]ChainVersionConfig
}
```

## Events

TODO: Add events

### Usage

The standard usage of the version module is to load the app version height
mappings that are embedded into the binary, but it is also possible to load
custom mappings by including a `map[string]ChainVersionConfig` in the
app options using the `version.CustomVersionConfigKey` key.

Mappings are defined by a `map[uint64]int64` where the key is the app version
and the value is the height at which the app version should be incremented. For
example:

```go
vm := map[uint64]int64{
1: 0,
2: 112093,
3: 300442,
4: 420420,
}
```

The application will convert this mapping into a ChainVersionConfig:

```go
// HeightRange is a range of heights that a version is valid for. It is an
// internal struct used to search for the correct version given a height, and
// should only be created using the createRange function. Heights are
// non-overlapping and inclusive, meaning that the version is valid for all
// heights >= Start and <= End.
type HeightRange struct {
Start int64
End int64
Version uint64
}

// ChainVersionConfig stores a set of version ranges and provides a method to
// retrieve the correct version for a given height.
type ChainVersionConfig struct {
Ranges []HeightRange
}
```

which is used to determine the correct version for a given height. The keeper is
initiated with a mapping of these configs depending on the chain-id. This allows
for the chain-id to be changed and for multiple chain's (ie testnets) to be
supported.

```go
type Keeper struct {
chainAppVersions map[string]ChainVersionConfig
}
```
22 changes: 22 additions & 0 deletions x/version/abci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package version

import (
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
coretypes "github.com/tendermint/tendermint/proto/tendermint/types"
)

// EndBlocker will modify the app version if the current height is equal to
// a predefined height at which the app version should be changed.
func EndBlocker(ctx sdk.Context, keeper Keeper, resp abci.ResponseEndBlock) abci.ResponseEndBlock {
newAppVersion := keeper.GetVersion(ctx)
if ctx.BlockHeader().Version.App != newAppVersion {
resp.ConsensusParamUpdates.Version = &coretypes.VersionParams{
AppVersion: newAppVersion,
}
// set the version in the application to ensure that tendermint is
// passed the correct value upon rebooting
keeper.versionSetter.SetProtocolVersion(newAppVersion)
}
return resp
}
25 changes: 25 additions & 0 deletions x/version/chains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package version

const (
MochaChainID = "mocha"
ArabicaChainID = "arabica-4"
BlockspaceraceChainID = "blockspacerace-0"
MainnetChainID = "celestia-1"
)

// StandardChainVersions returns a map of chain IDs to their respective
// ChainVersionConfig. Each hardfork should be added to this map.
func StandardChainVersions() map[string]ChainVersionConfig {
version0Only := NewChainVersionConfig(map[uint64]int64{
0: 0,
})
mainnetVersions := NewChainVersionConfig(map[uint64]int64{
1: 0,
})
return map[string]ChainVersionConfig{
MochaChainID: version0Only,
ArabicaChainID: version0Only,
BlockspaceraceChainID: version0Only,
MainnetChainID: mainnetVersions,
}
}
5 changes: 5 additions & 0 deletions x/version/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package version

const (
CustomVersionConfigKey = "custom-version-config"
)
63 changes: 63 additions & 0 deletions x/version/end_block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package version

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/upgrade/exported"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
coretypes "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/proto/tendermint/version"
)

func TestEndBlock(t *testing.T) {
testChainID := "test"
ctx := sdk.Context{}.
WithBlockHeader(coretypes.Header{
ChainID: testChainID,
Version: version.Consensus{App: 1},
Height: 1,
}).
WithChainID(testChainID)

vcfg := NewChainVersionConfig(
map[uint64]int64{
1: 1,
2: 2,
},
)
vsetter := &testVersionSetter{}
configs := map[string]ChainVersionConfig{
testChainID: vcfg,
}
keeper := NewKeeper(vsetter, configs)

resp := abci.ResponseEndBlock{ConsensusParamUpdates: &abci.ConsensusParams{}}
resp = EndBlocker(ctx, keeper, resp)
require.Nil(t, resp.ConsensusParamUpdates.Version)

ctx = ctx.WithBlockHeight(2)
resp = abci.ResponseEndBlock{ConsensusParamUpdates: &abci.ConsensusParams{}}
resp = EndBlocker(ctx, keeper, resp)
require.NotNil(t, resp.ConsensusParamUpdates.Version)
require.Equal(t, uint64(2), resp.ConsensusParamUpdates.Version.AppVersion)
require.Equal(t, uint64(2), vsetter.version)

ctx = ctx.WithBlockHeight(9999)
resp = abci.ResponseEndBlock{ConsensusParamUpdates: &abci.ConsensusParams{}}
resp = EndBlocker(ctx, keeper, resp)
require.NotNil(t, resp.ConsensusParamUpdates.Version)
require.Equal(t, uint64(2), resp.ConsensusParamUpdates.Version.AppVersion)
require.Equal(t, uint64(2), vsetter.version)
}

var _ exported.ProtocolVersionSetter = &testVersionSetter{}

type testVersionSetter struct {
version uint64
}

func (vs *testVersionSetter) SetProtocolVersion(v uint64) {
vs.version = v
}
Loading

0 comments on commit f2a18f4

Please sign in to comment.