Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions cmd/atomoned/cmd/inplace_testnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package cmd

import (
"errors"
"io"
"strings"

"github.com/spf13/cast"
"github.com/spf13/cobra"

"github.com/cometbft/cometbft/crypto"
"github.com/cometbft/cometbft/libs/bytes"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"

dbm "github.com/cosmos/cosmos-db"

"cosmossdk.io/log"
"cosmossdk.io/math"
storetypes "cosmossdk.io/store/types"

"github.com/cosmos/cosmos-sdk/client/flags"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/server"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
sdk "github.com/cosmos/cosmos-sdk/types"
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

atomone "github.com/atomone-hub/atomone/app"
"github.com/atomone-hub/atomone/app/params"
)

const valVotingPower int64 = 900000000000000

var flagAccountsToFund = "accounts-to-fund"

type valArgs struct {
newValAddr bytes.HexBytes
newOperatorAddress string
newValPubKey crypto.PubKey
accountsToFund []string
upgradeToTrigger string
homeDir string
}

func NewInPlaceTestnetCmd() *cobra.Command {
cmd := server.InPlaceTestnetCreator(newTestnetApp)
cmd.Example = `atomoned in-place-testnet testing-1 atonevaloper1w7f3xx7e75p4l7qdym5msqem9rd4dyc4jfa7ag --home $HOME/.atomone/validator1 --validator-privkey=6dq+/KHNvyiw2TToCgOpUpQKIzrLs69Rb8Az39xvmxPHNoPxY1Cil8FY+4DhT9YwD6s0tFABMlLcpaylzKKBOg== --accounts-to-fund="atone1f7twgcq4ypzg7y24wuywy06xmdet8pc4m7dv9c,atone1qvuhm5m644660nd8377d6l7yz9e9hhm9hv8p87"`

cmd.Flags().String(flagAccountsToFund, "", "Comma-separated list of account addresses that will be funded for testing purposes")
return cmd
}

// newTestnetApp starts by running the normal newApp method. From there, the app interface returned is modified in order
// for a testnet to be created from the provided app.
func newTestnetApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts servertypes.AppOptions) servertypes.Application {
// Create an app and type cast to an App
newApp := newApp(logger, db, traceStore, appOpts)
testApp, ok := newApp.(*atomone.AtomOneApp)
if !ok {
panic("app created from newApp is not of type App")
}

// Get command args
args, err := getCommandArgs(appOpts)
if err != nil {
panic(err)
}

return initAppForTestnet(testApp, args)
}

