Skip to content

Commit

Permalink
feat: scaffold consumer chain (backport #3660) (#4016)
Browse files Browse the repository at this point in the history
* feat: scaffold consumer chain (#3660)

* feat: add validation kind in config

Disable gentx generation when validation is consumer.

* Add consumer chain plush scaffolding

* update config.yml when scaffold consumer chain

* Add hard-coded interchain-security require

* remove comment

* fix bad merge go.sum

* update ibc to v8

* fix changelog

* chore: update interchain-security dependency

Use the latest compatible with sdk50

* update ccvconsumertypes -> ccvtypes

* templates: ibc-go/v7 -> ibc-go/v8

plus other dep updates

* fix imports

* fix imports paths

* do not pass CapabilityKeeper in dep.Inject

* Fix lint

* fix: add missing ibcconsumer.AppModule (#3848)

Co-authored-by: Pantani <Pantani>

* remove ICS dep

* wip exec plugin!

* use plugin repo

* restore templates/app/files w/o IsConsumerChain condition

* create files-consumer alternate template folder

* mark minimal and consumer flags as exclusive

* fix wrong location for consumer_*.go files

* fix error handling for IsInitialized

* revert commit wip plugin exec

* use plugin to read & write consumer module genesis

* fix linter

* backport NFT module #3924 in files-consumer

* update app-consumer url

* move app-consumer address to ignite org

* update CL

* use new plugin location

* use merged version of consumer app

* changelog

* sync fixes

* updates

* fixes

* changelog

* feedback

* fix linter

* update ante handlers

* import

* updates

---------

Co-authored-by: Ehsan-saradar <[email protected]>
Co-authored-by: Danilo Pantani <[email protected]>
Co-authored-by: Julien Robert <[email protected]>
(cherry picked from commit 5ed9632)

# Conflicts:
#	ignite/cmd/scaffold_chain.go
#	ignite/services/scaffolder/init.go
#	ignite/templates/app/files/go.mod.plush

* fix conflicts

* Update changelog.md

---------

Co-authored-by: Thomas Bruyelle <[email protected]>
Co-authored-by: Julien Robert <[email protected]>
  • Loading branch information
3 people authored Mar 14, 2024
1 parent a9a254c commit 09f1398
Show file tree
Hide file tree
Showing 30 changed files with 1,654 additions and 47 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- [#3985](https://github.com/ignite/cli/pull/3985) Make some `cmd` pkg functions public
- [#3956](https://github.com/ignite/cli/pull/3956) Prepare for wasm app
- [#3660](https://github.com/ignite/cli/pull/3660) Add ability to scaffold ICS consumer chain

### Changes

Expand Down
20 changes: 20 additions & 0 deletions docs/docs/08-references/02-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ to describe the development environment for your blockchain.
Only a default set of parameters is provided. If more nuanced configuration is
required, you can add these parameters to the `config.yml` file.

## Validation

Ignite uses the `validation` field to determine the kind of validation
of your blockchain. There are currently two supported kinds of validation:

- `sovereign` which is the standard kind of validation where your blockchain
has its own validator set. This is the default value when this field is not
in the config file.
- `consumer` indicates your blockchain is a consumer chain, in the sense of
Replicated Security. That means it doesn't have a validator set, but
inherits the one of a provider chain.

While the `sovereign` chain is the default validation when you run the `ignite scaffold
chain`, to scaffold a consumer chain, you have to run `ignite scaffold chain
--consumer`.

This field is, at this time of writing, only used by Ignite at the genesis
generation step, because the genesis of a sovereign chain and a consumer chain
are different.

## Accounts

A list of user accounts created during genesis of the blockchain.
Expand Down
15 changes: 9 additions & 6 deletions ignite/cmd/scaffold_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
flagMinimal = "minimal"
flagNoDefaultModule = "no-module"
flagSkipGit = "skip-git"
flagIsConsumer = "consumer"

tplScaffoldChainSuccess = `
⭐️ Successfully created a new blockchain '%[1]v'.
Expand Down Expand Up @@ -83,6 +84,9 @@ about Cosmos SDK on https://docs.cosmos.network
c.Flags().StringSlice(flagParams, []string{}, "add default module parameters")
c.Flags().Bool(flagSkipGit, false, "skip Git repository initialization")
c.Flags().Bool(flagMinimal, false, "create a minimal blockchain (with the minimum required Cosmos SDK modules)")
c.Flags().Bool(flagIsConsumer, false, "scafffold an ICS consumer chain")
// Cannot have both minimal and consumer flag
c.MarkFlagsMutuallyExclusive(flagIsConsumer, flagMinimal)

return c
}
Expand All @@ -98,12 +102,10 @@ func scaffoldChainHandler(cmd *cobra.Command, args []string) error {
noDefaultModule, _ = cmd.Flags().GetBool(flagNoDefaultModule)
skipGit, _ = cmd.Flags().GetBool(flagSkipGit)
minimal, _ = cmd.Flags().GetBool(flagMinimal)
isConsumer, _ = cmd.Flags().GetBool(flagIsConsumer)
params, _ = cmd.Flags().GetStringSlice(flagParams)
)

params, err := cmd.Flags().GetStringSlice(flagParams)
if err != nil {
return err
}
if noDefaultModule && len(params) > 0 {
return errors.New("params flag is only supported if the default module is enabled")
}
Expand All @@ -113,7 +115,7 @@ func scaffoldChainHandler(cmd *cobra.Command, args []string) error {
return err
}

appdir, err := scaffolder.Init(
appDir, err := scaffolder.Init(
cmd.Context(),
cacheStorage,
placeholder.New(),
Expand All @@ -123,13 +125,14 @@ func scaffoldChainHandler(cmd *cobra.Command, args []string) error {
noDefaultModule,
skipGit,
minimal,
isConsumer,
params,
)
if err != nil {
return err
}

path, err := xfilepath.RelativePath(appdir)
path, err := xfilepath.RelativePath(appDir)
if err != nil {
return err
}
Expand Down
36 changes: 29 additions & 7 deletions ignite/config/chain/base/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,29 @@ type Host struct {
API string `yaml:"api"`
}

// Validation describes the kind of validation the chain has.
type Validation string

const (
// ValidationSovereign is when the chain has his own validator set.
// Note that an empty string is also considered as a sovereign validation,
// because this is the default value.
ValidationSovereign = "sovereign"
// ValidationConsumer is when the chain is validated by a provider chain.
// Such chain is called a consumer chain.
ValidationConsumer = "consumer"
)

// Config defines a struct with the fields that are common to all config versions.
type Config struct {
Version version.Version `yaml:"version"`
Build Build `yaml:"build,omitempty"`
Accounts []Account `yaml:"accounts"`
Faucet Faucet `yaml:"faucet,omitempty"`
Client Client `yaml:"client,omitempty"`
Genesis xyaml.Map `yaml:"genesis,omitempty"`
Minimal bool `yaml:"minimal,omitempty"`
Validation Validation `yaml:"validation,omitempty"`
Version version.Version `yaml:"version"`
Build Build `yaml:"build,omitempty"`
Accounts []Account `yaml:"accounts"`
Faucet Faucet `yaml:"faucet,omitempty"`
Client Client `yaml:"client,omitempty"`
Genesis xyaml.Map `yaml:"genesis,omitempty"`
Minimal bool `yaml:"minimal,omitempty"`
}

// GetVersion returns the config version.
Expand All @@ -172,6 +186,14 @@ func (c Config) IsChainMinimal() bool {
return c.Minimal
}

func (c Config) IsSovereignChain() bool {
return c.Validation == "" || c.Validation == ValidationSovereign
}

func (c Config) IsConsumerChain() bool {
return c.Validation == ValidationConsumer
}

// SetDefaults assigns default values to empty config fields.
func (c *Config) SetDefaults() error {
return mergo.Merge(c, DefaultConfig())
Expand Down
39 changes: 39 additions & 0 deletions ignite/internal/plugin/consumer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package plugininternal

import (
"context"
"strconv"

"github.com/ignite/cli/v28/ignite/pkg/errors"
"github.com/ignite/cli/v28/ignite/services/plugin"
)

// TODO use released version of app-consumer.
const consumerPlugin = "github.com/ignite/apps/consumer"

// ConsumerWriteGenesis writes validators in the consumer module genesis.
// NOTE(tb): Using a plugin for this task avoids having the interchain-security
// dependency in Ignite.
func ConsumerWriteGenesis(ctx context.Context, c plugin.Chainer) error {
_, err := Execute(ctx, consumerPlugin, []string{"writeGenesis"}, plugin.WithChain(c))
if err != nil {
return errors.Errorf("execute consumer plugin 'writeGenesis': %w", err)
}
return nil
}

// ConsumerIsInitialized returns true if the consumer chain's genesis c has
// a consumer module entry with an initial validator set.
// NOTE(tb): Using a plugin for this task avoids having the interchain-security
// dependency in Ignite.
func ConsumerIsInitialized(ctx context.Context, c plugin.Chainer) (bool, error) {
out, err := Execute(ctx, consumerPlugin, []string{"isInitialized"}, plugin.WithChain(c))
if err != nil {
return false, errors.Errorf("execute consumer plugin 'isInitialized': %w", err)
}
b, err := strconv.ParseBool(out)
if err != nil {
return false, errors.Errorf("invalid consumer plugin 'isInitialized' output, got '%s': %w", out, err)
}
return b, nil
}
134 changes: 134 additions & 0 deletions ignite/internal/plugin/consumer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package plugininternal

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/ignite/cli/v28/ignite/services/plugin"
"github.com/ignite/cli/v28/ignite/services/plugin/mocks"
)

func TestConsumerPlugin(t *testing.T) {
tests := []struct {
name string
args []string
setup func(*testing.T, string)
expectedOutput string
expectedError string
}{
{
name: "fail: missing arg",
expectedError: "missing argument",
},
{
name: "fail: invalid arg",
args: []string{"xxx"},
expectedError: "invalid argument \"xxx\"",
},
{
name: "fail: writeGenesis w/o priv_validator_key.json",
args: []string{"writeGenesis"},
expectedError: "open .*/config/priv_validator_key.json: no such file or directory",
},
{
name: "fail: writeFenesis w/o genesis.json",
args: []string{"writeGenesis"},
setup: func(t *testing.T, path string) {
// Add priv_validator_key.json to path
bz, err := os.ReadFile("testdata/consumer/config/priv_validator_key.json")
require.NoError(t, err)
err = os.WriteFile(filepath.Join(path, "config", "priv_validator_key.json"), bz, 0o777)
require.NoError(t, err)
},
expectedError: ".*/config/genesis.json does not exist, run `init` first",
},

{
name: "ok: writeGenesis",
args: []string{"writeGenesis"},
setup: func(t *testing.T, path string) {
// Add priv_validator_key.json to path
bz, err := os.ReadFile("testdata/consumer/config/priv_validator_key.json")
require.NoError(t, err)
err = os.WriteFile(filepath.Join(path, "config", "priv_validator_key.json"), bz, 0o777)
require.NoError(t, err)

// Add genesis.json to path
bz, err = os.ReadFile("testdata/consumer/config/genesis.json")
require.NoError(t, err)
err = os.WriteFile(filepath.Join(path, "config", "genesis.json"), bz, 0o777)
require.NoError(t, err)
},
},
{
name: "ok: isInitialized returns false",
args: []string{"isInitialized"},
expectedOutput: "false",
},
{
name: "ok: isInitialized returns true",
args: []string{"isInitialized"},
setup: func(t *testing.T, path string) {
// isInitialized returns true if there's a consumer genesis with an
// InitialValSet length != 0
// Add priv_validator_key.json to path
bz, err := os.ReadFile("testdata/consumer/config/priv_validator_key.json")
require.NoError(t, err)
err = os.WriteFile(filepath.Join(path, "config", "priv_validator_key.json"), bz, 0o777)
require.NoError(t, err)

// Add genesis.json to path
bz, err = os.ReadFile("testdata/consumer/config/genesis.json")
require.NoError(t, err)
err = os.WriteFile(filepath.Join(path, "config", "genesis.json"), bz, 0o777)
require.NoError(t, err)

// Call writeGenesis to create the genesis
chainer := mocks.NewChainerInterface(t)
chainer.EXPECT().ID().Return("id", nil).Maybe()
chainer.EXPECT().AppPath().Return("apppath").Maybe()
chainer.EXPECT().ConfigPath().Return("configpath").Maybe()
chainer.EXPECT().Home().Return(path, nil).Maybe()
chainer.EXPECT().RPCPublicAddress().Return("rpcPublicAddress", nil).Maybe()
_, err = Execute(context.Background(), consumerPlugin, []string{"writeGenesis"}, plugin.WithChain(chainer))
require.NoError(t, err)
},
expectedOutput: "true",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
homePath := t.TempDir()
err := os.MkdirAll(filepath.Join(homePath, "config"), 0o777)
require.NoError(t, err)
chainer := mocks.NewChainerInterface(t)
chainer.EXPECT().ID().Return("id", nil).Maybe()
chainer.EXPECT().AppPath().Return("apppath").Maybe()
chainer.EXPECT().ConfigPath().Return("configpath").Maybe()
chainer.EXPECT().Home().Return(homePath, nil).Maybe()
chainer.EXPECT().RPCPublicAddress().Return("rpcPublicAddress", nil).Maybe()
if tt.setup != nil {
tt.setup(t, homePath)
}

out, err := Execute(
context.Background(),
consumerPlugin,
tt.args,
plugin.WithChain(chainer),
)

if tt.expectedError != "" {
require.Error(t, err)
require.Regexp(t, tt.expectedError, err.Error())
return
}
require.NoError(t, err)
require.Equal(t, tt.expectedOutput, out)
})
}
}
18 changes: 9 additions & 9 deletions ignite/internal/plugin/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ import (

func TestPluginExecute(t *testing.T) {
tests := []struct {
name string
pluginPath string
expectedOut string
expectedError string
name string
pluginPath string
expectedOutput string
expectedError string
}{
{
name: "fail: plugin doesnt exist",
pluginPath: "/not/exists",
expectedError: "local app path \"/not/exists\" not found: stat /not/exists: no such file or directory",
},
{
name: "ok: plugin execute ok ",
pluginPath: "testdata/execute_ok",
expectedOut: "ok args=[arg1 arg2] chainid=id appPath=apppath configPath=configpath home=home rpcAddress=rpcPublicAddress\n",
name: "ok: plugin execute ok",
pluginPath: "testdata/execute_ok",
expectedOutput: "ok args=[arg1 arg2] chainid=id appPath=apppath configPath=configpath home=home rpcAddress=rpcPublicAddress\n",
},
{
name: "ok: plugin execute fail ",
name: "ok: plugin execute fail",
pluginPath: "testdata/execute_fail",
expectedError: "fail",
},
Expand Down Expand Up @@ -64,7 +64,7 @@ func TestPluginExecute(t *testing.T) {
return
}
require.NoError(t, err)
require.Equal(t, tt.expectedOut, out)
require.Equal(t, tt.expectedOutput, out)
})
}
}
9 changes: 9 additions & 0 deletions ignite/internal/plugin/testdata/consumer/config/genesis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"app_name": "test",
"app_version": "",
"genesis_time": "2024-01-19T10:27:44.742750573Z",
"chain_id": "test",
"initial_height": 1,
"app_hash": null,
"app_state": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"address": "2D3C15095E5EAA318CAEDE1C2D02C77581584751",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "uBOT+dDuUvXjJrkfwMNrS4bRT4/O+fBnpwfYpR6n1Wk="
},
"priv_key": {
"type": "tendermint/PrivKeyEd25519",
"value": "HovIzTJTGMrQx5oBikjfypyMZYF9QP5MxS+S+S/3QYq4E5P50O5S9eMmuR/Aw2tLhtFPj8758GenB9ilHqfVaQ=="
}
}
Loading

0 comments on commit 09f1398

Please sign in to comment.