feat(x/genutil): bulk add genesis accounts (backport #21372) (#21544)

Co-authored-by: Reece Williams <31943163+Reecepbcups@users.noreply.github.com>
Co-authored-by: Reece Williams <reecepbcups@gmail.com>
Co-authored-by: Julien Robert <julien@rbrt.fr>
This commit is contained in:
mergify[bot] 2024-09-04 19:27:47 +00:00 committed by GitHub
parent d0c8d675d7
commit c8aec4d15e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 381 additions and 1 deletions

View File

@ -41,6 +41,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
## Features
* (cli) [#20779](https://github.com/cosmos/cosmos-sdk/pull/20779) Added `module-hash-by-height` command to query and retrieve module hashes at a specified blockchain height, enhancing debugging capabilities.
* (cli) [#21372](https://github.com/cosmos/cosmos-sdk/pull/21372) Added a `bulk-add-genesis-account` genesis command to add many genesis accounts at once.
### Improvements
@ -104,7 +105,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (types) [#19759](https://github.com/cosmos/cosmos-sdk/pull/19759) Align SignerExtractionAdapter in PriorityNonceMempool Remove.
* (client) [#19870](https://github.com/cosmos/cosmos-sdk/pull/19870) Add new query command `wait-tx`. Alias `event-query-tx-for` to `wait-tx` for backward compatibility.
### Improvements
### Improvements
* (telemetry) [#19903](https://github.com/cosmos/cosmos-sdk/pull/19903) Conditionally emit metrics based on enablement.
* **Introduction of `Now` Function**: Added a new function called `Now` to the telemetry package. It returns the current system time if telemetry is enabled, or a zero time if telemetry is not enabled.

View File

@ -39,6 +39,7 @@ func CommandsWithCustomMigrationMap(txConfig client.TxConfig, moduleBasics modul
CollectGenTxsCmd(banktypes.GenesisBalancesIterator{}, defaultNodeHome, gentxModule.GenTxValidator, txConfig.SigningContext().ValidatorAddressCodec()),
ValidateGenesisCmd(moduleBasics),
AddGenesisAccountCmd(defaultNodeHome, txConfig.SigningContext().AddressCodec()),
AddBulkGenesisAccountCmd(defaultNodeHome, txConfig.SigningContext().AddressCodec()),
)
return cmd

View File

@ -2,7 +2,9 @@ package cli
import (
"bufio"
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
@ -91,3 +93,68 @@ contain valid denominations. Accounts may optionally be supplied with vesting pa
return cmd
}
// AddBulkGenesisAccountCmd returns bulk-add-genesis-account cobra Command.
// This command is provided as a default, applications are expected to provide their own command if custom genesis accounts are needed.
func AddBulkGenesisAccountCmd(defaultNodeHome string, addressCodec address.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "bulk-add-genesis-account [/file/path.json]",
Short: "Bulk add genesis accounts to genesis.json",
Example: `bulk-add-genesis-account accounts.json
where accounts.json is:
[
{
"address": "cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5",
"coins": [
{ "denom": "umuon", "amount": "100000000" },
{ "denom": "stake", "amount": "200000000" }
]
},
{
"address": "cosmos1e0jnq2sun3dzjh8p2xq95kk0expwmd7shwjpfg",
"coins": [
{ "denom": "umuon", "amount": "500000000" }
],
"vesting_amt": [
{ "denom": "umuon", "amount": "400000000" }
],
"vesting_start": 1724711478,
"vesting_end": 1914013878
}
]
`,
Long: `Add genesis accounts in bulk to genesis.json. The provided account must specify
the account address and a list of initial coins. The list of initial tokens must
contain valid denominations. Accounts may optionally be supplied with vesting parameters.
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
serverCtx := server.GetServerContextFromCmd(cmd)
config := serverCtx.Config
config.SetRoot(clientCtx.HomeDir)
f, err := os.Open(args[0])
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()
var accounts []genutil.GenesisAccount
if err := json.NewDecoder(f).Decode(&accounts); err != nil {
return fmt.Errorf("failed to decode JSON: %w", err)
}
appendflag, _ := cmd.Flags().GetBool(flagAppendMode)
return genutil.AddGenesisAccounts(clientCtx.Codec, addressCodec, accounts, appendflag, config.GenesisFile())
},
}
cmd.Flags().Bool(flagAppendMode, false, "append the coins to an account already in the genesis.json file")
cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory")
flags.AddQueryFlagsToCmd(cmd)
return cmd
}

View File

@ -2,7 +2,10 @@ package cli_test
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"testing"
"github.com/spf13/viper"
@ -20,8 +23,11 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
"github.com/cosmos/cosmos-sdk/x/auth"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/genutil"
genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli"
genutiltest "github.com/cosmos/cosmos-sdk/x/genutil/client/testutil"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
)
func TestAddGenesisAccountCmd(t *testing.T) {
@ -106,3 +112,158 @@ func TestAddGenesisAccountCmd(t *testing.T) {
})
}
}
func TestBulkAddGenesisAccountCmd(t *testing.T) {
_, _, addr1 := testdata.KeyTestPubAddr()
_, _, addr2 := testdata.KeyTestPubAddr()
_, _, addr3 := testdata.KeyTestPubAddr()
addr1Str := addr1.String()
addr2Str := addr2.String()
addr3Str := addr3.String()
tests := []struct {
name string
state [][]genutil.GenesisAccount
expected map[string]sdk.Coins
appendFlag bool
expectErr bool
}{
{
name: "invalid address",
state: [][]genutil.GenesisAccount{
{
{
Address: "invalid",
Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)),
},
},
},
expectErr: true,
},
{
name: "no append flag for multiple account adds",
state: [][]genutil.GenesisAccount{
{
{
Address: addr1Str,
Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)),
},
},
{
{
Address: addr1Str,
Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 2)),
},
},
},
appendFlag: false,
expectErr: true,
},
{
name: "multiple additions with append",
state: [][]genutil.GenesisAccount{
{
{
Address: addr1Str,
Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)),
},
{
Address: addr2Str,
Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)),
},
},
{
{
Address: addr1Str,
Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 2)),
},
{
Address: addr2Str,
Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 1)),
},
{
Address: addr3Str,
Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)),
},
},
},
expected: map[string]sdk.Coins{
addr1Str: sdk.NewCoins(sdk.NewInt64Coin("test", 3)),
addr2Str: sdk.NewCoins(sdk.NewInt64Coin("test", 1), sdk.NewInt64Coin("stake", 1)),
addr3Str: sdk.NewCoins(sdk.NewInt64Coin("test", 1)),
},
appendFlag: true,
expectErr: false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
home := t.TempDir()
logger := log.NewNopLogger()
cfg, err := genutiltest.CreateDefaultCometConfig(home)
require.NoError(t, err)
appCodec := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}).Codec
err = genutiltest.ExecInitCmd(testMbm, home, appCodec)
require.NoError(t, err)
serverCtx := server.NewContext(viper.New(), cfg, logger)
clientCtx := client.Context{}.WithCodec(appCodec).WithHomeDir(home)
ctx := context.Background()
ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx)
ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx)
// The first iteration (pre-append) may not error.
// Check if any errors after all state transitions to genesis.
doesErr := false
// apply multiple state iterations if applicable (e.g. --append)
for _, state := range tc.state {
bz, err := json.Marshal(state)
require.NoError(t, err)
filePath := path.Join(home, "accounts.json")
err = os.WriteFile(filePath, bz, 0o600)
require.NoError(t, err)
cmd := genutilcli.AddBulkGenesisAccountCmd(home, addresscodec.NewBech32Codec("cosmos"))
args := []string{filePath}
if tc.appendFlag {
args = append(args, "--append")
}
cmd.SetArgs(args)
err = cmd.ExecuteContext(ctx)
if err != nil {
doesErr = true
}
}
require.Equal(t, tc.expectErr, doesErr)
// an error already occurred, no need to check the state
if doesErr {
return
}
appState, _, err := genutiltypes.GenesisStateFromGenFile(path.Join(home, "config", "genesis.json"))
require.NoError(t, err)
bankState := banktypes.GetGenesisStateFromAppState(appCodec, appState)
require.EqualValues(t, len(tc.expected), len(bankState.Balances))
for _, acc := range bankState.Balances {
require.True(t, tc.expected[acc.Address].Equal(acc.Coins), "expected: %v, got: %v", tc.expected[acc.Address], acc.Coins)
}
expectedSupply := sdk.NewCoins()
for _, coins := range tc.expected {
expectedSupply = expectedSupply.Add(coins...)
}
require.Equal(t, expectedSupply, bankState.Supply)
})
}
}

View File

@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"cosmossdk.io/core/address"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
@ -141,3 +143,151 @@ func AddGenesisAccount(
appGenesis.AppState = appStateJSON
return ExportGenesisFile(appGenesis, genesisFileURL)
}
type GenesisAccount struct {
// Base
Address string `json:"address"`
Coins sdk.Coins `json:"coins"`
// Vesting
VestingAmt sdk.Coins `json:"vesting_amt,omitempty"`
VestingStart int64 `json:"vesting_start,omitempty"`
VestingEnd int64 `json:"vesting_end,omitempty"`
// Module
ModuleName string `json:"module_name,omitempty"`
}
// AddGenesisAccounts adds genesis accounts to the genesis state.
// Where `cdc` is the client codec, `accounts` are the genesis accounts to add,
// `appendAcct` updates the account if already exists, and `genesisFileURL` is the path/url of the current genesis file.
func AddGenesisAccounts(
cdc codec.Codec,
ac address.Codec,
accounts []GenesisAccount,
appendAcct bool,
genesisFileURL string,
) error {
appState, appGenesis, err := genutiltypes.GenesisStateFromGenFile(genesisFileURL)
if err != nil {
return fmt.Errorf("failed to unmarshal genesis state: %w", err)
}
authGenState := authtypes.GetGenesisStateFromAppState(cdc, appState)
bankGenState := banktypes.GetGenesisStateFromAppState(cdc, appState)
accs, err := authtypes.UnpackAccounts(authGenState.Accounts)
if err != nil {
return fmt.Errorf("failed to get accounts from any: %w", err)
}
newSupplyCoinsCache := sdk.NewCoins()
balanceCache := make(map[string]banktypes.Balance)
for _, acc := range accs {
for _, balance := range bankGenState.GetBalances() {
if balance.Address == acc.GetAddress().String() {
balanceCache[acc.GetAddress().String()] = balance
}
}
}
for _, acc := range accounts {
addr := acc.Address
coins := acc.Coins
accAddr, err := ac.StringToBytes(addr)
if err != nil {
return fmt.Errorf("failed to parse account address %s: %w", addr, err)
}
// create concrete account type based on input parameters
var genAccount authtypes.GenesisAccount
balances := banktypes.Balance{Address: addr, Coins: coins.Sort()}
baseAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0)
vestingAmt := acc.VestingAmt
if !vestingAmt.IsZero() {
vestingStart := acc.VestingStart
vestingEnd := acc.VestingEnd
baseVestingAccount, err := authvesting.NewBaseVestingAccount(baseAccount, vestingAmt.Sort(), vestingEnd)
if err != nil {
return fmt.Errorf("failed to create base vesting account: %w", err)
}
if (balances.Coins.IsZero() && !baseVestingAccount.OriginalVesting.IsZero()) ||
baseVestingAccount.OriginalVesting.IsAnyGT(balances.Coins) {
return errors.New("vesting amount cannot be greater than total amount")
}
switch {
case vestingStart != 0 && vestingEnd != 0:
genAccount = authvesting.NewContinuousVestingAccountRaw(baseVestingAccount, vestingStart)
case vestingEnd != 0:
genAccount = authvesting.NewDelayedVestingAccountRaw(baseVestingAccount)
default:
return errors.New("invalid vesting parameters; must supply start and end time or end time")
}
} else if acc.ModuleName != "" {
genAccount = authtypes.NewEmptyModuleAccount(acc.ModuleName, authtypes.Burner, authtypes.Minter)
} else {
genAccount = baseAccount
}
if err := genAccount.Validate(); err != nil {
return fmt.Errorf("failed to validate new genesis account: %w", err)
}
if _, ok := balanceCache[addr]; ok {
if !appendAcct {
return fmt.Errorf(" Account %s already exists\nUse `append` flag to append account at existing address", accAddr)
}
for idx, acc := range bankGenState.Balances {
if acc.Address != addr {
continue
}
updatedCoins := acc.Coins.Add(coins...)
bankGenState.Balances[idx] = banktypes.Balance{Address: addr, Coins: updatedCoins.Sort()}
break
}
} else {
accs = append(accs, genAccount)
bankGenState.Balances = append(bankGenState.Balances, balances)
}
newSupplyCoinsCache = newSupplyCoinsCache.Add(coins...)
}
accs = authtypes.SanitizeGenesisAccounts(accs)
authGenState.Accounts, err = authtypes.PackAccounts(accs)
if err != nil {
return fmt.Errorf("failed to convert accounts into any's: %w", err)
}
appState[authtypes.ModuleName], err = cdc.MarshalJSON(&authGenState)
if err != nil {
return fmt.Errorf("failed to marshal auth genesis state: %w", err)
}
bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances)
bankGenState.Supply = bankGenState.Supply.Add(newSupplyCoinsCache...)
appState[banktypes.ModuleName], err = cdc.MarshalJSON(bankGenState)
if err != nil {
return fmt.Errorf("failed to marshal bank genesis state: %w", err)
}
appStateJSON, err := json.Marshal(appState)
if err != nil {
return fmt.Errorf("failed to marshal application genesis state: %w", err)
}
appGenesis.AppState = appStateJSON
return ExportGenesisFile(appGenesis, genesisFileURL)
}