func initAppForTestnet(app *atomone.AtomOneApp, args valArgs) *atomone.AtomOneApp {
// Required Changes:
//
ctx := app.NewUncachedContext(true, cmtproto.Header{})

pubkey := &ed25519.PubKey{Key: args.newValPubKey.Bytes()}
pubkeyAny, err := codectypes.NewAnyWithValue(pubkey)
handleErr(err)

// STAKING
//

// Create Validator struct for our new validator.
newVal := stakingtypes.Validator{
OperatorAddress: args.newOperatorAddress,
ConsensusPubkey: pubkeyAny,
Jailed: false,
Status: stakingtypes.Bonded,
Tokens: math.NewInt(valVotingPower),
DelegatorShares: math.LegacyMustNewDecFromStr("10000000"),
Description: stakingtypes.Description{
Moniker: "Testnet Validator",
},
Commission: stakingtypes.Commission{
CommissionRates: stakingtypes.CommissionRates{
Rate: math.LegacyMustNewDecFromStr("0.05"),
MaxRate: math.LegacyMustNewDecFromStr("0.1"),
MaxChangeRate: math.LegacyMustNewDecFromStr("0.05"),
},
},
MinSelfDelegation: math.OneInt(),
}

validator, err := app.StakingKeeper.ValidatorAddressCodec().StringToBytes(newVal.GetOperator())
handleErr(err)

// Remove all validators from power store
stakingKey := app.GetKey(stakingtypes.ModuleName)
stakingStore := ctx.KVStore(stakingKey)
iterator, err := app.StakingKeeper.ValidatorsPowerStoreIterator(ctx)
handleErr(err)

for ; iterator.Valid(); iterator.Next() {
stakingStore.Delete(iterator.Key())
}
iterator.Close()

// Remove all validators from last validators store
iterator, err = app.StakingKeeper.LastValidatorsIterator(ctx)
handleErr(err)

for ; iterator.Valid(); iterator.Next() {
stakingStore.Delete(iterator.Key())
}
iterator.Close()

// Remove all validators from validators store
iterator = stakingStore.Iterator(stakingtypes.ValidatorsKey, storetypes.PrefixEndBytes(stakingtypes.ValidatorsKey))
for ; iterator.Valid(); iterator.Next() {
stakingStore.Delete(iterator.Key())
}
iterator.Close()

// Remove all validators from unbonding queue
iterator = stakingStore.Iterator(stakingtypes.ValidatorQueueKey, storetypes.PrefixEndBytes(stakingtypes.ValidatorQueueKey))
for ; iterator.Valid(); iterator.Next() {
stakingStore.Delete(iterator.Key())
}
iterator.Close()

// Add our validator to power and last validators store
handleErr(app.StakingKeeper.SetValidator(ctx, newVal))
handleErr(app.StakingKeeper.SetValidatorByConsAddr(ctx, newVal))
handleErr(app.StakingKeeper.SetValidatorByPowerIndex(ctx, newVal))
handleErr(app.StakingKeeper.SetLastValidatorPower(ctx, validator, 0))
handleErr(app.StakingKeeper.Hooks().AfterValidatorCreated(ctx, validator))
Comment thread
Pantani marked this conversation as resolved.

// DISTRIBUTION
//

// Initialize records for this validator across all distribution stores
handleErr(app.DistrKeeper.SetValidatorHistoricalRewards(ctx, validator, 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1)))
handleErr(app.DistrKeeper.SetValidatorCurrentRewards(ctx, validator, distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1)))
handleErr(app.DistrKeeper.SetValidatorAccumulatedCommission(ctx, validator, distrtypes.InitialValidatorAccumulatedCommission()))
handleErr(app.DistrKeeper.SetValidatorOutstandingRewards(ctx, validator, distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}))

// SLASHING
//

// Set validator signing info for our new validator.
newConsAddr := sdk.ConsAddress(args.newValAddr.Bytes())
newValidatorSigningInfo := slashingtypes.ValidatorSigningInfo{
Address: newConsAddr.String(),
StartHeight: app.LastBlockHeight() - 1,
Tombstoned: false,
}
_ = app.SlashingKeeper.SetValidatorSigningInfo(ctx, newConsAddr, newValidatorSigningInfo)

// BANK
//
bondDenom, err := app.StakingKeeper.BondDenom(ctx)
handleErr(err)
if bondDenom == "" {
bondDenom = params.BondDenom
}

defaultCoins := sdk.NewCoins(sdk.NewInt64Coin(bondDenom, 1000000000))
Comment thread
tbruyelle marked this conversation as resolved.

// Fund local accounts
for _, accountStr := range args.accountsToFund {
handleErr(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins))

account, err := app.AccountKeeper.AddressCodec().StringToBytes(accountStr)
handleErr(err)

handleErr(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, account, defaultCoins))
}

return app
}

// parse the input flags and returns valArgs
func getCommandArgs(appOpts servertypes.AppOptions) (valArgs, error) {
args := valArgs{}

newValAddr, ok := appOpts.Get(server.KeyNewValAddr).(bytes.HexBytes)
if !ok {
return args, errors.New("newValAddr is not of type bytes.HexBytes")
}
args.newValAddr = newValAddr
newValPubKey, ok := appOpts.Get(server.KeyUserPubKey).(crypto.PubKey)
if !ok {
return args, errors.New("newValPubKey is not of type crypto.PubKey")
}
args.newValPubKey = newValPubKey
newOperatorAddress, ok := appOpts.Get(server.KeyNewOpAddr).(string)
if !ok {
return args, errors.New("newOperatorAddress is not of type string")
}
args.newOperatorAddress = newOperatorAddress
upgradeToTrigger, ok := appOpts.Get(server.KeyTriggerTestnetUpgrade).(string)
if !ok {
return args, errors.New("upgradeToTrigger is not of type string")
}
args.upgradeToTrigger = upgradeToTrigger

// parsing and set accounts to fund
accountsString := cast.ToString(appOpts.Get(flagAccountsToFund))
if len(accountsString) > 0 {
args.accountsToFund = strings.Split(accountsString, ",")
}

// home dir
homeDir := cast.ToString(appOpts.Get(flags.FlagHome))
if homeDir == "" {
return args, errors.New("invalid home dir")
}
args.homeDir = homeDir

return args, nil
}

