From 0682f794d2c72f8e1b159bca33b137639c7225cf Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Mon, 23 Feb 2026 19:54:03 +0100 Subject: [PATCH 1/7] fix: add default EVM coin info initialization and clean up governance hooks --- .github/workflows/branch.yml | 1 + app/app.go | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 9001f5b..82d6e9e 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -4,6 +4,7 @@ on: push: branches: - "main" + - "fix-check-tx-issue" concurrency: # Cancel old runs if there is a new commit in the same branch diff --git a/app/app.go b/app/app.go index 4d82b30..c8f731a 100644 --- a/app/app.go +++ b/app/app.go @@ -491,7 +491,12 @@ func New( app.SlashingKeeper, appCodec, ), - ) + ).WithDefaultEvmCoinInfo(evmtypes.EvmCoinInfo{ + Denom: BaseDenom, + ExtendedDenom: BaseDenom, + DisplayDenom: Denom, + Decimals: 18, + }) // ERC20 Keeper app.Erc20Keeper = erc20keeper.NewKeeper( @@ -572,9 +577,7 @@ func New( govKeeper.SetLegacyRouter(govRouter) app.GovKeeper = *govKeeper.SetHooks( - govtypes.NewMultiGovHooks( - // register the governance hooks - ), + govtypes.NewMultiGovHooks(), ) /**** IBC Routing ****/ From 69d24b5e9e88f51bec9182c8d92bc3945eb0019a Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Mon, 23 Feb 2026 19:56:57 +0100 Subject: [PATCH 2/7] fix: comment out unit and integration tests in Dockerfile for now --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b60eff4..3f6bb5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,9 +26,9 @@ RUN make build FROM base AS integration RUN make lint # Unit tests -RUN make test-poa +# RUN make test-poa # Integration tests -RUN make test-integration +# RUN make test-integration # Simulation tests # TODO: Restore simulation tests if possible # RUN make test-sim-benchmark-simulation From ebd405aaffe90b450740f9649d4db96d724a55e5 Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Mon, 23 Feb 2026 21:16:08 +0100 Subject: [PATCH 3/7] fix: update evm-sec-papyrus tag to v0.6.0-xrplevm.4 and re-enable tests in Dockerfile --- Dockerfile | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3f6bb5e..b60eff4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,9 +26,9 @@ RUN make build FROM base AS integration RUN make lint # Unit tests -# RUN make test-poa +RUN make test-poa # Integration tests -# RUN make test-integration +RUN make test-integration # Simulation tests # TODO: Restore simulation tests if possible # RUN make test-sim-benchmark-simulation diff --git a/go.mod b/go.mod index 33ef93b..d881945 100644 --- a/go.mod +++ b/go.mod @@ -285,7 +285,7 @@ replace ( // use Cosmos-SDK fork to enable Ledger functionality github.com/cosmos/cosmos-sdk => github.com/xrplevm/cosmos-sdk v0.53.6-xrplevm.1 // cosmos evm private fork - github.com/cosmos/evm => github.com/xrplevm/evm-sec-papyrus v0.6.0-xrplevm.3 + github.com/cosmos/evm => github.com/xrplevm/evm-sec-papyrus v0.6.0-xrplevm.4 // fix cosmos-sdk store path mismatch // github.com/cosmos/cosmos-sdk/store => cosmossdk.io/store v1.1.2 github.com/ethereum/go-ethereum => github.com/cosmos/go-ethereum v0.0.0-20250806193535-2fc7571efa91 diff --git a/go.sum b/go.sum index 26e6501..cdbccdd 100644 --- a/go.sum +++ b/go.sum @@ -1727,8 +1727,8 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrplevm/cosmos-sdk v0.53.6-xrplevm.1 h1:fBMklkMKZbrVoEhGU0JyeaINkRA9lVA9K/zRY73EGh0= github.com/xrplevm/cosmos-sdk v0.53.6-xrplevm.1/go.mod h1:N6YuprhAabInbT3YGumGDKONbvPX5dNro7RjHvkQoKE= -github.com/xrplevm/evm-sec-papyrus v0.6.0-xrplevm.3 h1:d5Ukiu9WmGOzFIYWNxWN44D8+G9d9w8S9Jvcv1NNTOw= -github.com/xrplevm/evm-sec-papyrus v0.6.0-xrplevm.3/go.mod h1:MUrVrODPlGdehAzc2KjUPEVHLtA7WChEvrFLs5kWa9E= +github.com/xrplevm/evm-sec-papyrus v0.6.0-xrplevm.4 h1:byK9DGqwgPZPX/tUxwIw05pyR1DtXMBCDHPoVGcICCg= +github.com/xrplevm/evm-sec-papyrus v0.6.0-xrplevm.4/go.mod h1:MUrVrODPlGdehAzc2KjUPEVHLtA7WChEvrFLs5kWa9E= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= From 4fafc36fa3ba1ac1d01cda3605cf4d4b296841eb Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Tue, 24 Feb 2026 20:04:42 +0100 Subject: [PATCH 4/7] add in-place-testnet cmd --- cmd/exrpd/cmd/in-place-testnet.go | 620 ++++++++++++++++++++++++++++++ cmd/exrpd/cmd/root.go | 1 + 2 files changed, 621 insertions(+) create mode 100644 cmd/exrpd/cmd/in-place-testnet.go diff --git a/cmd/exrpd/cmd/in-place-testnet.go b/cmd/exrpd/cmd/in-place-testnet.go new file mode 100644 index 0000000..d816a0c --- /dev/null +++ b/cmd/exrpd/cmd/in-place-testnet.go @@ -0,0 +1,620 @@ +package cmd + +import ( + "encoding/base64" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + upgradetypes "cosmossdk.io/x/upgrade/types" + "github.com/spf13/cast" + "github.com/spf13/cobra" + + cometdbm "github.com/cometbft/cometbft-db" + "github.com/cometbft/cometbft/crypto" + tmd25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtstate "github.com/cometbft/cometbft/proto/tendermint/state" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + sm "github.com/cometbft/cometbft/state" + "github.com/cometbft/cometbft/store" + tmtypes "github.com/cometbft/cometbft/types" + + dbm "github.com/cosmos/cosmos-db" + + "cosmossdk.io/log" + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client/flags" + "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" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + + xrplevm "github.com/xrplevm/node/v10/app" +) + +const ( + valVotingPower int64 = 1000000000 + valTokens int64 = 1000000000000000 +) + +var ( + flagValidatorOperatorAddress = "validator-operator" + flagValidatorPubKey = "validator-pubkey" + flagValidatorPrivKey = "validator-privkey" + flagAccountsToFund = "accounts-to-fund" + flagReplaceValidator = "replace-validator" + flagReplacedOperatorAddress = "replaced-operator-address" + flagReplacedConsensusAddress = "replaced-consensus-address" + flagXrpOwnerAddress = "xrp-owner-address" + flagUpgradeVersion = "upgrade-version" +) + +type valArgs struct { + validatorOperatorAddress string // valoper address + validatorConsPubKeyByte []byte // validator's consensus public key + validatorConsPrivKey crypto.PrivKey // validator's consensus private key + accountsToFund []sdk.AccAddress // list of accounts to fund and use for testing later on + homeDir string + replaceValidator bool // if set, replaces a validator with new keys + replacedOperatorAddress string // operator address of the validator to be replaced + replacedConsensusAddress string // consensus address of the validator to be replaced + xrpOwnerAddress sdk.AccAddress // new XRP owner address + upgradeVersion string // upgrade version that will be applied +} + +func testnetUnsafeStartLocalValidatorCmd(ac appCreator) *cobra.Command { + cmd := server.StartCmd(ac.newTestingApp, xrplevm.DefaultNodeHome) + cmd.Use = "unsafe-start-local-validator" + cmd.Short = "Updates chain's application and consensus state with provided validator info and starts the node" + cmd.Long = `The unsafe-start-local-validator command modifies both application and consensus stores within a local mainnet node and starts the node, +with the aim of facilitating testing procedures. This command replaces existing validator data with updated information, +thereby removing the old validator set and introducing a new set suitable for local testing purposes. By altering the state extracted from the mainnet node, +it enables developers to configure their local environments to reflect mainnet conditions more accurately. + +Example: + # Wipes the entire validator set and replaces it with a single validator + simd testnet unsafe-start-local-validator --validator-operator="cosmosvaloper17fjdcqy7g80pn0seexcch5pg0dtvs45p57t97r" --validator-pubkey="SLpHEfzQHuuNO9J1BB/hXyiH6c1NmpoIVQ2pMWmyctE=" --validator-privkey="AiayvI2px5CZVl/uOGmacfFjcIBoyk3Oa2JPBO6zEcdIukcR/NAe64070nUEH+FfKIfpzU2amghVDakxabJy0Q==" --accounts-to-fund="cosmos1ju6tlfclulxumtt2kglvnxduj5d93a64r5czge,cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl" [other_server_start_flags] + # Replaces the specified validator with the new keys + simd testnet unsafe-start-local-validator --validator-operator="cosmosvaloper17fjdcqy7g80pn0seexcch5pg0dtvs45p57t97r" --validator-pubkey="SLpHEfzQHuuNO9J1BB/hXyiH6c1NmpoIVQ2pMWmyctE=" --validator-privkey="AiayvI2px5CZVl/uOGmacfFjcIBoyk3Oa2JPBO6zEcdIukcR/NAe64070nUEH+FfKIfpzU2amghVDakxabJy0Q==" --accounts-to-fund="cosmos1ju6tlfclulxumtt2kglvnxduj5d93a64r5czge,cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl" --replace-validator -- replaced-operator-address="cosmosvaloper1r5v5srda7xfth3hn2s26txvrcrntldju7lnwmv" --replaced-consensus-address="04B333F6E43751948C2D56B273DC41C3E13E5932" [other_server_start_flags] + ` + cmd.Flags().String(flagValidatorOperatorAddress, "", "Validator operator address e.g. cosmosvaloper17fjdcqy7g80pn0seexcch5pg0dtvs45p57t97r") + cmd.Flags().String(flagValidatorPubKey, "", "Validator tendermint/PubKeyEd25519 consensus public key from the priv_validator_key.json file") + cmd.Flags().String(flagValidatorPrivKey, "", "Validator tendermint/PrivKeyEd25519 consensus private key from the priv_validator_key.json file") + cmd.Flags().String(flagAccountsToFund, "", "Comma-separated list of account addresses that will be funded for testing purposes") + cmd.Flags().Bool(flagReplaceValidator, false, "Replaces a specified validator with the new keys") + cmd.Flags().String(flagReplacedOperatorAddress, "", "Operator address of the validator to be replaced") + cmd.Flags().String(flagReplacedConsensusAddress, "", "Consensus address of the validator to be replaced") + cmd.Flags().String(flagXrpOwnerAddress, "", "New XRP owner address") + cmd.Flags().String(flagUpgradeVersion, "", "If set, the upgrade version that will be applied right just after the chain starts") + + return cmd +} + +// parse the input flags and returns valArgs +func getCommandArgs(appOpts servertypes.AppOptions) (valArgs, error) { + args := valArgs{} + // validate and set validator operator address + valoperAddress := cast.ToString(appOpts.Get(flagValidatorOperatorAddress)) + if valoperAddress == "" { + return args, errors.New("invalid validator operator address string") + } + _, err := sdk.ValAddressFromBech32(valoperAddress) + if err != nil { + return args, fmt.Errorf("invalid validator operator address format %w", err) + } + args.validatorOperatorAddress = valoperAddress + + // validate and set validator pubkey + validatorPubKey := cast.ToString(appOpts.Get(flagValidatorPubKey)) + if validatorPubKey == "" { + return args, errors.New("invalid validator pubkey string") + } + decPubKey, err := base64.StdEncoding.DecodeString(validatorPubKey) + if err != nil { + return args, fmt.Errorf("cannot decode validator pubkey %w", err) + } + args.validatorConsPubKeyByte = []byte(decPubKey) + + // validate and set validator privkey + validatorPrivKey := cast.ToString(appOpts.Get(flagValidatorPrivKey)) + if validatorPrivKey == "" { + return args, fmt.Errorf("invalid validator private key %w", err) + } + decPrivKey, err := base64.StdEncoding.DecodeString(validatorPrivKey) + if err != nil { + return args, fmt.Errorf("cannot decode validator private key %w", err) + } + args.validatorConsPrivKey = tmd25519.PrivKey([]byte(decPrivKey)) + + // validate and set accounts to fund + accountsString := cast.ToString(appOpts.Get(flagAccountsToFund)) + + for _, account := range strings.Split(accountsString, ",") { + if account != "" { + addr, err := sdk.AccAddressFromBech32(account) + if err != nil { + return args, fmt.Errorf("invalid bech32 address format %w", err) + } + args.accountsToFund = append(args.accountsToFund, addr) + } + } + + replaceValidator := cast.ToBool(appOpts.Get(flagReplaceValidator)) + args.replaceValidator = replaceValidator + if replaceValidator { + replacedOperatorAddress := cast.ToString(appOpts.Get(flagReplacedOperatorAddress)) + if replacedOperatorAddress == "" { + return args, errors.New("invalid replaced validator operator address string") + } + _, err := sdk.ValAddressFromBech32(replacedOperatorAddress) + if err != nil { + return args, fmt.Errorf("invalid replaced validator operator address format %w", err) + } + args.replacedOperatorAddress = replacedOperatorAddress + + replacedConsensusAddress := cast.ToString(appOpts.Get(flagReplacedConsensusAddress)) + if replacedConsensusAddress == "" { + return args, errors.New("invalid replaced validator consensus address string") + } + args.replacedConsensusAddress = replacedConsensusAddress + } + + newXrpOwnerAddress := cast.ToString(appOpts.Get(flagXrpOwnerAddress)) + if newXrpOwnerAddress != "" { + newOwnerAddr, err := sdk.AccAddressFromBech32(newXrpOwnerAddress) + if err != nil { + return args, fmt.Errorf("invalid bech32 address format %w", err) + } + args.xrpOwnerAddress = newOwnerAddr + } + + upgradeVersion := cast.ToString(appOpts.Get(flagUpgradeVersion)) + if upgradeVersion != "" { + args.upgradeVersion = upgradeVersion + } + + // home dir + homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) + if homeDir == "" { + return args, errors.New("invalid home dir") + } + args.homeDir = homeDir + + return args, nil +} + +// returns xrplevm app with modified application and consensus states by replacing validator related data +func (a appCreator) newTestingApp( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + appOpts servertypes.AppOptions, +) servertypes.Application { + app := a.newApp(logger, db, traceStore, appOpts) + gaiaApp, ok := app.(*xrplevm.App) + if !ok { + panic(errors.New("invalid xrplevm application")) + } + + // Get command args + args, err := getCommandArgs(appOpts) + if err != nil { + panic(err) + } + + if args.replaceValidator { + logger.Info("Replacing validator", "operator_address", args.replacedOperatorAddress, "consensus_address", args.replacedConsensusAddress) + } else { + logger.Info("Clearing the validator set") + } + + err = updateConsensusState(logger, appOpts, gaiaApp.CommitMultiStore().LatestVersion(), args) + if err != nil { + panic(err) + } + err = updateApplicationState(gaiaApp, args) + if err != nil { + panic(err) + } + + return gaiaApp +} + +func updateApplicationState(app *xrplevm.App, args valArgs) error { + pubkey := &ed25519.PubKey{Key: args.validatorConsPubKeyByte} + pubkeyAny, err := types.NewAnyWithValue(pubkey) + if err != nil { + return err + } + + appCtx := app.BaseApp.NewUncachedContext(true, tmproto.Header{}) + + // STAKING + // Create Validator struct for our new validator. + newVal := stakingtypes.Validator{ + OperatorAddress: args.validatorOperatorAddress, + ConsensusPubkey: pubkeyAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(valTokens), + DelegatorShares: math.LegacyMustNewDecFromStr(fmt.Sprintf("%d", valTokens)), + 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(), + } + + newValAddr, err := app.StakingKeeper.ValidatorAddressCodec().StringToBytes(newVal.GetOperator()) + if err != nil { + return err + } + + store := appCtx.KVStore(app.GetKey(stakingtypes.ModuleName)) + validators, err := app.StakingKeeper.GetAllValidators(appCtx) + if err != nil { + return err + } + + foundTargetValidator := false + for _, v := range validators { + if (args.replaceValidator && v.GetOperator() == args.replacedOperatorAddress) || !args.replaceValidator { + valConsAddr, err := v.GetConsAddr() + if err != nil { + return err + } + + // delete the old validator record + valAddr, err := app.StakingKeeper.ValidatorAddressCodec().StringToBytes(v.GetOperator()) + if err != nil { + return err + } + store.Delete(stakingtypes.GetValidatorKey(valAddr)) + store.Delete(stakingtypes.GetValidatorByConsAddrKey(valConsAddr)) + store.Delete(stakingtypes.GetValidatorsByPowerIndexKey(v, app.StakingKeeper.PowerReduction(appCtx), app.StakingKeeper.ValidatorAddressCodec())) + store.Delete(stakingtypes.GetLastValidatorPowerKey(valAddr)) + if v.IsUnbonding() { + app.StakingKeeper.DeleteValidatorQueueTimeSlice(appCtx, v.UnbondingTime, v.UnbondingHeight) + } + if args.replaceValidator { + foundTargetValidator = true + appCtx.Logger().Info("Found validator to replace", "operator_address", v.GetOperator()) + break + } + } + } + if args.replaceValidator && !foundTargetValidator { + return fmt.Errorf("validator with operator address %s not found", args.replacedOperatorAddress) + } + + // Add our validator to power and last validators store + app.StakingKeeper.SetValidator(appCtx, newVal) + err = app.StakingKeeper.SetValidatorByConsAddr(appCtx, newVal) + if err != nil { + return err + } + app.StakingKeeper.SetValidatorByPowerIndex(appCtx, newVal) + app.StakingKeeper.SetLastValidatorPower(appCtx, newValAddr, valVotingPower) + if err := app.StakingKeeper.Hooks().AfterValidatorCreated(appCtx, newValAddr); err != nil { + return err + } + // DISTRIBUTION + // Initialize records for this validator across all distribution stores + app.DistrKeeper.SetValidatorHistoricalRewards(appCtx, newValAddr, 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1)) + app.DistrKeeper.SetValidatorCurrentRewards(appCtx, newValAddr, distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1)) + app.DistrKeeper.SetValidatorAccumulatedCommission(appCtx, newValAddr, distrtypes.InitialValidatorAccumulatedCommission()) + app.DistrKeeper.SetValidatorOutstandingRewards(appCtx, newValAddr, distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}) + + // SLASHING + // Set validator signing info for our new validator. + newConsAddr := sdk.ConsAddress(pubkey.Address().Bytes()) + newValidatorSigningInfo := slashingtypes.ValidatorSigningInfo{ + Address: newConsAddr.String(), + StartHeight: app.LastBlockHeight() - 1, + Tombstoned: false, + } + + app.SlashingKeeper.SetValidatorSigningInfo(appCtx, newConsAddr, newValidatorSigningInfo) + + // GOVERNANCE + shortVotingPeriod := time.Second * 20 + expeditedVotingPeriod := time.Second * 10 + params, err := app.GovKeeper.Params.Get(appCtx) + if err != nil { + return err + } + params.VotingPeriod = &shortVotingPeriod + params.ExpeditedVotingPeriod = &expeditedVotingPeriod + params.MinDeposit = sdk.NewCoins(sdk.NewInt64Coin(xrplevm.BaseDenom, 1)) + params.ExpeditedMinDeposit = sdk.NewCoins(sdk.NewInt64Coin(xrplevm.BaseDenom, 1)) + err = app.GovKeeper.Params.Set(appCtx, params) + if err != nil { + return err + } + appCtx.Logger().Info("Updated governance params", "voting_period", shortVotingPeriod, "expedited_voting_period", expeditedVotingPeriod, "min_deposit", params.MinDeposit) + + // BANK + if err != nil { + return err + } + // Fund accounts with both the bond denom and axrp (needed for gov deposits and gas) + defaultCoins := sdk.NewCoins( + sdk.NewInt64Coin(xrplevm.BaseDenom, 1000000000000000000), + ) + + // Fund testnet accounts + for _, account := range args.accountsToFund { + for i := 0; i < 50; i++ { + err := app.BankKeeper.MintCoins(appCtx, evmtypes.ModuleName, defaultCoins) + if err != nil { + return err + } + err = app.BankKeeper.SendCoinsFromModuleToAccount(appCtx, evmtypes.ModuleName, account, defaultCoins) + if err != nil { + return err + } + } + } + + // ERC20 + if args.xrpOwnerAddress != nil { + pair, found := app.Erc20Keeper.GetTokenPair(appCtx, app.Erc20Keeper.GetTokenPairID(appCtx, xrplevm.BaseDenom)) + if !found { + return errors.New("token pair not found") + } + app.Erc20Keeper.SetTokenPairOwnerAddress(appCtx, pair, args.xrpOwnerAddress.String()) + } + + // UPGRADE + if args.upgradeVersion != "" { + upgradeHeight := app.LastBlockHeight() + 5 + upgradePlan := upgradetypes.Plan{ + Height: upgradeHeight, + Name: args.upgradeVersion, + Info: "Testnet upgrade to " + args.upgradeVersion, + } + if err := app.UpgradeKeeper.ScheduleUpgrade(appCtx, upgradePlan); err != nil { + panic( + fmt.Errorf( + "failed to schedule upgrade %s during BeginBlock at height %d: %w", + upgradePlan.Name, upgradeHeight, err, + ), + ) + } + } + + return nil +} + +func updateConsensusState(logger log.Logger, appOpts servertypes.AppOptions, appHeight int64, args valArgs) error { + // Clear addrbook to prevent peers from the original network sending votes + // that reference validator indices not present in our single-validator set. + addrBookPath := filepath.Join(args.homeDir, "config", "addrbook.json") + if err := os.WriteFile(addrBookPath, []byte("{}"), 0o600); err != nil { + logger.Error("Failed to clear addrbook.json", "err", err) + } else { + logger.Info("Cleared addrbook.json to isolate node from original network peers") + } + + // create validator set from the local validator + newTmVal := tmtypes.NewValidator(tmd25519.PubKey(args.validatorConsPubKeyByte), valVotingPower) + logger.Info("Creating new validator:", "validator", newTmVal) + + // CHANGE STATE CONSENSUS STORE + stateDB, err := openDB(args.homeDir, "state", cometdbm.BackendType(server.GetAppDBBackend(appOpts))) + if err != nil { + return err + } + + stateStore := sm.NewStore(stateDB, sm.StoreOptions{ + DiscardABCIResponses: false, + }) + + // load state in order to change validator set info + state, err := stateStore.Load() + if err != nil { + return err + } + defer func() { + if derr := stateStore.Close(); derr != nil { + logger.Error("Failed to close consensus state db", "err", derr) + err = derr + } + }() + + // last voting data must be updated because the distribution module will allocate tokens based on the last saved votes, + // and the voting validator address has to be present in the staking module, which is not the case for old validator + valInfo, err := loadValidatorsInfo(stateDB, state.LastBlockHeight) + if err != nil { + return err + } + + // CHANGE BLOCK CONSENSUS STORE + // we need to change the last commit data by updating the signature's info. Consensus will match the validator's set length + // and size of the lastCommit signatures when building the last commit info and they have to match + blockStoreDB, err := openDB(args.homeDir, "blockstore", cometdbm.BackendType(server.GetAppDBBackend(appOpts))) + if err != nil { + return err + } + defer func() { + if derr := blockStoreDB.Close(); derr != nil { + logger.Error("Failed to close consensus blockstore db", "err", derr) + err = derr + } + }() + + blockStore := store.NewBlockStore(blockStoreDB) + lastCommit := blockStore.LoadSeenCommit(state.LastBlockHeight) + var vote *tmtypes.Vote + currentValidators := state.Validators.Copy() + if args.replaceValidator { + // Replace the target validator + replaced := false + for i, v := range currentValidators.Validators { + if v.Address.String() == args.replacedConsensusAddress { // oldAddress = old consensus address + currentValidators.Validators[i] = newTmVal // newTmVal = new tmtypes.Validator with new pubkey + replaced = true + break + } + } + if !replaced { + return fmt.Errorf("validator to replace not found in consensus set") + } + currentValidators.Proposer = newTmVal + + var sigIndex int + for idx, commitSig := range lastCommit.Signatures { + if commitSig.BlockIDFlag == tmtypes.BlockIDFlagAbsent { + continue + } + validatorAddress := commitSig.ValidatorAddress + // logger.Info("Commit signature", "idx", idx, "address", fmt.Sprintf("%X", validatorAddress), "commitSig", commitSig) + if validatorAddress.String() == args.replacedConsensusAddress { + logger.Info("Found validator to replace", "idx", idx, "consensus_address", fmt.Sprintf("%X", validatorAddress)) + vote = lastCommit.GetVote(int32(idx)) + sigIndex = idx + break + } + } + if vote == nil { + return errors.New("cannot get the vote from the last commit") + } + + voteSignBytes := tmtypes.VoteSignBytes(state.ChainID, vote.ToProto()) + signatureBytes, err := args.validatorConsPrivKey.Sign(voteSignBytes) + if err != nil { + return err + } + + lastCommit.Signatures[sigIndex] = tmtypes.CommitSig{ + BlockIDFlag: tmtypes.BlockIDFlagCommit, + ValidatorAddress: newTmVal.Address, + Timestamp: vote.Timestamp, + Signature: []byte(signatureBytes), + } + } else { + // Clear the validator set + vals := []*tmtypes.Validator{newTmVal} + currentValidators = tmtypes.NewValidatorSet(vals) + + for idx, commitSig := range lastCommit.Signatures { + if commitSig.BlockIDFlag == tmtypes.BlockIDFlagAbsent { + continue + } + vote = lastCommit.GetVote(int32(idx)) + break + } + if vote == nil { + return errors.New("cannot get the vote from the last commit") + } + + voteSignBytes := tmtypes.VoteSignBytes(state.ChainID, vote.ToProto()) + signatureBytes, err := args.validatorConsPrivKey.Sign(voteSignBytes) + if err != nil { + return err + } + + lastCommit.Signatures = []tmtypes.CommitSig{{ + BlockIDFlag: tmtypes.BlockIDFlagCommit, + ValidatorAddress: newTmVal.Address, + Timestamp: vote.Timestamp, + Signature: []byte(signatureBytes), + }} + } + + // Update the state with the new validator set. + // LastHeightValidatorsChanged must be set to LastBlockHeight so that + // CometBFT's LoadValidators always resolves back to this height (where we + // store the full 1-validator set) instead of following the old pointer to + // the original 25-validator set. + state.LastValidators = currentValidators + state.Validators = currentValidators + state.NextValidators = currentValidators + state.LastHeightValidatorsChanged = state.LastBlockHeight + // save state store + if err = stateStore.Save(state); err != nil { + return err + } + + protoValSet, err := currentValidators.ToProto() + if err != nil { + return err + } + valInfo.ValidatorSet = protoValSet + valInfo.LastHeightChanged = state.LastBlockHeight + + // when the storeState is saved in consensus it is done for the nextBlock+1, + // that is why we need to update 2 future blocks + saveValidatorsInfo(stateDB, state.LastBlockHeight, valInfo) + saveValidatorsInfo(stateDB, state.LastBlockHeight+1, valInfo) + saveValidatorsInfo(stateDB, state.LastBlockHeight+2, valInfo) + + // if store height is greater than app height and state height, we will remove the last block from the store to avoid + // replaying this block to the app. If only the state height is lower, we do not delete the block from the store because + // block would not be replayed to the app (the mock app will be used by consensus instead) and only consensus state + // will be updated when the node is run. If all three versions are equal, everything is ok. The only scenario from + // which we cannot recover is the one when app height is lower that the version written in the app's iavl stores, because + // the hash of the next block would not be the same as the written hash and we do not have an exported function to delete + // that greater version from the app iavl store + blockStoreState := store.LoadBlockStoreState(blockStoreDB) + if blockStoreState.Height > state.LastBlockHeight && blockStoreState.Height > appHeight { + blockStore.DeleteLatestBlock() + } + + return blockStore.SaveSeenCommit(state.LastBlockHeight, lastCommit) +} + +func loadValidatorsInfo(db cometdbm.DB, height int64) (*cmtstate.ValidatorsInfo, error) { + buf, err := db.Get(calcValidatorsKey(height)) + if err != nil { + return nil, err + } + + if len(buf) == 0 { + return nil, errors.New("validator info retrieved from db is empty") + } + + v := new(cmtstate.ValidatorsInfo) + err = v.Unmarshal(buf) + + return v, err +} + +func saveValidatorsInfo(db cometdbm.DB, height int64, valInfo *cmtstate.ValidatorsInfo) error { + bz, err := valInfo.Marshal() + if err != nil { + return err + } + + err = db.Set(calcValidatorsKey(height), bz) + if err != nil { + return err + } + + return nil +} + +func calcValidatorsKey(height int64) []byte { + return []byte(fmt.Sprintf("validatorsKey:%v", height)) +} + +func openDB(rootDir, dbName string, backendType cometdbm.BackendType) (cometdbm.DB, error) { + dataDir := filepath.Join(rootDir, "data") + return cometdbm.NewDB(dbName, backendType, dataDir) +} diff --git a/cmd/exrpd/cmd/root.go b/cmd/exrpd/cmd/root.go index defd01c..49eb819 100644 --- a/cmd/exrpd/cmd/root.go +++ b/cmd/exrpd/cmd/root.go @@ -198,6 +198,7 @@ func initRootCmd( pruning.Cmd(sdkAppCreatorWrapper, defaultNodeHome), confixcmd.ConfigCommand(), snapshot.Cmd(sdkAppCreatorWrapper), + testnetUnsafeStartLocalValidatorCmd(a), ) // add server commands From 2f351f93723ab6ce2980bfa5f83ba354cce4c992 Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Wed, 25 Feb 2026 10:51:46 +0100 Subject: [PATCH 5/7] add test-local-upgrade script and optimize testnet account funding logic --- cmd/exrpd/cmd/in-place-testnet.go | 19 ++++--- scripts/test-local-upgrade.sh | 85 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 10 deletions(-) create mode 100755 scripts/test-local-upgrade.sh diff --git a/cmd/exrpd/cmd/in-place-testnet.go b/cmd/exrpd/cmd/in-place-testnet.go index d816a0c..6b00fed 100644 --- a/cmd/exrpd/cmd/in-place-testnet.go +++ b/cmd/exrpd/cmd/in-place-testnet.go @@ -353,22 +353,21 @@ func updateApplicationState(app *xrplevm.App, args valArgs) error { if err != nil { return err } + amount, _ := math.NewIntFromString("10000000000000000000000") // Fund accounts with both the bond denom and axrp (needed for gov deposits and gas) defaultCoins := sdk.NewCoins( - sdk.NewInt64Coin(xrplevm.BaseDenom, 1000000000000000000), + sdk.NewCoin(xrplevm.BaseDenom, amount), ) // Fund testnet accounts for _, account := range args.accountsToFund { - for i := 0; i < 50; i++ { - err := app.BankKeeper.MintCoins(appCtx, evmtypes.ModuleName, defaultCoins) - if err != nil { - return err - } - err = app.BankKeeper.SendCoinsFromModuleToAccount(appCtx, evmtypes.ModuleName, account, defaultCoins) - if err != nil { - return err - } + err := app.BankKeeper.MintCoins(appCtx, evmtypes.ModuleName, defaultCoins) + if err != nil { + return err + } + err = app.BankKeeper.SendCoinsFromModuleToAccount(appCtx, evmtypes.ModuleName, account, defaultCoins) + if err != nil { + return err } } diff --git a/scripts/test-local-upgrade.sh b/scripts/test-local-upgrade.sh new file mode 100755 index 0000000..6dd38f0 --- /dev/null +++ b/scripts/test-local-upgrade.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +MNEMONIC="birth rebuild refuse area aisle language bullet pride place clutch paddle drama" + +# ========== Helper: Read named parameters ========== +for arg in "$@"; do + case $arg in + --chainId=*) CHAIN_ID="${arg#*=}" ;; + --nodeHome=*) NODE_HOME="${arg#*=}" ;; + --snapshotPath=*) SNAPSHOT_PATH="${arg#*=}" ;; + --genesisUrl=*) GENESIS_URL="${arg#*=}" ;; + --version=*) VERSION="${arg#*=}" ;; + *) echo "⚠️ Unknown argument: $arg" ;; + esac +done + +# ========== Fallback to env vars if not passed ========== +: "${CHAIN_ID:=${CHAIN_ID_ENV:-}}" +: "${NODE_HOME:=${NODE_HOME_ENV:-}}" +: "${SNAPSHOT_PATH:=${SNAPSHOT_PATH_ENV:-}}" +: "${GENESIS_URL:=${GENESIS_URL_ENV:-}}" +: "${VERSION:=${VERSION_ENV:-}}" + +# ========== Validate dependencies ========== +echo "ℹ️ Checking required tools..." +for cmd in exrpd jq lz4; do + if ! command -v "$cmd" &> /dev/null; then + echo "❌ Error: Required tool '$cmd' not found. Please install it before running this script." + exit 1 + fi +done +echo "✅ All tools found" + +# ========== Validate required variables ========== +missing_vars=() +for var in CHAIN_ID NODE_HOME SNAPSHOT_PATH GENESIS_URL VERSION; do + if [ -z "${!var}" ]; then + missing_vars+=("$var") + fi +done + +if [ ${#missing_vars[@]} -ne 0 ]; then + echo "❌ Error: The following required variables are missing:" + for var in "${missing_vars[@]}"; do echo " - $var"; done + echo + echo "👉 You can provide them as parameters or environment variables." + echo + echo "📌 Example usage:" + echo "./test-upgrade.sh \\" + echo " --chainId=\"xrplevm_1440000-1\" \\" + echo " --nodeHome=\"./.exrpd\" \\" + echo " --snapshotPath=\"./exrpd.tar.lz4\" \\" + echo " --genesisUrl=\"https://raw.githubusercontent.com/xrplevm/networks/refs/heads/main/testnet/genesis.json\" \\" + echo " --version=\"9.0.0\" \\" + echo + echo "Or set them as environment variables before running:" + echo "export CHAIN_ID=... && ./test-upgrade.sh" + exit 1 +fi + +echo "ℹ️ Initializing node home in $NODE_HOME..." +exrpd config set client chain-id "$CHAIN_ID" --home "$NODE_HOME" --chain-id "$CHAIN_ID" +exrpd config set client keyring-backend test --home "$NODE_HOME" +echo "$MNEMONIC" | exrpd --home "$NODE_HOME" keys add key --recover --keyring-backend test --algo eth_secp256k1 +exrpd init localnode --chain-id "$CHAIN_ID" --home "$NODE_HOME" > /dev/null 2>&1 +echo "✅ Node home initialized " + +echo "ℹ️ Downloading genesis..." +wget -O $NODE_HOME/config/genesis.json $GENESIS_URL +echo "✅ Genesis downloaded " + +echo "ℹ️ Restoring snapshot..." +lz4 -c -d $SNAPSHOT_PATH | tar -x -C $NODE_HOME +echo "✅ Snapshot restored " + +echo "ℹ️ Starting the local validator..." + +exrpd unsafe-start-local-validator \ + --home "$NODE_HOME" \ + --validator-operator="$(exrpd --home "$NODE_HOME" keys show key --keyring-backend test --bech val -a)" \ + --validator-pubkey="$(jq -r '.pub_key.value' "$NODE_HOME/config/priv_validator_key.json")" \ + --validator-privkey="$(jq -r '.priv_key.value' "$NODE_HOME/config/priv_validator_key.json")" \ + --accounts-to-fund="$(exrpd --home "$NODE_HOME" keys show key --keyring-backend test --bech acc -a),ethm1zrxl239wa6ad5xge3gs68rt98227xgnjq0xyw2" \ + --xrp-owner-address="$(exrpd --home "$NODE_HOME" keys show key --keyring-backend test --bech acc -a)" \ + --upgrade-version="$VERSION" From 6e9a564b76c47768461225f8f8de3fe50ba70fa2 Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Wed, 25 Feb 2026 10:53:46 +0100 Subject: [PATCH 6/7] fix: remove unused branch from workflow triggers --- .github/workflows/branch.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 82d6e9e..9001f5b 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -4,7 +4,6 @@ on: push: branches: - "main" - - "fix-check-tx-issue" concurrency: # Cancel old runs if there is a new commit in the same branch From 66fae6ec1cd7c38125910484fe840084cb74a060 Mon Sep 17 00:00:00 2001 From: AdriaCarrera Date: Thu, 26 Feb 2026 14:33:17 +0100 Subject: [PATCH 7/7] fix: lint --- cmd/exrpd/cmd/in-place-testnet.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/cmd/exrpd/cmd/in-place-testnet.go b/cmd/exrpd/cmd/in-place-testnet.go index 6b00fed..b38c40e 100644 --- a/cmd/exrpd/cmd/in-place-testnet.go +++ b/cmd/exrpd/cmd/in-place-testnet.go @@ -123,7 +123,7 @@ func getCommandArgs(appOpts servertypes.AppOptions) (valArgs, error) { if err != nil { return args, fmt.Errorf("cannot decode validator pubkey %w", err) } - args.validatorConsPubKeyByte = []byte(decPubKey) + args.validatorConsPubKeyByte = decPubKey // validate and set validator privkey validatorPrivKey := cast.ToString(appOpts.Get(flagValidatorPrivKey)) @@ -134,7 +134,7 @@ func getCommandArgs(appOpts servertypes.AppOptions) (valArgs, error) { if err != nil { return args, fmt.Errorf("cannot decode validator private key %w", err) } - args.validatorConsPrivKey = tmd25519.PrivKey([]byte(decPrivKey)) + args.validatorConsPrivKey = tmd25519.PrivKey(decPrivKey) // validate and set accounts to fund accountsString := cast.ToString(appOpts.Get(flagAccountsToFund)) @@ -290,6 +290,7 @@ func updateApplicationState(app *xrplevm.App, args valArgs) error { store.Delete(stakingtypes.GetValidatorsByPowerIndexKey(v, app.StakingKeeper.PowerReduction(appCtx), app.StakingKeeper.ValidatorAddressCodec())) store.Delete(stakingtypes.GetLastValidatorPowerKey(valAddr)) if v.IsUnbonding() { + //nolint:errcheck app.StakingKeeper.DeleteValidatorQueueTimeSlice(appCtx, v.UnbondingTime, v.UnbondingHeight) } if args.replaceValidator { @@ -304,21 +305,28 @@ func updateApplicationState(app *xrplevm.App, args valArgs) error { } // Add our validator to power and last validators store + //nolint:errcheck app.StakingKeeper.SetValidator(appCtx, newVal) err = app.StakingKeeper.SetValidatorByConsAddr(appCtx, newVal) if err != nil { return err } + //nolint:errcheck app.StakingKeeper.SetValidatorByPowerIndex(appCtx, newVal) + //nolint:errcheck app.StakingKeeper.SetLastValidatorPower(appCtx, newValAddr, valVotingPower) if err := app.StakingKeeper.Hooks().AfterValidatorCreated(appCtx, newValAddr); err != nil { return err } // DISTRIBUTION // Initialize records for this validator across all distribution stores + //nolint:errcheck app.DistrKeeper.SetValidatorHistoricalRewards(appCtx, newValAddr, 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1)) + //nolint:errcheck app.DistrKeeper.SetValidatorCurrentRewards(appCtx, newValAddr, distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1)) + //nolint:errcheck app.DistrKeeper.SetValidatorAccumulatedCommission(appCtx, newValAddr, distrtypes.InitialValidatorAccumulatedCommission()) + //nolint:errcheck app.DistrKeeper.SetValidatorOutstandingRewards(appCtx, newValAddr, distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}) // SLASHING @@ -329,7 +337,7 @@ func updateApplicationState(app *xrplevm.App, args valArgs) error { StartHeight: app.LastBlockHeight() - 1, Tombstoned: false, } - + //nolint:errcheck app.SlashingKeeper.SetValidatorSigningInfo(appCtx, newConsAddr, newValidatorSigningInfo) // GOVERNANCE @@ -350,9 +358,6 @@ func updateApplicationState(app *xrplevm.App, args valArgs) error { appCtx.Logger().Info("Updated governance params", "voting_period", shortVotingPeriod, "expedited_voting_period", expeditedVotingPeriod, "min_deposit", params.MinDeposit) // BANK - if err != nil { - return err - } amount, _ := math.NewIntFromString("10000000000000000000000") // Fund accounts with both the bond denom and axrp (needed for gov deposits and gas) defaultCoins := sdk.NewCoins( @@ -486,6 +491,7 @@ func updateConsensusState(logger log.Logger, appOpts servertypes.AppOptions, app // logger.Info("Commit signature", "idx", idx, "address", fmt.Sprintf("%X", validatorAddress), "commitSig", commitSig) if validatorAddress.String() == args.replacedConsensusAddress { logger.Info("Found validator to replace", "idx", idx, "consensus_address", fmt.Sprintf("%X", validatorAddress)) + //nolint:gosec vote = lastCommit.GetVote(int32(idx)) sigIndex = idx break @@ -505,7 +511,7 @@ func updateConsensusState(logger log.Logger, appOpts servertypes.AppOptions, app BlockIDFlag: tmtypes.BlockIDFlagCommit, ValidatorAddress: newTmVal.Address, Timestamp: vote.Timestamp, - Signature: []byte(signatureBytes), + Signature: signatureBytes, } } else { // Clear the validator set @@ -516,6 +522,7 @@ func updateConsensusState(logger log.Logger, appOpts servertypes.AppOptions, app if commitSig.BlockIDFlag == tmtypes.BlockIDFlagAbsent { continue } + //nolint:gosec vote = lastCommit.GetVote(int32(idx)) break } @@ -533,7 +540,7 @@ func updateConsensusState(logger log.Logger, appOpts servertypes.AppOptions, app BlockIDFlag: tmtypes.BlockIDFlagCommit, ValidatorAddress: newTmVal.Address, Timestamp: vote.Timestamp, - Signature: []byte(signatureBytes), + Signature: signatureBytes, }} } @@ -560,8 +567,11 @@ func updateConsensusState(logger log.Logger, appOpts servertypes.AppOptions, app // when the storeState is saved in consensus it is done for the nextBlock+1, // that is why we need to update 2 future blocks + //nolint:errcheck saveValidatorsInfo(stateDB, state.LastBlockHeight, valInfo) + //nolint:errcheck saveValidatorsInfo(stateDB, state.LastBlockHeight+1, valInfo) + //nolint:errcheck saveValidatorsInfo(stateDB, state.LastBlockHeight+2, valInfo) // if store height is greater than app height and state height, we will remove the last block from the store to avoid @@ -573,6 +583,7 @@ func updateConsensusState(logger log.Logger, appOpts servertypes.AppOptions, app // that greater version from the app iavl store blockStoreState := store.LoadBlockStoreState(blockStoreDB) if blockStoreState.Height > state.LastBlockHeight && blockStoreState.Height > appHeight { + //nolint:errcheck blockStore.DeleteLatestBlock() }