Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tools/cosmovisor): cosmovisor batch upgrades #21790

Merged
merged 30 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c347f6c
wip: batch upgrade
psiphi5 Sep 10, 2024
cd54cb2
add logic for batch upgrade cmd; logic for swapping upgrade files
psiphi5 Sep 18, 2024
e38739d
fmt
psiphi5 Sep 18, 2024
322ff72
made some fixes; command now runs successfuly on my machine
psiphi5 Sep 18, 2024
b30a8d1
lint
psiphi5 Sep 18, 2024
980201d
lint
psiphi5 Sep 18, 2024
bc0651d
go mod tidy
psiphi5 Sep 18, 2024
68bc6f9
godoc; lint; changelog
psiphi5 Sep 19, 2024
34bb0c8
ty coderabbitai
psiphi5 Sep 19, 2024
071c677
cometbftrpc default value
psiphi5 Sep 19, 2024
054d9ce
fix test; better naming
psiphi5 Sep 20, 2024
a7e6459
switch from cometbft websocket to cosmos grpc
psiphi5 Sep 27, 2024
3f3c787
cpu opti
psiphi5 Sep 28, 2024
d4bdb57
ty coderabbitai
psiphi5 Sep 30, 2024
4ae231e
improve ux; nit
psiphi5 Oct 1, 2024
bc9fb9b
ty coderabbitai
psiphi5 Oct 1, 2024
96dd220
minute things
psiphi5 Oct 1, 2024
7f4309e
ty coderabbitai
psiphi5 Oct 1, 2024
13be656
gracefully handle corrupted batch files
psiphi5 Oct 1, 2024
88da1ba
merge main
psiphi5 Oct 2, 2024
1d21f55
Merge branch 'main' into issue-20630
psiphi5 Oct 2, 2024
250eae9
switch to using csv file
psiphi5 Oct 2, 2024
7e8112d
mark flags with mutex
psiphi5 Oct 2, 2024
57bf0a0
nit
psiphi5 Oct 2, 2024
ebc78ba
Merge branch 'main' into issue-20630
psiphi5 Oct 2, 2024
f91bfc3
fix tests
psiphi5 Oct 2, 2024
28d03fe
Merge branch 'main' into issue-20630
psiphi5 Oct 3, 2024
4c12e9b
Merge branch 'main' into issue-20630
julienrbrt Oct 9, 2024
4f31076
fix merge
psiphi5 Oct 10, 2024
33be971
Merge branch 'main' into issue-20630
psiphi5 Oct 10, 2024
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
1 change: 1 addition & 0 deletions tools/cosmovisor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* [#21790](https://github.com/cosmos/cosmos-sdk/pull/21790) Add `add-batch-upgrade` command.
* [#21972](https://github.com/cosmos/cosmos-sdk/pull/21972) Add `prepare-upgrade` command

### Improvements
Expand Down
5 changes: 5 additions & 0 deletions tools/cosmovisor/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ func (cfg *Config) UpgradeInfoFilePath() string {
return filepath.Join(cfg.Home, "data", upgradetypes.UpgradeInfoFilename)
}

// UpgradeInfoBatchFilePath is the same as UpgradeInfoFilePath but with a batch suffix.
func (cfg *Config) UpgradeInfoBatchFilePath() string {
return cfg.UpgradeInfoFilePath() + ".batch"
}

// SymLinkToGenesis creates a symbolic link from "./current" to the genesis directory.
func (cfg *Config) SymLinkToGenesis() (string, error) {
genesis := filepath.Join(cfg.Root(), genesisDir)
Expand Down
69 changes: 43 additions & 26 deletions tools/cosmovisor/cmd/cosmovisor/add_upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func NewAddUpgradeCmd() *cobra.Command {
Short: "Add APP upgrade binary to cosmovisor",
SilenceUsage: true,
Args: cobra.ExactArgs(2),
RunE: AddUpgrade,
RunE: addUpgradeCmd,
}

addUpgrade.Flags().Bool(cosmovisor.FlagForce, false, "overwrite existing upgrade binary / upgrade-info.json file")
Expand All @@ -28,26 +28,14 @@ func NewAddUpgradeCmd() *cobra.Command {
return addUpgrade
}

// AddUpgrade adds upgrade info to manifest
func AddUpgrade(cmd *cobra.Command, args []string) error {
configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig)
if err != nil {
return fmt.Errorf("failed to get config flag: %w", err)
}

cfg, err := cosmovisor.GetConfigFromFile(configPath)
if err != nil {
return err
}

// addUpgrade adds upgrade info to manifest
func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath, upgradeInfoPath string) error {
psiphi5 marked this conversation as resolved.
Show resolved Hide resolved
logger := cfg.Logger(os.Stdout)

upgradeName := args[0]
if !cfg.DisableRecase {
upgradeName = strings.ToLower(args[0])
upgradeName = strings.ToLower(upgradeName)
}

executablePath := args[1]
if _, err := os.Stat(executablePath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("invalid executable path: %w", err)
Expand All @@ -68,21 +56,14 @@ func AddUpgrade(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to read binary: %w", err)
}

force, err := cmd.Flags().GetBool(cosmovisor.FlagForce)
if err != nil {
return fmt.Errorf("failed to get force flag: %w", err)
}

if err := saveOrAbort(cfg.UpgradeBin(upgradeName), executableData, force); err != nil {
return err
}

logger.Info(fmt.Sprintf("Using %s for %s upgrade", executablePath, upgradeName))
logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName)))

if upgradeHeight, err := cmd.Flags().GetInt64(cosmovisor.FlagUpgradeHeight); err != nil {
return fmt.Errorf("failed to get upgrade-height flag: %w", err)
} else if upgradeHeight > 0 {
if upgradeHeight > 0 {
plan := upgradetypes.Plan{Name: upgradeName, Height: upgradeHeight}
if err := plan.ValidateBasic(); err != nil {
panic(fmt.Errorf("something is wrong with cosmovisor: %w", err))
Expand All @@ -94,16 +75,52 @@ func AddUpgrade(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to marshal upgrade plan: %w", err)
}

if err := saveOrAbort(cfg.UpgradeInfoFilePath(), planData, force); err != nil {
if err := saveOrAbort(upgradeInfoPath, planData, force); err != nil {
psiphi5 marked this conversation as resolved.
Show resolved Hide resolved
return err
}

logger.Info(fmt.Sprintf("%s created, %s upgrade binary will switch at height %d", cfg.UpgradeInfoFilePath(), upgradeName, upgradeHeight))
logger.Info(fmt.Sprintf("%s created, %s upgrade binary will switch at height %d", upgradeInfoPath, upgradeName, upgradeHeight))
}

return nil
}

// GetConfig returns a Config using passed-in flag
func getConfigFromCmd(cmd *cobra.Command) (*cosmovisor.Config, error) {
configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig)
if err != nil {
return nil, fmt.Errorf("failed to get config flag: %w", err)
}

cfg, err := cosmovisor.GetConfigFromFile(configPath)
if err != nil {
return nil, err
}
return cfg, nil
}

// addUpgradeCmd parses input flags and adds upgrade info to manifest
func addUpgradeCmd(cmd *cobra.Command, args []string) error {
cfg, err := getConfigFromCmd(cmd)
if err != nil {
return err
}

upgradeName, executablePath := args[0], args[1]

force, err := cmd.Flags().GetBool(cosmovisor.FlagForce)
if err != nil {
return fmt.Errorf("failed to get force flag: %w", err)
}

upgradeHeight, err := cmd.Flags().GetInt64(cosmovisor.FlagUpgradeHeight)
if err != nil {
return fmt.Errorf("failed to get upgrade-height flag: %w", err)
}

return addUpgrade(cfg, force, upgradeHeight, upgradeName, executablePath, cfg.UpgradeInfoFilePath())
psiphi5 marked this conversation as resolved.
Show resolved Hide resolved
}

// saveOrAbort saves data to path or aborts if file exists and force is false
func saveOrAbort(path string, data []byte, force bool) error {
if _, err := os.Stat(path); err == nil {
Expand Down
143 changes: 143 additions & 0 deletions tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package main

import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/spf13/cobra"

"cosmossdk.io/tools/cosmovisor"
)

func NewBatchAddUpgradeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add-batch-upgrade [flags]",
Short: "Add multiple upgrade binaries at specified heights to cosmovisor",
Long: `This command allows you to specify multiple upgrades at once at specific heights, copying or creating a batch upgrade file that's actively watched during 'cosmovisor run'.
You can provide upgrades in two ways:

1. Using --upgrade-file: Specify a path to a headerless CSV batch upgrade file in the format:
upgrade-name,path-to-exec,upgrade-height

2. Using --upgrade-list: Provide a comma-separated list of upgrades.
Each upgrade is defined by three colon-separated values:
a. upgrade-name: A unique identifier for the upgrade
b. path-to-exec: The file path to the upgrade's executable binary
c. upgrade-height: The block height at which the upgrade should occur
This creates a batch upgrade JSON file with the upgrade-info objects in the upgrade directory.

Note: You must provide either --upgrade-file or --upgrade-list.`,
Example: `cosmovisor add-batch-upgrade --upgrade-list upgrade_v2:/path/to/v2/binary:1000000,upgrade_v3:/path/to/v3/binary:2000000

cosmovisor add-batch-upgrade --upgrade-file /path/to/batch_upgrade.json`,
psiphi5 marked this conversation as resolved.
Show resolved Hide resolved
SilenceUsage: true,
Args: cobra.NoArgs,
RunE: addBatchUpgrade,
}

cmd.Flags().String("upgrade-file", "", "Path to a batch upgrade file which is a JSON array of upgrade-info objects")
cmd.Flags().StringSlice("upgrade-list", []string{}, "List of comma-separated upgrades in the format 'name:path/to/binary:height'")
cmd.MarkFlagsMutuallyExclusive("upgrade-file", "upgrade-list")

return cmd
}

// addBatchUpgrade takes in multiple specified upgrades and creates a single
// batch upgrade file out of them
func addBatchUpgrade(cmd *cobra.Command, args []string) error {
cfg, err := getConfigFromCmd(cmd)
if err != nil {
return err
}
upgradeFile, err := cmd.Flags().GetString("upgrade-file")
if err == nil && upgradeFile != "" {
return processUpgradeFile(cfg, upgradeFile)
}
upgradeList, err := cmd.Flags().GetStringSlice("upgrade-list")
if err != nil || len(upgradeList) == 0 {
return fmt.Errorf("either --upgrade-file or --upgrade-list must be provided")
}
var splitUpgrades [][]string
for _, upgrade := range upgradeList {
splitUpgrades = append(splitUpgrades, strings.Split(upgrade, ":"))
}
return processUpgradeList(cfg, splitUpgrades)
}
psiphi5 marked this conversation as resolved.
Show resolved Hide resolved

// processUpgradeList takes in a list of upgrades and creates a batch upgrade file
func processUpgradeList(cfg *cosmovisor.Config, upgradeList [][]string) error {
upgradeInfoPaths := []string{}
for i, upgrade := range upgradeList {
if len(upgrade) != 3 {
return fmt.Errorf("argument at position %d (%s) is invalid", i, upgrade)
}
upgradeName := filepath.Base(upgrade[0])
upgradePath := upgrade[1]
upgradeHeight, err := strconv.ParseInt(upgrade[2], 10, 64)
if err != nil {
return fmt.Errorf("upgrade height at position %d (%s) is invalid", i, upgrade[2])
}
upgradeInfoPath := cfg.UpgradeInfoFilePath() + "." + upgradeName
upgradeInfoPaths = append(upgradeInfoPaths, upgradeInfoPath)
if err := addUpgrade(cfg, true, upgradeHeight, upgradeName, upgradePath, upgradeInfoPath); err != nil {
return err
}
}
psiphi5 marked this conversation as resolved.
Show resolved Hide resolved

var allData []json.RawMessage
for _, uip := range upgradeInfoPaths {
fileData, err := os.ReadFile(uip)
if err != nil {
return fmt.Errorf("error reading file %s: %w", uip, err)
}

// Verify it's valid JSON
var jsonData json.RawMessage
if err := json.Unmarshal(fileData, &jsonData); err != nil {
return fmt.Errorf("error parsing JSON from file %s: %w", uip, err)
}

// Add to our slice
allData = append(allData, jsonData)
}

// Marshal the combined data
batchData, err := json.MarshalIndent(allData, "", " ")
if err != nil {
return fmt.Errorf("error marshaling combined JSON: %w", err)
}

// Write to output file
err = os.WriteFile(cfg.UpgradeInfoBatchFilePath(), batchData, 0o600)
if err != nil {
return fmt.Errorf("error writing combined JSON to file: %w", err)
}

return nil
}
psiphi5 marked this conversation as resolved.
Show resolved Hide resolved

// processUpgradeFile takes in a CSV batch upgrade file, parses it and calls processUpgradeList
func processUpgradeFile(cfg *cosmovisor.Config, upgradeFile string) error {
file, err := os.Open(upgradeFile)
if err != nil {
return fmt.Errorf("error opening upgrade CSV file %s: %w", upgradeFile, err)
}
defer file.Close()

r := csv.NewReader(file)
r.FieldsPerRecord = 3
r.TrimLeadingSpace = true
records, err := r.ReadAll()
if err != nil {
return fmt.Errorf("error parsing upgrade CSV file %s: %w", upgradeFile, err)
}
if err := processUpgradeList(cfg, records); err != nil {
return err
}
return nil
}
1 change: 1 addition & 0 deletions tools/cosmovisor/cmd/cosmovisor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func NewRootCmd() *cobra.Command {
NewVersionCmd(),
NewAddUpgradeCmd(),
NewShowUpgradeInfoCmd(),
NewBatchAddUpgradeCmd(),
NewPrepareUpgradeCmd(),
)

Expand Down
4 changes: 2 additions & 2 deletions tools/cosmovisor/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ go 1.23
require (
cosmossdk.io/log v1.4.1
cosmossdk.io/x/upgrade v0.1.4
github.com/cosmos/cosmos-sdk v0.50.7
github.com/fsnotify/fsnotify v1.7.0
github.com/otiai10/copy v1.14.0
github.com/pelletier/go-toml/v2 v2.2.3
github.com/spf13/cobra v1.8.1
Expand Down Expand Up @@ -51,7 +53,6 @@ require (
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-db v1.0.2 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
github.com/cosmos/cosmos-sdk v0.50.7 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/gogoproto v1.7.0 // indirect
Expand All @@ -69,7 +70,6 @@ require (
github.com/emicklei/dot v1.6.2 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/getsentry/sentry-go v0.28.0 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
Expand Down
Loading
Loading