// handleErr prints the error and exits the program if the error is not nil
func handleErr(err error) {
if err != nil {
panic(err)
}
}
1 change: 1 addition & 0 deletions cmd/atomoned/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func initRootCmd(
) {
rootCmd.AddCommand(
genutilcli.InitCmd(basicManager, atomone.DefaultNodeHome),
NewInPlaceTestnetCmd(),
tmcli.NewCompletionCmd(rootCmd, true),
NewTestnetCmd(basicManager, banktypes.GenesisBalancesIterator{}),
addDebugCommands(debug.Cmd()),
Expand Down
85 changes: 85 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,88 @@ localnet.
the chain data). Block production should restart.
10. Check that the upgrade procedure has been executed properly.
11. Restart the node to ensure it continues producing blocks after the upgrade.

## In-place Testnet from Mainnet State

The `in-place-testnet` command takes an existing chain's data directory
(typically a mainnet snapshot) and converts it into a single-validator
testnet running locally. This is useful to:

- Dry-run an upgrade handler against real mainnet state.
- Reproduce a mainnet bug locally with full balances and history.
- Test governance flows that require concentrated voting power (your sole
validator can pass any active proposal in seconds).

### Setup

1. Initialize a fresh node home:
```sh
atomoned init localnet --home ~/.atomone/validator1 --chain-id atomone-1
```

2. Replace the generated genesis with the `atomone-1` mainnet genesis:
```sh
wget -O ~/.atomone/validator1/config/genesis.json \
https://snapshots.polkachu.com/genesis/atomone/genesis.json
```

3. Set `minimum-gas-prices` in `~/.atomone/validator1/config/app.toml` (e.g.
`0uatone`) and wire `seeds` / `persistent_peers` in `config.toml` from the
[`atomone-hub/networks`](https://github.com/atomone-hub/networks/tree/main/atomone-1)
repo.

4. Download a recent mainnet snapshot (e.g. from
[polkachu](https://polkachu.com/tendermint_snapshots/atomone) or itrocket)
and extract it. `lz4` is required for decompression.
```sh
rm -rf ~/.atomone/validator1/data && mkdir ~/.atomone/validator1/data
echo '{ "height": "0", "round": 0, "step": 0 }' \
> ~/.atomone/validator1/data/priv_validator_state.json
lz4 -d <snapshot>.tar.lz4 -c | tar -x -C ~/.atomone/validator1
```

5. Start the node so it loads the snapshot, then stop it cleanly with
`Ctrl-C` once `atomoned status` shows the snapshot height. The node does
not need to be caught up to tip; `in-place-testnet` only needs the latest
committed block in the blockstore.
```sh
atomoned start --home ~/.atomone/validator1 --x-crisis-skip-assert-invariants
```

The binary version must be compatible with the snapshot height (e.g.
`v3.3.0` for a current mainnet snapshot). To dry-run a future upgrade,
load the snapshot with the pre-upgrade binary first, then run
`in-place-testnet` with the post-upgrade binary and
`--trigger-testnet-upgrade=<plan-name>`.

### Convert to Testnet

1. Add a local key whose valoper address will become the new sole validator:
```sh
atomoned keys add testnet-val --home ~/.atomone/validator1 --keyring-backend test
VAL_OPER=$(atomoned keys show testnet-val --bech val -a \
--home ~/.atomone/validator1 --keyring-backend test)
ACCOUNT=$(atomoned keys show testnet-val -a \
--home ~/.atomone/validator1 --keyring-backend test)
```

2. Run `in-place-testnet`:
```sh
atomoned in-place-testnet testing-1 "$VAL_OPER" \
--home ~/.atomone/validator1 \
--accounts-to-fund="$ACCOUNT"
```
Confirm with `y` when prompted. The node will restart with chain-id
`testing-1`, your local key as the only validator, and the funded
accounts holding 1000 ATONE each.

3. Verify:
```sh
atomoned status --home ~/.atomone/validator1 \
| jq '.NodeInfo.network, .ValidatorInfo'
atomoned q staking validators --home ~/.atomone/validator1 -o json \
| jq '.validators | length'
atomoned q bank balances "$ACCOUNT" --home ~/.atomone/validator1
```
Expect `network: "testing-1"`, exactly one validator, and
`1000000000 uatone` on the funded account.
Loading