From 09fe843326cdfa9857a45e0e0ece4e790cff6ae3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 14 May 2025 12:28:43 -0400 Subject: [PATCH 001/115] fix(cosmovisor): make manual upgrades use halt-height --- tools/cosmovisor/args.go | 5 + .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 22 ++--- tools/cosmovisor/manual.go | 97 +++++++++++++++++++ 3 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 tools/cosmovisor/manual.go diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index f5418a5b2cbf..18a7f1098b44 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -112,6 +112,11 @@ func (cfg *Config) UpgradeInfoFilePath() string { return filepath.Join(cfg.Home, "data", upgradetypes.UpgradeInfoFilename) } +// UpgradeInfoFilePath is the expected upgrade-info filename created by `x/upgrade/keeper`. +func (cfg *Config) ManualUpgradesFilePath() string { + return filepath.Join(cfg.Home, "data", ManualUpgradesFilename) +} + // UpgradeInfoBatchFilePath is the same as UpgradeInfoFilePath but with a batch suffix. func (cfg *Config) UpgradeInfoBatchFilePath() string { return cfg.UpgradeInfoFilePath() + ".batch" diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 742b53ca5ca4..604bdabe8f84 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "os" "path" @@ -10,8 +9,6 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" - - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) func NewAddUpgradeCmd() *cobra.Command { @@ -65,22 +62,15 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) 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)) - } - - // create upgrade-info.json file - planData, err := json.Marshal(plan) - if err != nil { - return fmt.Errorf("failed to marshal upgrade plan: %w", err) + plan := cosmovisor.ManualUpgradePlan{ + Name: upgradeName, + Height: upgradeHeight, } - - if err := saveOrAbort(upgradeInfoPath, planData, force); err != nil { - return err + if err := plan.ValidateBasic(); err != nil { + panic(fmt.Errorf("invalid manual upgrade plan: %w", err)) } - logger.Info(fmt.Sprintf("%s created, %s upgrade binary will switch at height %d", upgradeInfoPath, upgradeName, upgradeHeight)) + logger.Info(fmt.Sprintf("added manual upgrade, node will be set to halt at height %d, and binary for upgrade %q will be activated", upgradeHeight, upgradeName)) } return nil diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go new file mode 100644 index 000000000000..08565b60898a --- /dev/null +++ b/tools/cosmovisor/manual.go @@ -0,0 +1,97 @@ +package cosmovisor + +import ( + "encoding/json" + "fmt" + "os" +) + +// ManualUpgradesFilename is the file to store manual upgrade information +const ManualUpgradesFilename = "manual-upgrades.json" + +// ManualUpgradePlan is the plan for a manual upgrade. +type ManualUpgradePlan struct { + // Name is the name of the upgrade. + Name string `json:"name"` + // Height is the block height that the node will halt at (by setting the --halt-height flag). + Height int64 `json:"height"` + // Info is any additional information about the upgrade. + Info string `json:"info"` +} + +func (p ManualUpgradePlan) ValidateBasic() error { + if len(p.Name) == 0 { + return fmt.Errorf("name cannot be empty") + } + + if p.Height <= 0 { + return fmt.Errorf("height must be greater than 0") + } + + return nil +} + +// ManualUpgradeData is the data structure for manual upgrades. +type ManualUpgradeData struct { + // Upgrades is a map of manual upgrade names to their plans. + Upgrades map[string]ManualUpgradePlan `json:"upgrades"` +} + +func (m ManualUpgradeData) NextPlanAfter(height int64) *ManualUpgradePlan { + if m.Upgrades == nil { + return nil + } + + // sort the upgrades by height + var nextPlan *ManualUpgradePlan + for _, plan := range m.Upgrades { + if plan.Height > height { + if nextPlan == nil || plan.Height < nextPlan.Height { + nextPlan = &plan + } + } + } + return nextPlan +} + +// ReadManualUpgrades reads the manual upgrade data. +func ReadManualUpgrades(cfg *Config) (ManualUpgradeData, error) { + bz, err := os.ReadFile(cfg.ManualUpgradesFilePath()) + if err != nil { + if os.IsNotExist(err) { + return ManualUpgradeData{}, nil + } + return ManualUpgradeData{}, err + } + manualUpgrades := ManualUpgradeData{} + if err := json.Unmarshal(bz, &manualUpgrades); err != nil { + return ManualUpgradeData{}, err + } + return manualUpgrades, nil +} + +// AddManualUpgrade adds a manual upgrade plan. +// If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, +// otherwise os.ErrExist will be returned. +func AddManualUpgrade(cfg *Config, plan ManualUpgradePlan, forceOverwrite bool) error { + manualUpgrades, err := ReadManualUpgrades(cfg) + if err != nil { + return err + } + + if manualUpgrades.Upgrades == nil { + manualUpgrades.Upgrades = make(map[string]ManualUpgradePlan) + } + + if _, exists := manualUpgrades.Upgrades[plan.Name]; exists && !forceOverwrite { + return os.ErrExist + } + + manualUpgrades.Upgrades[plan.Name] = plan + manualUpgradesData, err := json.MarshalIndent(manualUpgrades, "", " ") + if err != nil { + return err + } + + return os.WriteFile(cfg.ManualUpgradesFilePath(), manualUpgradesData, 0644) +} From 4087e2b75bdb9b151188810a10dae5578fa82332 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 15 May 2025 17:09:52 -0400 Subject: [PATCH 002/115] WIP on mock-node --- tools/cosmovisor/cmd/mock_node/main.go | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tools/cosmovisor/cmd/mock_node/main.go diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go new file mode 100644 index 000000000000..868db0aceca2 --- /dev/null +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/server" +) + +func main() { + cmd := &cobra.Command{ + Use: "mock_node", + Short: "A mock node for testing cosmovisor.", + Long: `The --halt-interval flag is required and must be specified in order to halt the node. +The --upgrade-plan and --halt-height flags are mutually exclusive. It is an error to specify both. +Based on which flag is specified the node will either exhibit --halt-height before or +x/upgrade upgrade-info.json behavior.`, + } + var haltInterval time.Duration + var upgradePlan string + var haltHeight uint64 + cmd.Flags().DurationVar(&haltInterval, "halt-interval", 0, "Interval to wait before halting the node") + cmd.Flags().StringVar(&upgradePlan, "upgrade-plan", "", "upgrade-info.json to create after the halt duration is reached. Either this flag or --halt-height must be specified but not both.") + cmd.Flags().Uint64Var(&haltHeight, server.FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node. E") + cmd.RunE = func(cmd *cobra.Command, args []string) error { + if upgradePlan != "" && haltHeight > 0 { + return fmt.Errorf("cannot specify both --upgrade-plan and --halt-height") + } + if upgradePlan == "" && haltHeight == 0 { + return fmt.Errorf("must specify either --upgrade-plan or --halt-height") + } + if haltInterval == 0 { + return fmt.Errorf("must specify --halt-interval") + } + time.Sleep(haltInterval) + if haltHeight > 0 { + panic(fmt.Errorf("halt per configuration height %d time %d", haltHeight, 0)) + } else if upgradePlan != "" { + panic("upgrade-info.json not implemented") + } + return nil + } + if err := cmd.Execute(); err != nil { + panic(err) + } +} From 56897954ebd5ecc162872bd40ca64e00fff37271 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 16 May 2025 15:15:29 -0400 Subject: [PATCH 003/115] WIP on watcher --- tools/cosmovisor/watcher.go | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tools/cosmovisor/watcher.go diff --git a/tools/cosmovisor/watcher.go b/tools/cosmovisor/watcher.go new file mode 100644 index 000000000000..ea5921e22fb8 --- /dev/null +++ b/tools/cosmovisor/watcher.go @@ -0,0 +1,71 @@ +package cosmovisor + +import ( + "encoding/json" + "time" +) + +type DataWatcher[T any] struct { + watcher FileWatcher + ReceivedUpdate chan T +} + +func NewDataWatcher[T any](watcher FileWatcher) *DataWatcher[T] { + ch := make(chan T) + go func() { + for { + select { + case contents := <-watcher.FileContentsUpdated(): + var data T + err := json.Unmarshal([]byte(contents), &data) + if err == nil { + ch <- data + } + } + } + }() + return &DataWatcher[T]{ + watcher: watcher, + } +} + +type FileWatcher interface { + FileContentsUpdated() <-chan string + Stop() +} + +type PollWatcher struct { +} + +var _ FileWatcher = (*PollWatcher)(nil) + +func NewPollWatcher(filename string, pollInterval time.Duration) *PollWatcher { + return &PollWatcher{} +} + +func (w *PollWatcher) FileContentsUpdated() <-chan string { + panic("not implemented") +} + +func (w *PollWatcher) Stop() { + //TODO implement me + panic("implement me") +} + +type FSNotifyWatcher struct { +} + +var _ FileWatcher = (*FSNotifyWatcher)(nil) + +func NewFSNotifyWatcher(filename string) *FSNotifyWatcher { + return &FSNotifyWatcher{} +} + +func (w *FSNotifyWatcher) FileContentsUpdated() <-chan string { + panic("not implemented") +} + +func (w *FSNotifyWatcher) Stop() { + //TODO implement me + panic("implement me") +} From c48952463387c5080c29be6df168269ffbdddaf4 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 21 May 2025 14:25:22 -0400 Subject: [PATCH 004/115] poll watcher tests pass --- tests/systemtests/upgrade_test.go | 2 +- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 4 + tools/cosmovisor/fsnotify_watcher.go | 92 +++++++++++++++++++ tools/cosmovisor/poll_watcher.go | 59 ++++++++++++ tools/cosmovisor/poll_watcher_test.go | 60 ++++++++++++ tools/cosmovisor/watcher.go | 51 ++-------- 6 files changed, 223 insertions(+), 45 deletions(-) create mode 100644 tools/cosmovisor/fsnotify_watcher.go create mode 100644 tools/cosmovisor/poll_watcher.go create mode 100644 tools/cosmovisor/poll_watcher_test.go diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index 59d47e271e41..88dd93a5d3ff 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -82,7 +82,7 @@ func TestChainUpgrade(t *testing.T) { votingPeriod := 5 * time.Second // enough time to vote systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) - systest.Sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) + systest.Sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight-1)) cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) govAddr := sdk.AccAddress(address.Module("gov")).String() diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 604bdabe8f84..929b5b54ea0a 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -70,6 +70,10 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade panic(fmt.Errorf("invalid manual upgrade plan: %w", err)) } + if err := cosmovisor.AddManualUpgrade(cfg, plan, force); err != nil { + panic(fmt.Errorf("failed to add manual upgrade: %w", err)) + } + logger.Info(fmt.Sprintf("added manual upgrade, node will be set to halt at height %d, and binary for upgrade %q will be activated", upgradeHeight, upgradeName)) } diff --git a/tools/cosmovisor/fsnotify_watcher.go b/tools/cosmovisor/fsnotify_watcher.go new file mode 100644 index 000000000000..eb06355ca3d0 --- /dev/null +++ b/tools/cosmovisor/fsnotify_watcher.go @@ -0,0 +1,92 @@ +package cosmovisor + +import ( + "context" + "fmt" + "os" + + "github.com/fsnotify/fsnotify" +) + +type FSNotifyWatcher struct { + watcher *fsnotify.Watcher + outChan chan FileUpdate + errChan chan error +} + +var _ Watcher[FileUpdate] = (*FSNotifyWatcher)(nil) + +type FileUpdate struct { + Filename string + Contents []byte +} + +func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*FSNotifyWatcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + err = watcher.Add(dir) + if err != nil { + return nil, fmt.Errorf("failed to watch directory %s: %w", dir, err) + } + + // TODO check that filenames are in dir & fully qualified + filenameSet := make(map[string]struct{}) + for _, filename := range filenames { + filenameSet[filename] = struct{}{} + } + + outChan := make(chan FileUpdate, 1) + errChan := make(chan error, 1) + go func() { + // close the watcher and channels + // when the goroutines exits via return's + defer watcher.Close() + defer close(outChan) + defer close(errChan) + + for { + select { + case <-ctx.Done(): + return + case event, ok := <-watcher.Events: + if !ok { // channel closed + return + } + if event.Has(fsnotify.Write) { + if _, ok := filenameSet[event.Name]; !ok { + continue + } + filename := event.Name + bz, err := os.ReadFile(filename) + if err != nil { + errChan <- fmt.Errorf("failed to read file %s: %w", filename, err) + } else { + outChan <- FileUpdate{Filename: filename, Contents: bz} + } + } + case err, ok := <-watcher.Errors: + if !ok { // channel closed + return + } + errChan <- fmt.Errorf("fsnotify error: %w", err) + } + } + }() + + return &FSNotifyWatcher{ + watcher: watcher, + outChan: outChan, + errChan: errChan, + }, nil +} + +func (w *FSNotifyWatcher) Updated() <-chan FileUpdate { + return w.outChan +} + +func (w *FSNotifyWatcher) Errors() <-chan error { + return w.errChan +} diff --git a/tools/cosmovisor/poll_watcher.go b/tools/cosmovisor/poll_watcher.go new file mode 100644 index 000000000000..4fa829ad951a --- /dev/null +++ b/tools/cosmovisor/poll_watcher.go @@ -0,0 +1,59 @@ +package cosmovisor + +import ( + "context" + "fmt" + "os" + "time" +) + +type PollWatcher struct { + outChan chan []byte + errChan chan error +} + +var _ Watcher[[]byte] = (*PollWatcher)(nil) + +func NewPollWatcher(ctx context.Context, filename string, pollInterval time.Duration) *PollWatcher { + outChan := make(chan []byte, 1) + errChan := make(chan error, 1) + ticker := time.NewTicker(pollInterval) + go func() { + defer ticker.Stop() + defer close(outChan) + defer close(errChan) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + stat, err := os.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + errChan <- fmt.Errorf("failed to stat upgrade info file: %w", err) + } + } else if stat.Size() > 0 { + bz, err := os.ReadFile(filename) + if err != nil { + errChan <- fmt.Errorf("failed to read file %s: %w", filename, err) + } else { + outChan <- bz + } + } + } + } + }() + return &PollWatcher{ + outChan: outChan, + errChan: errChan, + } +} + +func (w *PollWatcher) Updated() <-chan []byte { + return w.outChan +} + +func (w *PollWatcher) Errors() <-chan error { + return w.errChan +} diff --git a/tools/cosmovisor/poll_watcher_test.go b/tools/cosmovisor/poll_watcher_test.go new file mode 100644 index 000000000000..3bcc1977d5f1 --- /dev/null +++ b/tools/cosmovisor/poll_watcher_test.go @@ -0,0 +1,60 @@ +package cosmovisor + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestPollWatcher(t *testing.T) { + dir, err := os.MkdirTemp("", "watcher") + require.NoError(t, err) + filename := filepath.Join(dir, "testfile") + + ctx, cancel := context.WithCancel(context.Background()) + watcher := NewPollWatcher(ctx, filename, time.Millisecond*100) + expectedContext := []byte("test") + go func() { + time.Sleep(time.Second) + err := os.WriteFile(filename, expectedContext, 0644) + require.NoError(t, err) + time.Sleep(time.Second) + cancel() + }() + var actualContext []byte + + // we check all the channels in a function which we'll return from whenever + // a channel is closed or we get the done signal + func() { + for { + select { + case bz, ok := <-watcher.Updated(): + if !ok { + return + } + actualContext = bz + case err, ok := <-watcher.Errors(): + if !ok { + return + } + require.NoError(t, err) + case <-ctx.Done(): + return + } + } + }() + + // check we have the expected context + require.Equal(t, expectedContext, actualContext) + + // check that all the channels are closed + _, open := <-watcher.Updated() + require.False(t, open) + _, open = <-watcher.Errors() + require.False(t, open) + +} diff --git a/tools/cosmovisor/watcher.go b/tools/cosmovisor/watcher.go index ea5921e22fb8..5850a39b9b81 100644 --- a/tools/cosmovisor/watcher.go +++ b/tools/cosmovisor/watcher.go @@ -2,22 +2,21 @@ package cosmovisor import ( "encoding/json" - "time" ) type DataWatcher[T any] struct { - watcher FileWatcher + watcher Watcher[[]byte] ReceivedUpdate chan T } -func NewDataWatcher[T any](watcher FileWatcher) *DataWatcher[T] { +func NewDataWatcher[T any](watcher Watcher[[]byte]) *DataWatcher[T] { ch := make(chan T) go func() { for { select { - case contents := <-watcher.FileContentsUpdated(): + case contents := <-watcher.Updated(): var data T - err := json.Unmarshal([]byte(contents), &data) + err := json.Unmarshal(contents, &data) if err == nil { ch <- data } @@ -29,43 +28,7 @@ func NewDataWatcher[T any](watcher FileWatcher) *DataWatcher[T] { } } -type FileWatcher interface { - FileContentsUpdated() <-chan string - Stop() -} - -type PollWatcher struct { -} - -var _ FileWatcher = (*PollWatcher)(nil) - -func NewPollWatcher(filename string, pollInterval time.Duration) *PollWatcher { - return &PollWatcher{} -} - -func (w *PollWatcher) FileContentsUpdated() <-chan string { - panic("not implemented") -} - -func (w *PollWatcher) Stop() { - //TODO implement me - panic("implement me") -} - -type FSNotifyWatcher struct { -} - -var _ FileWatcher = (*FSNotifyWatcher)(nil) - -func NewFSNotifyWatcher(filename string) *FSNotifyWatcher { - return &FSNotifyWatcher{} -} - -func (w *FSNotifyWatcher) FileContentsUpdated() <-chan string { - panic("not implemented") -} - -func (w *FSNotifyWatcher) Stop() { - //TODO implement me - panic("implement me") +type Watcher[T any] interface { + Updated() <-chan T + Errors() <-chan error } From 2277ffb65869e0e4d1763ebbec12d0f6c87cac6b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 21 May 2025 14:53:21 -0400 Subject: [PATCH 005/115] data watcher tests --- tools/cosmovisor/data_watcher.go | 55 ++++++++++++++++++ tools/cosmovisor/data_watcher_test.go | 83 +++++++++++++++++++++++++++ tools/cosmovisor/poll_watcher_test.go | 19 ++++-- tools/cosmovisor/watcher.go | 28 --------- 4 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 tools/cosmovisor/data_watcher.go create mode 100644 tools/cosmovisor/data_watcher_test.go diff --git a/tools/cosmovisor/data_watcher.go b/tools/cosmovisor/data_watcher.go new file mode 100644 index 000000000000..fd91a0030db8 --- /dev/null +++ b/tools/cosmovisor/data_watcher.go @@ -0,0 +1,55 @@ +package cosmovisor + +import ( + "context" + "encoding/json" +) + +type DataWatcher[T any] struct { + outChan chan T + errChan chan error +} + +func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte]) *DataWatcher[T] { + outChan := make(chan T, 1) + errChan := make(chan error, 1) + go func() { + defer close(outChan) + defer close(errChan) + for { + select { + case <-ctx.Done(): + return + case contents, ok := <-watcher.Updated(): + if !ok { + return + } + var data T + err := json.Unmarshal(contents, &data) + // ignore errors because failing JSON unmarshal probably just means the file is incomplete + if err == nil { + outChan <- data + } + case err, ok := <-watcher.Errors(): + if !ok { + return + } + errChan <- err + + } + } + }() + return &DataWatcher[T]{ + outChan: outChan, + errChan: errChan, + } +} + +func (d DataWatcher[T]) Updated() <-chan T { + return d.outChan + +} + +func (d DataWatcher[T]) Errors() <-chan error { + return d.errChan +} diff --git a/tools/cosmovisor/data_watcher_test.go b/tools/cosmovisor/data_watcher_test.go new file mode 100644 index 000000000000..082eddedf803 --- /dev/null +++ b/tools/cosmovisor/data_watcher_test.go @@ -0,0 +1,83 @@ +package cosmovisor + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type TestData struct { + X int `json:"x"` + Y string `json:"y"` +} + +func TestDataWatcher(t *testing.T) { + dir, err := os.MkdirTemp("", "watcher") + require.NoError(t, err) + filename := filepath.Join(dir, "testfile.json") + + ctx, cancel := context.WithCancel(context.Background()) + pollWatcher := NewPollWatcher(ctx, filename, time.Millisecond*100) + dataWatcher := NewDataWatcher[TestData](ctx, pollWatcher) + + expectedContent := TestData{ + X: 10, + Y: "testtesttest", + } + go func() { + // write some dummy data to the file + time.Sleep(time.Second) + err = os.WriteFile(filename, []byte("unexpected content - should be ignored"), 0644) + require.NoError(t, err) + + // write the expected content to the file + time.Sleep(time.Second) + bz, err := json.Marshal(expectedContent) + require.NoError(t, err) + err = os.WriteFile(filename, bz, 0644) + require.NoError(t, err) + + // wait a bit to ensure the watcher has time to pick up the change + // then cancel the context + time.Sleep(time.Second) + cancel() + }() + + var actualContext *TestData + + // we check all the channels in a function which we'll return from whenever + // a channel is closed or we get the done signal + func() { + for { + select { + case content, ok := <-dataWatcher.Updated(): + if !ok { + return + } + actualContext = &content + case err, ok := <-dataWatcher.Errors(): + if !ok { + return + } + require.NoError(t, err) + case <-ctx.Done(): + return + } + } + }() + + // check we have the expected context + require.Equal(t, expectedContent, *actualContext) + + // check that all the channels are closed + _, open := <-dataWatcher.Updated() + require.False(t, open) + _, open = <-dataWatcher.Errors() + require.False(t, open) + +} diff --git a/tools/cosmovisor/poll_watcher_test.go b/tools/cosmovisor/poll_watcher_test.go index 3bcc1977d5f1..d791351c5da5 100644 --- a/tools/cosmovisor/poll_watcher_test.go +++ b/tools/cosmovisor/poll_watcher_test.go @@ -17,16 +17,25 @@ func TestPollWatcher(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) watcher := NewPollWatcher(ctx, filename, time.Millisecond*100) - expectedContext := []byte("test") + expectedContent := []byte("test") go func() { + // write some dummy data to the file time.Sleep(time.Second) - err := os.WriteFile(filename, expectedContext, 0644) + err = os.WriteFile(filename, []byte("unexpected content - should be updated later"), 0644) require.NoError(t, err) + + // write the expected content to the file + time.Sleep(time.Second) + err := os.WriteFile(filename, expectedContent, 0644) + require.NoError(t, err) + + // wait a bit to ensure the watcher has time to pick up the change + // then cancel the context time.Sleep(time.Second) cancel() }() - var actualContext []byte + var actualContent []byte // we check all the channels in a function which we'll return from whenever // a channel is closed or we get the done signal func() { @@ -36,7 +45,7 @@ func TestPollWatcher(t *testing.T) { if !ok { return } - actualContext = bz + actualContent = bz case err, ok := <-watcher.Errors(): if !ok { return @@ -49,7 +58,7 @@ func TestPollWatcher(t *testing.T) { }() // check we have the expected context - require.Equal(t, expectedContext, actualContext) + require.Equal(t, expectedContent, actualContent) // check that all the channels are closed _, open := <-watcher.Updated() diff --git a/tools/cosmovisor/watcher.go b/tools/cosmovisor/watcher.go index 5850a39b9b81..ec547f017fc0 100644 --- a/tools/cosmovisor/watcher.go +++ b/tools/cosmovisor/watcher.go @@ -1,33 +1,5 @@ package cosmovisor -import ( - "encoding/json" -) - -type DataWatcher[T any] struct { - watcher Watcher[[]byte] - ReceivedUpdate chan T -} - -func NewDataWatcher[T any](watcher Watcher[[]byte]) *DataWatcher[T] { - ch := make(chan T) - go func() { - for { - select { - case contents := <-watcher.Updated(): - var data T - err := json.Unmarshal(contents, &data) - if err == nil { - ch <- data - } - } - } - }() - return &DataWatcher[T]{ - watcher: watcher, - } -} - type Watcher[T any] interface { Updated() <-chan T Errors() <-chan error From f0f5caf5885d04a7b778ee3da014b8a9b19237ee Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 22 May 2025 13:06:31 -0400 Subject: [PATCH 006/115] notes --- tools/cosmovisor/cmd/cosmovisor/add_upgrade.go | 1 + tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go | 2 ++ tools/cosmovisor/process.go | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 929b5b54ea0a..3a5bfbca66d1 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -27,6 +27,7 @@ func NewAddUpgradeCmd() *cobra.Command { } // addUpgrade adds upgrade info to manifest +// TODO batch-upgrade and add-upgrade should write to the same batch file func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath, upgradeInfoPath string) error { logger := cfg.Logger(os.Stdout) diff --git a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go index a66f65b406ab..0e62460f51b7 100644 --- a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go @@ -84,6 +84,7 @@ func processUpgradeList(cfg *cosmovisor.Config, upgradeList [][]string) error { } upgradeInfoPath := cfg.UpgradeInfoFilePath() + "." + upgradeName upgradeInfoPaths = append(upgradeInfoPaths, upgradeInfoPath) + // TODO we shouldn't be calling this to create a file and then later read it back! if err := addUpgrade(cfg, true, upgradeHeight, upgradeName, upgradePath, upgradeInfoPath); err != nil { return err } @@ -112,6 +113,7 @@ func processUpgradeList(cfg *cosmovisor.Config, upgradeList [][]string) error { return fmt.Errorf("error marshaling combined JSON: %w", err) } + // TODO batch-upgrade and add-upgrade should write to the same batch file // Write to output file err = os.WriteFile(cfg.UpgradeInfoBatchFilePath(), batchData, 0o600) if err != nil { diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index e2bb122d8be6..33763171f17f 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -207,6 +207,7 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup wg.Add(1) + // TODO: replace BatchUpgradeWatcher go func() { defer wg.Done() BatchUpgradeWatcher(ctx, l.cfg, l.logger) @@ -264,6 +265,7 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) // It returns (false, nil) if the process exited normally without triggering an upgrade. This is very unlikely // to happen with "start" but may happen with short-lived commands like `simd genesis export ...` func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { + // TODO we shouldn't be getting any current upgrade because we're only using upgrade-info.json to receive signals from the node currentUpgrade, err := l.cfg.UpgradeInfo() if err != nil { // upgrade info not found do nothing @@ -276,6 +278,8 @@ func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { }() select { + // TODO add manual-upgrades watcher + // TODO replace with upgrade-info.json watcher case <-l.fw.MonitorUpdate(currentUpgrade): // upgrade - kill the process and restart l.logger.Info("daemon shutting down in an attempt to restart") From 3f628b964d16686c9e34aff4061bc918ac93ac01 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 22 May 2025 13:19:39 -0400 Subject: [PATCH 007/115] use common logic for add-batch-upgrade and add-upgrade --- tools/cosmovisor/args.go | 13 ++- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 9 +- .../cmd/cosmovisor/batch_upgrade.go | 46 ++-------- tools/cosmovisor/manual.go | 85 +++++-------------- 4 files changed, 38 insertions(+), 115 deletions(-) diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index 18a7f1098b44..2e4a0f0d1ac4 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -112,11 +112,6 @@ func (cfg *Config) UpgradeInfoFilePath() string { return filepath.Join(cfg.Home, "data", upgradetypes.UpgradeInfoFilename) } -// UpgradeInfoFilePath is the expected upgrade-info filename created by `x/upgrade/keeper`. -func (cfg *Config) ManualUpgradesFilePath() string { - return filepath.Join(cfg.Home, "data", ManualUpgradesFilename) -} - // UpgradeInfoBatchFilePath is the same as UpgradeInfoFilePath but with a batch suffix. func (cfg *Config) UpgradeInfoBatchFilePath() string { return cfg.UpgradeInfoFilePath() + ".batch" @@ -580,7 +575,7 @@ func (cfg Config) DetailString() string { var sb strings.Builder sb.WriteString("Configurable Values:\n") for _, kv := range configEntries { - fmt.Fprintf(&sb, " %s: %s\n", kv.name, kv.value) + _, _ = fmt.Fprintf(&sb, " %s: %s\n", kv.name, kv.value) } sb.WriteString("Derived Values:\n") dnl := 0 @@ -591,7 +586,7 @@ func (cfg Config) DetailString() string { } dFmt := fmt.Sprintf(" %%%ds: %%s\n", dnl) for _, kv := range derivedEntries { - fmt.Fprintf(&sb, dFmt, kv.name, kv.value) + _, _ = fmt.Fprintf(&sb, dFmt, kv.name, kv.value) } return sb.String() } @@ -619,7 +614,9 @@ func (cfg Config) Export() (string, error) { // convert the time value to its format option cfg.TimeFormatLogs = ValueToTimeFormatOption(cfg.TimeFormatLogs) - defer file.Close() + defer func(file *os.File) { + _ = file.Close() + }(file) // write the configuration to the file err = toml.NewEncoder(file).Encode(cfg) diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 3a5bfbca66d1..0d6d08d734e2 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) func NewAddUpgradeCmd() *cobra.Command { @@ -20,7 +21,7 @@ func NewAddUpgradeCmd() *cobra.Command { RunE: addUpgradeCmd, } - addUpgrade.Flags().Bool(cosmovisor.FlagForce, false, "overwrite existing upgrade binary / upgrade-info.json file") + addUpgrade.Flags().Bool(cosmovisor.FlagForce, false, "overwrite existing upgrade binary and plan with the same name") addUpgrade.Flags().Int64(cosmovisor.FlagUpgradeHeight, 0, "define a height at which to upgrade the binary automatically (without governance proposal)") return addUpgrade @@ -28,7 +29,7 @@ func NewAddUpgradeCmd() *cobra.Command { // addUpgrade adds upgrade info to manifest // TODO batch-upgrade and add-upgrade should write to the same batch file -func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath, upgradeInfoPath string) error { +func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath string) error { logger := cfg.Logger(os.Stdout) if !cfg.DisableRecase { @@ -63,7 +64,7 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) if upgradeHeight > 0 { - plan := cosmovisor.ManualUpgradePlan{ + plan := upgradetypes.Plan{ Name: upgradeName, Height: upgradeHeight, } @@ -114,7 +115,7 @@ func addUpgradeCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get upgrade-height flag: %w", err) } - return addUpgrade(cfg, force, upgradeHeight, upgradeName, executablePath, cfg.UpgradeInfoFilePath()) + return addUpgrade(cfg, force, upgradeHeight, upgradeName, executablePath) } // saveOrAbort saves data to path or aborts if file exists and force is false diff --git a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go index 0e62460f51b7..1baa4a81e4ac 100644 --- a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go @@ -2,7 +2,6 @@ package main import ( "encoding/csv" - "encoding/json" "fmt" "os" "path/filepath" @@ -49,7 +48,7 @@ cosmovisor add-batch-upgrade --upgrade-file /path/to/batch_upgrade.csv`, // addBatchUpgrade takes in multiple specified upgrades and creates a single // batch upgrade file out of them -func addBatchUpgrade(cmd *cobra.Command, args []string) error { +func addBatchUpgrade(cmd *cobra.Command, _ []string) error { cfg, err := getConfigFromCmd(cmd) if err != nil { return err @@ -71,7 +70,6 @@ func addBatchUpgrade(cmd *cobra.Command, args []string) error { // 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) @@ -82,44 +80,12 @@ func processUpgradeList(cfg *cosmovisor.Config, upgradeList [][]string) error { 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) - // TODO we shouldn't be calling this to create a file and then later read it back! - if err := addUpgrade(cfg, true, upgradeHeight, upgradeName, upgradePath, upgradeInfoPath); err != nil { - return err - } - } - - 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) + // we use the same logic as the add-upgrade command here, appending to any existing manual upgrade data + if err := addUpgrade(cfg, true, upgradeHeight, upgradeName, upgradePath); err != nil { + return 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) - } - - // TODO batch-upgrade and add-upgrade should write to the same batch file - // 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 } @@ -129,7 +95,9 @@ func processUpgradeFile(cfg *cosmovisor.Config, upgradeFile string) error { if err != nil { return fmt.Errorf("error opening upgrade CSV file %s: %w", upgradeFile, err) } - defer file.Close() + defer func(file *os.File) { + _ = file.Close() + }(file) r := csv.NewReader(file) r.FieldsPerRecord = 3 diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 08565b60898a..873388413a87 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -4,94 +4,51 @@ import ( "encoding/json" "fmt" "os" -) - -// ManualUpgradesFilename is the file to store manual upgrade information -const ManualUpgradesFilename = "manual-upgrades.json" - -// ManualUpgradePlan is the plan for a manual upgrade. -type ManualUpgradePlan struct { - // Name is the name of the upgrade. - Name string `json:"name"` - // Height is the block height that the node will halt at (by setting the --halt-height flag). - Height int64 `json:"height"` - // Info is any additional information about the upgrade. - Info string `json:"info"` -} - -func (p ManualUpgradePlan) ValidateBasic() error { - if len(p.Name) == 0 { - return fmt.Errorf("name cannot be empty") - } - - if p.Height <= 0 { - return fmt.Errorf("height must be greater than 0") - } - - return nil -} - -// ManualUpgradeData is the data structure for manual upgrades. -type ManualUpgradeData struct { - // Upgrades is a map of manual upgrade names to their plans. - Upgrades map[string]ManualUpgradePlan `json:"upgrades"` -} -func (m ManualUpgradeData) NextPlanAfter(height int64) *ManualUpgradePlan { - if m.Upgrades == nil { - return nil - } - - // sort the upgrades by height - var nextPlan *ManualUpgradePlan - for _, plan := range m.Upgrades { - if plan.Height > height { - if nextPlan == nil || plan.Height < nextPlan.Height { - nextPlan = &plan - } - } - } - return nextPlan -} + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) // ReadManualUpgrades reads the manual upgrade data. -func ReadManualUpgrades(cfg *Config) (ManualUpgradeData, error) { - bz, err := os.ReadFile(cfg.ManualUpgradesFilePath()) +func ReadManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { + bz, err := os.ReadFile(cfg.UpgradeInfoBatchFilePath()) if err != nil { if os.IsNotExist(err) { - return ManualUpgradeData{}, nil + return nil, nil } - return ManualUpgradeData{}, err + return nil, err } - manualUpgrades := ManualUpgradeData{} + var manualUpgrades []upgradetypes.Plan if err := json.Unmarshal(bz, &manualUpgrades); err != nil { - return ManualUpgradeData{}, err + return nil, err } return manualUpgrades, nil } // AddManualUpgrade adds a manual upgrade plan. // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, -// otherwise os.ErrExist will be returned. -func AddManualUpgrade(cfg *Config, plan ManualUpgradePlan, forceOverwrite bool) error { +// otherwise an error will be returned. +func AddManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) error { manualUpgrades, err := ReadManualUpgrades(cfg) if err != nil { return err } - if manualUpgrades.Upgrades == nil { - manualUpgrades.Upgrades = make(map[string]ManualUpgradePlan) - } - - if _, exists := manualUpgrades.Upgrades[plan.Name]; exists && !forceOverwrite { - return os.ErrExist + var newUpgrades []upgradetypes.Plan + for _, existing := range manualUpgrades { + if existing.Name == plan.Name { + if !forceOverwrite { + return fmt.Errorf("upgrade with name %s already exists", plan.Name) + } + newUpgrades = append(newUpgrades, plan) + } else { + newUpgrades = append(newUpgrades, existing) + } } - manualUpgrades.Upgrades[plan.Name] = plan manualUpgradesData, err := json.MarshalIndent(manualUpgrades, "", " ") if err != nil { return err } - return os.WriteFile(cfg.ManualUpgradesFilePath(), manualUpgradesData, 0644) + return os.WriteFile(cfg.UpgradeInfoBatchFilePath(), manualUpgradesData, 0644) } From 19858b3ded9297ee9346e4e0e291252d88e899b6 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 22 May 2025 13:31:54 -0400 Subject: [PATCH 008/115] refactoring watchers --- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 2 +- tools/cosmovisor/data_watcher.go | 10 +-- tools/cosmovisor/data_watcher_test.go | 4 +- tools/cosmovisor/fsnotify_watcher.go | 20 +++--- tools/cosmovisor/hybrid_watcher.go | 65 +++++++++++++++++++ tools/cosmovisor/manual.go | 21 ++++-- tools/cosmovisor/poll_watcher.go | 12 ++-- tools/cosmovisor/poll_watcher_test.go | 2 +- tools/cosmovisor/process.go | 40 ++++-------- tools/cosmovisor/watcher.go | 2 +- 10 files changed, 118 insertions(+), 60 deletions(-) create mode 100644 tools/cosmovisor/hybrid_watcher.go diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 0d6d08d734e2..648b24bc89e7 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -72,7 +72,7 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade panic(fmt.Errorf("invalid manual upgrade plan: %w", err)) } - if err := cosmovisor.AddManualUpgrade(cfg, plan, force); err != nil { + if err := cosmovisor.addManualUpgrade(cfg, plan, force); err != nil { panic(fmt.Errorf("failed to add manual upgrade: %w", err)) } diff --git a/tools/cosmovisor/data_watcher.go b/tools/cosmovisor/data_watcher.go index fd91a0030db8..053bf6d0b805 100644 --- a/tools/cosmovisor/data_watcher.go +++ b/tools/cosmovisor/data_watcher.go @@ -5,12 +5,12 @@ import ( "encoding/json" ) -type DataWatcher[T any] struct { +type dataWatcher[T any] struct { outChan chan T errChan chan error } -func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte]) *DataWatcher[T] { +func newDataWatcher[T any](ctx context.Context, watcher watcher[[]byte]) *dataWatcher[T] { outChan := make(chan T, 1) errChan := make(chan error, 1) go func() { @@ -39,17 +39,17 @@ func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte]) *DataWa } } }() - return &DataWatcher[T]{ + return &dataWatcher[T]{ outChan: outChan, errChan: errChan, } } -func (d DataWatcher[T]) Updated() <-chan T { +func (d dataWatcher[T]) Updated() <-chan T { return d.outChan } -func (d DataWatcher[T]) Errors() <-chan error { +func (d dataWatcher[T]) Errors() <-chan error { return d.errChan } diff --git a/tools/cosmovisor/data_watcher_test.go b/tools/cosmovisor/data_watcher_test.go index 082eddedf803..3bbc0cbb3801 100644 --- a/tools/cosmovisor/data_watcher_test.go +++ b/tools/cosmovisor/data_watcher_test.go @@ -22,8 +22,8 @@ func TestDataWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile.json") ctx, cancel := context.WithCancel(context.Background()) - pollWatcher := NewPollWatcher(ctx, filename, time.Millisecond*100) - dataWatcher := NewDataWatcher[TestData](ctx, pollWatcher) + pollWatcher := newPollWatcher(ctx, filename, time.Millisecond*100) + dataWatcher := newDataWatcher[TestData](ctx, pollWatcher) expectedContent := TestData{ X: 10, diff --git a/tools/cosmovisor/fsnotify_watcher.go b/tools/cosmovisor/fsnotify_watcher.go index eb06355ca3d0..d5d7b0b30ddf 100644 --- a/tools/cosmovisor/fsnotify_watcher.go +++ b/tools/cosmovisor/fsnotify_watcher.go @@ -8,20 +8,20 @@ import ( "github.com/fsnotify/fsnotify" ) -type FSNotifyWatcher struct { +type fsNotifyWatcher struct { watcher *fsnotify.Watcher - outChan chan FileUpdate + outChan chan fileUpdate errChan chan error } -var _ Watcher[FileUpdate] = (*FSNotifyWatcher)(nil) +var _ watcher[fileUpdate] = (*fsNotifyWatcher)(nil) -type FileUpdate struct { +type fileUpdate struct { Filename string Contents []byte } -func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*FSNotifyWatcher, error) { +func newFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*fsNotifyWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err @@ -38,7 +38,7 @@ func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*F filenameSet[filename] = struct{}{} } - outChan := make(chan FileUpdate, 1) + outChan := make(chan fileUpdate, 1) errChan := make(chan error, 1) go func() { // close the watcher and channels @@ -64,7 +64,7 @@ func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*F if err != nil { errChan <- fmt.Errorf("failed to read file %s: %w", filename, err) } else { - outChan <- FileUpdate{Filename: filename, Contents: bz} + outChan <- fileUpdate{Filename: filename, Contents: bz} } } case err, ok := <-watcher.Errors: @@ -76,17 +76,17 @@ func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*F } }() - return &FSNotifyWatcher{ + return &fsNotifyWatcher{ watcher: watcher, outChan: outChan, errChan: errChan, }, nil } -func (w *FSNotifyWatcher) Updated() <-chan FileUpdate { +func (w *fsNotifyWatcher) Updated() <-chan fileUpdate { return w.outChan } -func (w *FSNotifyWatcher) Errors() <-chan error { +func (w *fsNotifyWatcher) Errors() <-chan error { return w.errChan } diff --git a/tools/cosmovisor/hybrid_watcher.go b/tools/cosmovisor/hybrid_watcher.go new file mode 100644 index 000000000000..7e1c8dbb9a38 --- /dev/null +++ b/tools/cosmovisor/hybrid_watcher.go @@ -0,0 +1,65 @@ +package cosmovisor + +import ( + "context" + "time" +) + +type hybridWatcher struct { + outChan chan []byte + errChan chan error +} + +var _ watcher[[]byte] = &hybridWatcher{} + +func newHybridWatcher(ctx context.Context, dirWatcher *fsNotifyWatcher, filename string, backupPollInterval time.Duration) *hybridWatcher { + pollWatcher := newPollWatcher(ctx, filename, backupPollInterval) + outChan := make(chan []byte, 1) + errChan := make(chan error, 1) + + go func() { + defer close(outChan) + defer close(errChan) + for { + select { + case <-ctx.Done(): + return + case update, ok := <-dirWatcher.Updated(): + if !ok { + return + } + if update.Filename == filename { + outChan <- update.Contents + } + case update, ok := <-pollWatcher.Updated(): + if !ok { + return + } + outChan <- update + case err, ok := <-dirWatcher.Errors(): + if !ok { + return + } + errChan <- err + case err, ok := <-pollWatcher.Errors(): + if !ok { + return + } + errChan <- err + } + } + }() + + return &hybridWatcher{ + outChan: outChan, + errChan: errChan, + } +} + +func (h hybridWatcher) Updated() <-chan []byte { + return h.outChan +} + +func (h hybridWatcher) Errors() <-chan error { + return h.errChan +} diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 873388413a87..8e69f66224cd 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -4,12 +4,13 @@ import ( "encoding/json" "fmt" "os" + "sort" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -// ReadManualUpgrades reads the manual upgrade data. -func ReadManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { +// readManualUpgrades reads the manual upgrade data. +func readManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { bz, err := os.ReadFile(cfg.UpgradeInfoBatchFilePath()) if err != nil { if os.IsNotExist(err) { @@ -21,14 +22,16 @@ func ReadManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { if err := json.Unmarshal(bz, &manualUpgrades); err != nil { return nil, err } + + sortUpgrades(manualUpgrades) return manualUpgrades, nil } -// AddManualUpgrade adds a manual upgrade plan. +// addManualUpgrade adds a manual upgrade plan. // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. -func AddManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) error { - manualUpgrades, err := ReadManualUpgrades(cfg) +func addManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) error { + manualUpgrades, err := readManualUpgrades(cfg) if err != nil { return err } @@ -45,6 +48,8 @@ func AddManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) } } + sortUpgrades(manualUpgrades) + manualUpgradesData, err := json.MarshalIndent(manualUpgrades, "", " ") if err != nil { return err @@ -52,3 +57,9 @@ func AddManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) return os.WriteFile(cfg.UpgradeInfoBatchFilePath(), manualUpgradesData, 0644) } + +func sortUpgrades(upgrades []upgradetypes.Plan) { + sort.Slice(upgrades, func(i, j int) bool { + return upgrades[i].Height < upgrades[j].Height + }) +} diff --git a/tools/cosmovisor/poll_watcher.go b/tools/cosmovisor/poll_watcher.go index 4fa829ad951a..f46de2a705f2 100644 --- a/tools/cosmovisor/poll_watcher.go +++ b/tools/cosmovisor/poll_watcher.go @@ -7,14 +7,14 @@ import ( "time" ) -type PollWatcher struct { +type pollWatcher struct { outChan chan []byte errChan chan error } -var _ Watcher[[]byte] = (*PollWatcher)(nil) +var _ watcher[[]byte] = (*pollWatcher)(nil) -func NewPollWatcher(ctx context.Context, filename string, pollInterval time.Duration) *PollWatcher { +func newPollWatcher(ctx context.Context, filename string, pollInterval time.Duration) *pollWatcher { outChan := make(chan []byte, 1) errChan := make(chan error, 1) ticker := time.NewTicker(pollInterval) @@ -44,16 +44,16 @@ func NewPollWatcher(ctx context.Context, filename string, pollInterval time.Dura } } }() - return &PollWatcher{ + return &pollWatcher{ outChan: outChan, errChan: errChan, } } -func (w *PollWatcher) Updated() <-chan []byte { +func (w *pollWatcher) Updated() <-chan []byte { return w.outChan } -func (w *PollWatcher) Errors() <-chan error { +func (w *pollWatcher) Errors() <-chan error { return w.errChan } diff --git a/tools/cosmovisor/poll_watcher_test.go b/tools/cosmovisor/poll_watcher_test.go index d791351c5da5..2aa20f3ab047 100644 --- a/tools/cosmovisor/poll_watcher_test.go +++ b/tools/cosmovisor/poll_watcher_test.go @@ -16,7 +16,7 @@ func TestPollWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile") ctx, cancel := context.WithCancel(context.Background()) - watcher := NewPollWatcher(ctx, filename, time.Millisecond*100) + watcher := newPollWatcher(ctx, filename, time.Millisecond*100) expectedContent := []byte("test") go func() { // write some dummy data to the file diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index 33763171f17f..8a40103648ab 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -10,7 +10,6 @@ import ( "os/exec" "os/signal" "path/filepath" - "sort" "strconv" "strings" "sync" @@ -32,7 +31,10 @@ import ( type Launcher struct { logger log.Logger cfg *Config - fw *fileWatcher + // nodeUpgradeWatcher watches for data in an upgrade-info.json created by the running node + nodeUpgradeWatcher *dataWatcher[upgradetypes.Plan] + // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator + manualUpgradesWatcher *dataWatcher[[]upgradetypes.Plan] } func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { @@ -41,27 +43,7 @@ func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { return Launcher{}, err } - return Launcher{logger: logger, cfg: cfg, fw: fw}, nil -} - -// loadBatchUpgradeFile loads the batch upgrade file into memory, sorted by -// their upgrade heights -func loadBatchUpgradeFile(cfg *Config) ([]upgradetypes.Plan, error) { - var uInfos []upgradetypes.Plan - upgradeInfoFile, err := os.ReadFile(cfg.UpgradeInfoBatchFilePath()) - if os.IsNotExist(err) { - return uInfos, nil - } else if err != nil { - return nil, fmt.Errorf("error while reading %s: %w", cfg.UpgradeInfoBatchFilePath(), err) - } - - if err = json.Unmarshal(upgradeInfoFile, &uInfos); err != nil { - return nil, err - } - sort.Slice(uInfos, func(i, j int) bool { - return uInfos[i].Height < uInfos[j].Height - }) - return uInfos, nil + return Launcher{logger: logger, cfg: cfg, nodeUpgradeWatcher: fw}, nil } // BatchUpgradeWatcher starts a watcher loop that swaps upgrade manifests at the correct @@ -69,7 +51,7 @@ func loadBatchUpgradeFile(cfg *Config) ([]upgradetypes.Plan, error) { // via the websocket API. func BatchUpgradeWatcher(ctx context.Context, cfg *Config, logger log.Logger) { // load batch file in memory - uInfos, err := loadBatchUpgradeFile(cfg) + uInfos, err := readManualUpgrades(cfg) if err != nil { logger.Warn("failed to load batch upgrade file", "error", err) uInfos = []upgradetypes.Plan{} @@ -229,7 +211,7 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) return false, err } - if !IsSkipUpgradeHeight(args, l.fw.currentInfo) { + if !IsSkipUpgradeHeight(args, l.nodeUpgradeWatcher.currentInfo) { l.cfg.WaitRestartDelay() if err := l.doBackup(); err != nil { @@ -240,7 +222,7 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) return false, err } - if err := UpgradeBinary(l.logger, l.cfg, l.fw.currentInfo); err != nil { + if err := UpgradeBinary(l.logger, l.cfg, l.nodeUpgradeWatcher.currentInfo); err != nil { return false, err } @@ -280,7 +262,7 @@ func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { select { // TODO add manual-upgrades watcher // TODO replace with upgrade-info.json watcher - case <-l.fw.MonitorUpdate(currentUpgrade): + case <-l.nodeUpgradeWatcher.MonitorUpdate(currentUpgrade): // upgrade - kill the process and restart l.logger.Info("daemon shutting down in an attempt to restart") @@ -311,14 +293,14 @@ func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { _ = cmd.Process.Kill() } case err := <-cmdDone: - l.fw.Stop() + l.nodeUpgradeWatcher.Stop() // no error -> command exits normally (eg. short command like `gaiad version`) if err == nil { return false, nil } // the app x/upgrade causes a panic and the app can die before the filwatcher finds the // update, so we need to recheck update-info file. - if !l.fw.CheckUpdate(currentUpgrade) { + if !l.nodeUpgradeWatcher.CheckUpdate(currentUpgrade) { return false, err } } diff --git a/tools/cosmovisor/watcher.go b/tools/cosmovisor/watcher.go index ec547f017fc0..6fe7bfbae713 100644 --- a/tools/cosmovisor/watcher.go +++ b/tools/cosmovisor/watcher.go @@ -1,6 +1,6 @@ package cosmovisor -type Watcher[T any] interface { +type watcher[T any] interface { Updated() <-chan T Errors() <-chan error } From 496bba866474b2b76d1608409544d4db12b5ebd1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 22 May 2025 13:46:11 -0400 Subject: [PATCH 009/115] watcher initialization --- tools/cosmovisor/args.go | 7 ++++- .../{ => internal/watchers}/data_watcher.go | 12 ++++---- .../watchers}/data_watcher_test.go | 6 ++-- .../watchers}/fsnotify_watcher.go | 30 +++++++++---------- .../{ => internal/watchers}/hybrid_watcher.go | 16 +++++----- .../{ => internal/watchers}/poll_watcher.go | 14 ++++----- .../watchers}/poll_watcher_test.go | 4 +-- tools/cosmovisor/internal/watchers/watcher.go | 6 ++++ tools/cosmovisor/manual.go | 10 +++---- tools/cosmovisor/process.go | 30 +++++++++++++++---- tools/cosmovisor/scanner.go | 12 ++++++++ tools/cosmovisor/watcher.go | 6 ---- 12 files changed, 94 insertions(+), 59 deletions(-) rename tools/cosmovisor/{ => internal/watchers}/data_watcher.go (73%) rename tools/cosmovisor/{ => internal/watchers}/data_watcher_test.go (92%) rename tools/cosmovisor/{ => internal/watchers}/fsnotify_watcher.go (74%) rename tools/cosmovisor/{ => internal/watchers}/hybrid_watcher.go (66%) rename tools/cosmovisor/{ => internal/watchers}/poll_watcher.go (72%) rename tools/cosmovisor/{ => internal/watchers}/poll_watcher_test.go (94%) create mode 100644 tools/cosmovisor/internal/watchers/watcher.go delete mode 100644 tools/cosmovisor/watcher.go diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index 2e4a0f0d1ac4..4e529459c50b 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -107,9 +107,14 @@ func (cfg *Config) BaseUpgradeDir() string { return filepath.Join(cfg.Root(), upgradesDir) } +// UpgradeInfoDir is the directory where upgrade-info.json is expected to be created by `x/upgrade/keeper`. +func (cfg *Config) UpgradeInfoDir() string { + return filepath.Join(cfg.Home, "data") +} + // UpgradeInfoFilePath is the expected upgrade-info filename created by `x/upgrade/keeper`. func (cfg *Config) UpgradeInfoFilePath() string { - return filepath.Join(cfg.Home, "data", upgradetypes.UpgradeInfoFilename) + return filepath.Join(cfg.UpgradeInfoDir(), upgradetypes.UpgradeInfoFilename) } // UpgradeInfoBatchFilePath is the same as UpgradeInfoFilePath but with a batch suffix. diff --git a/tools/cosmovisor/data_watcher.go b/tools/cosmovisor/internal/watchers/data_watcher.go similarity index 73% rename from tools/cosmovisor/data_watcher.go rename to tools/cosmovisor/internal/watchers/data_watcher.go index 053bf6d0b805..d3886de71c2a 100644 --- a/tools/cosmovisor/data_watcher.go +++ b/tools/cosmovisor/internal/watchers/data_watcher.go @@ -1,16 +1,16 @@ -package cosmovisor +package watchers import ( "context" "encoding/json" ) -type dataWatcher[T any] struct { +type DataWatcher[T any] struct { outChan chan T errChan chan error } -func newDataWatcher[T any](ctx context.Context, watcher watcher[[]byte]) *dataWatcher[T] { +func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte]) *DataWatcher[T] { outChan := make(chan T, 1) errChan := make(chan error, 1) go func() { @@ -39,17 +39,17 @@ func newDataWatcher[T any](ctx context.Context, watcher watcher[[]byte]) *dataWa } } }() - return &dataWatcher[T]{ + return &DataWatcher[T]{ outChan: outChan, errChan: errChan, } } -func (d dataWatcher[T]) Updated() <-chan T { +func (d DataWatcher[T]) Updated() <-chan T { return d.outChan } -func (d dataWatcher[T]) Errors() <-chan error { +func (d DataWatcher[T]) Errors() <-chan error { return d.errChan } diff --git a/tools/cosmovisor/data_watcher_test.go b/tools/cosmovisor/internal/watchers/data_watcher_test.go similarity index 92% rename from tools/cosmovisor/data_watcher_test.go rename to tools/cosmovisor/internal/watchers/data_watcher_test.go index 3bbc0cbb3801..8d3c80f32e39 100644 --- a/tools/cosmovisor/data_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/data_watcher_test.go @@ -1,4 +1,4 @@ -package cosmovisor +package watchers import ( "context" @@ -22,8 +22,8 @@ func TestDataWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile.json") ctx, cancel := context.WithCancel(context.Background()) - pollWatcher := newPollWatcher(ctx, filename, time.Millisecond*100) - dataWatcher := newDataWatcher[TestData](ctx, pollWatcher) + pollWatcher := NewPollWatcher(ctx, filename, time.Millisecond*100) + dataWatcher := NewDataWatcher[TestData](ctx, pollWatcher) expectedContent := TestData{ X: 10, diff --git a/tools/cosmovisor/fsnotify_watcher.go b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go similarity index 74% rename from tools/cosmovisor/fsnotify_watcher.go rename to tools/cosmovisor/internal/watchers/fsnotify_watcher.go index d5d7b0b30ddf..71af8bad7b16 100644 --- a/tools/cosmovisor/fsnotify_watcher.go +++ b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go @@ -1,4 +1,4 @@ -package cosmovisor +package watchers import ( "context" @@ -8,20 +8,15 @@ import ( "github.com/fsnotify/fsnotify" ) -type fsNotifyWatcher struct { +type FSNotifyWatcher struct { watcher *fsnotify.Watcher - outChan chan fileUpdate + outChan chan FileUpdate errChan chan error } -var _ watcher[fileUpdate] = (*fsNotifyWatcher)(nil) +var _ Watcher[FileUpdate] = (*FSNotifyWatcher)(nil) -type fileUpdate struct { - Filename string - Contents []byte -} - -func newFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*fsNotifyWatcher, error) { +func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*FSNotifyWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err @@ -38,7 +33,7 @@ func newFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*f filenameSet[filename] = struct{}{} } - outChan := make(chan fileUpdate, 1) + outChan := make(chan FileUpdate, 1) errChan := make(chan error, 1) go func() { // close the watcher and channels @@ -64,7 +59,7 @@ func newFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*f if err != nil { errChan <- fmt.Errorf("failed to read file %s: %w", filename, err) } else { - outChan <- fileUpdate{Filename: filename, Contents: bz} + outChan <- FileUpdate{Filename: filename, Contents: bz} } } case err, ok := <-watcher.Errors: @@ -76,17 +71,22 @@ func newFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*f } }() - return &fsNotifyWatcher{ + return &FSNotifyWatcher{ watcher: watcher, outChan: outChan, errChan: errChan, }, nil } -func (w *fsNotifyWatcher) Updated() <-chan fileUpdate { +type FileUpdate struct { + Filename string + Contents []byte +} + +func (w *FSNotifyWatcher) Updated() <-chan FileUpdate { return w.outChan } -func (w *fsNotifyWatcher) Errors() <-chan error { +func (w *FSNotifyWatcher) Errors() <-chan error { return w.errChan } diff --git a/tools/cosmovisor/hybrid_watcher.go b/tools/cosmovisor/internal/watchers/hybrid_watcher.go similarity index 66% rename from tools/cosmovisor/hybrid_watcher.go rename to tools/cosmovisor/internal/watchers/hybrid_watcher.go index 7e1c8dbb9a38..0f920b399d91 100644 --- a/tools/cosmovisor/hybrid_watcher.go +++ b/tools/cosmovisor/internal/watchers/hybrid_watcher.go @@ -1,19 +1,19 @@ -package cosmovisor +package watchers import ( "context" "time" ) -type hybridWatcher struct { +type HybridWatcher struct { outChan chan []byte errChan chan error } -var _ watcher[[]byte] = &hybridWatcher{} +var _ Watcher[[]byte] = &HybridWatcher{} -func newHybridWatcher(ctx context.Context, dirWatcher *fsNotifyWatcher, filename string, backupPollInterval time.Duration) *hybridWatcher { - pollWatcher := newPollWatcher(ctx, filename, backupPollInterval) +func NewHybridWatcher(ctx context.Context, dirWatcher *FSNotifyWatcher, filename string, backupPollInterval time.Duration) *HybridWatcher { + pollWatcher := NewPollWatcher(ctx, filename, backupPollInterval) outChan := make(chan []byte, 1) errChan := make(chan error, 1) @@ -50,16 +50,16 @@ func newHybridWatcher(ctx context.Context, dirWatcher *fsNotifyWatcher, filename } }() - return &hybridWatcher{ + return &HybridWatcher{ outChan: outChan, errChan: errChan, } } -func (h hybridWatcher) Updated() <-chan []byte { +func (h HybridWatcher) Updated() <-chan []byte { return h.outChan } -func (h hybridWatcher) Errors() <-chan error { +func (h HybridWatcher) Errors() <-chan error { return h.errChan } diff --git a/tools/cosmovisor/poll_watcher.go b/tools/cosmovisor/internal/watchers/poll_watcher.go similarity index 72% rename from tools/cosmovisor/poll_watcher.go rename to tools/cosmovisor/internal/watchers/poll_watcher.go index f46de2a705f2..1c4a10b8cae3 100644 --- a/tools/cosmovisor/poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/poll_watcher.go @@ -1,4 +1,4 @@ -package cosmovisor +package watchers import ( "context" @@ -7,14 +7,14 @@ import ( "time" ) -type pollWatcher struct { +type PollWatcher struct { outChan chan []byte errChan chan error } -var _ watcher[[]byte] = (*pollWatcher)(nil) +var _ Watcher[[]byte] = (*PollWatcher)(nil) -func newPollWatcher(ctx context.Context, filename string, pollInterval time.Duration) *pollWatcher { +func NewPollWatcher(ctx context.Context, filename string, pollInterval time.Duration) *PollWatcher { outChan := make(chan []byte, 1) errChan := make(chan error, 1) ticker := time.NewTicker(pollInterval) @@ -44,16 +44,16 @@ func newPollWatcher(ctx context.Context, filename string, pollInterval time.Dura } } }() - return &pollWatcher{ + return &PollWatcher{ outChan: outChan, errChan: errChan, } } -func (w *pollWatcher) Updated() <-chan []byte { +func (w *PollWatcher) Updated() <-chan []byte { return w.outChan } -func (w *pollWatcher) Errors() <-chan error { +func (w *PollWatcher) Errors() <-chan error { return w.errChan } diff --git a/tools/cosmovisor/poll_watcher_test.go b/tools/cosmovisor/internal/watchers/poll_watcher_test.go similarity index 94% rename from tools/cosmovisor/poll_watcher_test.go rename to tools/cosmovisor/internal/watchers/poll_watcher_test.go index 2aa20f3ab047..afe6b5afdb40 100644 --- a/tools/cosmovisor/poll_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/poll_watcher_test.go @@ -1,4 +1,4 @@ -package cosmovisor +package watchers import ( "context" @@ -16,7 +16,7 @@ func TestPollWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile") ctx, cancel := context.WithCancel(context.Background()) - watcher := newPollWatcher(ctx, filename, time.Millisecond*100) + watcher := NewPollWatcher(ctx, filename, time.Millisecond*100) expectedContent := []byte("test") go func() { // write some dummy data to the file diff --git a/tools/cosmovisor/internal/watchers/watcher.go b/tools/cosmovisor/internal/watchers/watcher.go new file mode 100644 index 000000000000..994048961a22 --- /dev/null +++ b/tools/cosmovisor/internal/watchers/watcher.go @@ -0,0 +1,6 @@ +package watchers + +type Watcher[T any] interface { + Updated() <-chan T + Errors() <-chan error +} diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 8e69f66224cd..046fb6b7abec 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -9,8 +9,8 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -// readManualUpgrades reads the manual upgrade data. -func readManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { +// ReadManualUpgrades reads the manual upgrade data. +func ReadManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { bz, err := os.ReadFile(cfg.UpgradeInfoBatchFilePath()) if err != nil { if os.IsNotExist(err) { @@ -27,11 +27,11 @@ func readManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { return manualUpgrades, nil } -// addManualUpgrade adds a manual upgrade plan. +// AddManualUpgrade adds a manual upgrade plan. // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. -func addManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) error { - manualUpgrades, err := readManualUpgrades(cfg) +func AddManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) error { + manualUpgrades, err := ReadManualUpgrades(cfg) if err != nil { return err } diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index 8a40103648ab..fde9b34cad9b 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -23,6 +23,7 @@ import ( "cosmossdk.io/log" + "cosmossdk.io/tools/cosmovisor/internal/watchers" "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" @@ -31,19 +32,36 @@ import ( type Launcher struct { logger log.Logger cfg *Config + ctx context.Context + cancel context.CancelFunc // nodeUpgradeWatcher watches for data in an upgrade-info.json created by the running node - nodeUpgradeWatcher *dataWatcher[upgradetypes.Plan] + nodeUpgradeWatcher watchers.Watcher[upgradetypes.Plan] // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator - manualUpgradesWatcher *dataWatcher[[]upgradetypes.Plan] + manualUpgradesWatcher watchers.Watcher[[]upgradetypes.Plan] } func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { - fw, err := newUpgradeFileWatcher(cfg) + ctx, cancel := context.WithCancel(context.Background()) + + dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, cfg.UpgradeInfoDir(), []string{ + cfg.UpgradeInfoFilePath(), + cfg.UpgradeInfoBatchFilePath(), + }) if err != nil { - return Launcher{}, err + logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) } - return Launcher{logger: logger, cfg: cfg, nodeUpgradeWatcher: fw}, nil + nodeUpgradeWatcher := initWatcher[upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoFilePath()) + manualUpgradesWatcher := initWatcher[[]upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoBatchFilePath()) + + return Launcher{ + logger: logger, + cfg: cfg, + ctx: ctx, + cancel: cancel, + nodeUpgradeWatcher: nodeUpgradeWatcher, + manualUpgradesWatcher: manualUpgradesWatcher, + }, nil } // BatchUpgradeWatcher starts a watcher loop that swaps upgrade manifests at the correct @@ -51,7 +69,7 @@ func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { // via the websocket API. func BatchUpgradeWatcher(ctx context.Context, cfg *Config, logger log.Logger) { // load batch file in memory - uInfos, err := readManualUpgrades(cfg) + uInfos, err := ReadManualUpgrades(cfg) if err != nil { logger.Warn("failed to load batch upgrade file", "error", err) uInfos = []upgradetypes.Plan{} diff --git a/tools/cosmovisor/scanner.go b/tools/cosmovisor/scanner.go index e435c3858163..90550e43817d 100644 --- a/tools/cosmovisor/scanner.go +++ b/tools/cosmovisor/scanner.go @@ -1,6 +1,7 @@ package cosmovisor import ( + "context" "encoding/json" "errors" "fmt" @@ -15,11 +16,22 @@ import ( dbm "github.com/cometbft/cometbft-db" "github.com/cometbft/cometbft/store" + "cosmossdk.io/tools/cosmovisor/internal/watchers" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) var errUntestAble = errors.New("untestable") +func initWatcher[T any](ctx context.Context, cfg *Config, dirWatcher *watchers.FSNotifyWatcher, filename string) watchers.Watcher[T] { + if dirWatcher != nil { + hybridWatcher := watchers.NewHybridWatcher(ctx, dirWatcher, filename, cfg.PollInterval) + return watchers.NewDataWatcher[T](ctx, hybridWatcher) + } else { + pollWatcher := watchers.NewPollWatcher(ctx, filename, cfg.PollInterval) + return watchers.NewDataWatcher[T](ctx, pollWatcher) + } +} + type fileWatcher struct { daemonHome string filename string // full path to a watched file diff --git a/tools/cosmovisor/watcher.go b/tools/cosmovisor/watcher.go deleted file mode 100644 index 6fe7bfbae713..000000000000 --- a/tools/cosmovisor/watcher.go +++ /dev/null @@ -1,6 +0,0 @@ -package cosmovisor - -type watcher[T any] interface { - Updated() <-chan T - Errors() <-chan error -} From fc37590c7c4a36eef4480a04fa76bd3779743436 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 27 May 2025 11:08:40 -0400 Subject: [PATCH 010/115] WIP on checkers --- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 2 +- tools/cosmovisor/cmd/mock_node/main.go | 2 + tools/cosmovisor/internal/checkers/checker.go | 5 + .../internal/checkers/http_block.go | 58 +++++++ tools/cosmovisor/internal/checkers/sniff.go | 1 + .../internal/watchers/fsnotify_watcher.go | 4 +- tools/cosmovisor/scanner.go | 157 ------------------ 7 files changed, 70 insertions(+), 159 deletions(-) create mode 100644 tools/cosmovisor/internal/checkers/checker.go create mode 100644 tools/cosmovisor/internal/checkers/http_block.go create mode 100644 tools/cosmovisor/internal/checkers/sniff.go diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 648b24bc89e7..0d6d08d734e2 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -72,7 +72,7 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade panic(fmt.Errorf("invalid manual upgrade plan: %w", err)) } - if err := cosmovisor.addManualUpgrade(cfg, plan, force); err != nil { + if err := cosmovisor.AddManualUpgrade(cfg, plan, force); err != nil { panic(fmt.Errorf("failed to add manual upgrade: %w", err)) } diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 868db0aceca2..b5f2d5bf67a0 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -10,6 +10,8 @@ import ( ) func main() { + // TODO response to GetLatestBlock, status, and write leveldb block number + cmd := &cobra.Command{ Use: "mock_node", Short: "A mock node for testing cosmovisor.", diff --git a/tools/cosmovisor/internal/checkers/checker.go b/tools/cosmovisor/internal/checkers/checker.go new file mode 100644 index 000000000000..925cd332c2ce --- /dev/null +++ b/tools/cosmovisor/internal/checkers/checker.go @@ -0,0 +1,5 @@ +package checkers + +type LatestBlockChecker interface { + GetLatestBlockHeight() (uint64, error) +} diff --git a/tools/cosmovisor/internal/checkers/http_block.go b/tools/cosmovisor/internal/checkers/http_block.go new file mode 100644 index 000000000000..b2165500478b --- /dev/null +++ b/tools/cosmovisor/internal/checkers/http_block.go @@ -0,0 +1,58 @@ +package checkers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" +) + +func NewHTTPRPCBLockChecker(url string) LatestBlockChecker { + panic("implement me") +} + +type httpRPCBlockChecker struct { + url string +} + +func (j httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { + res, err := http.Get(j.url) + if err != nil { + return 0, fmt.Errorf("failed to get latest block height: %w", err) + } + defer res.Body.Close() + + bz, err := io.ReadAll(res.Body) + if err != nil { + return 0, fmt.Errorf("failed to read latest block height: %w", err) + } + + return getHeightFromRPCBlockResponse(bz) +} + +var _ LatestBlockChecker = httpRPCBlockChecker{} + +func getHeightFromRPCBlockResponse(bz []byte) (uint64, error) { + type Header struct { + Height string `json:"height"` + } + type Block struct { + Header Header `json:"header"` + } + type Result struct { + Block Block `json:"block"` + } + type Response struct { + Result Result `json:"result"` + } + + var response Response + err := json.Unmarshal(bz, &response) + if err != nil { + return 0, fmt.Errorf("failed to unmarshal block response: %w", err) + } + + height := response.Result.Block.Header.Height + return strconv.ParseUint(height, 10, 64) +} diff --git a/tools/cosmovisor/internal/checkers/sniff.go b/tools/cosmovisor/internal/checkers/sniff.go new file mode 100644 index 000000000000..d0ae62bb397a --- /dev/null +++ b/tools/cosmovisor/internal/checkers/sniff.go @@ -0,0 +1 @@ +package checkers diff --git a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go index 71af8bad7b16..9db5cff222c1 100644 --- a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go +++ b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go @@ -38,7 +38,9 @@ func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*F go func() { // close the watcher and channels // when the goroutines exits via return's - defer watcher.Close() + defer func(watcher *fsnotify.Watcher) { + _ = watcher.Close() + }(watcher) defer close(outChan) defer close(errChan) diff --git a/tools/cosmovisor/scanner.go b/tools/cosmovisor/scanner.go index 90550e43817d..8bb43250c90a 100644 --- a/tools/cosmovisor/scanner.go +++ b/tools/cosmovisor/scanner.go @@ -48,163 +48,6 @@ type fileWatcher struct { disableRecase bool } -func newUpgradeFileWatcher(cfg *Config) (*fileWatcher, error) { - filename := cfg.UpgradeInfoFilePath() - if filename == "" { - return nil, errors.New("filename undefined") - } - - filenameAbs, err := filepath.Abs(filename) - if err != nil { - return nil, fmt.Errorf("invalid path: %s must be a valid file path: %w", filename, err) - } - - dirname := filepath.Dir(filename) - if info, err := os.Stat(dirname); err != nil || !info.IsDir() { - return nil, fmt.Errorf("invalid path: %s must be an existing directory: %w", dirname, err) - } - - bin, err := cfg.CurrentBin() - if err != nil { - return nil, fmt.Errorf("error creating symlink to genesis: %w", err) - } - - return &fileWatcher{ - daemonHome: cfg.Home, - currentBin: bin, - filename: filenameAbs, - interval: cfg.PollInterval, - currentInfo: upgradetypes.Plan{}, - lastModTime: time.Time{}, - cancel: make(chan bool), - ticker: time.NewTicker(cfg.PollInterval), - needsUpdate: false, - initialized: false, - disableRecase: cfg.DisableRecase, - }, nil -} - -func (fw *fileWatcher) Stop() { - close(fw.cancel) - fw.ticker.Stop() -} - -func (fw *fileWatcher) IsStop() bool { - select { - case <-fw.cancel: - return true - default: - return false - } -} - -// MonitorUpdate pools the filesystem to check for new upgrade currentInfo. -// currentName is the name of currently running upgrade. The check is rejected if it finds -// an upgrade with the same name. -func (fw *fileWatcher) MonitorUpdate(currentUpgrade upgradetypes.Plan) <-chan struct{} { - fw.ticker.Reset(fw.interval) - done := make(chan struct{}) - fw.cancel = make(chan bool) - fw.needsUpdate = false - - go func() { - for { - select { - case <-fw.ticker.C: - if fw.CheckUpdate(currentUpgrade) { - done <- struct{}{} - return - } - - case <-fw.cancel: - return - } - } - }() - - return done -} - -// CheckUpdate reads update plan from file and checks if there is a new update request -// currentName is the name of currently running upgrade. The check is rejected if it finds -// an upgrade with the same name. -func (fw *fileWatcher) CheckUpdate(currentUpgrade upgradetypes.Plan) bool { - if fw.needsUpdate { - return true - } - - stat, err := os.Stat(fw.filename) - if err != nil { - if os.IsNotExist(err) { - return false - } else { - panic(fmt.Errorf("failed to stat upgrade info file: %w", err)) - } - } - - // check https://github.com/cosmos/cosmos-sdk/issues/21086 - // If new file is still empty, wait a small amount of time for write to complete - if stat.Size() == 0 { - for range 10 { - time.Sleep(2 * time.Millisecond) - stat, err = os.Stat(fw.filename) - if err != nil { - if os.IsNotExist(err) { - return false - } else { - panic(fmt.Errorf("failed to stat upgrade info file: %w", err)) - } - } - if stat.Size() == 0 { - break - } - } - } - if stat.Size() == 0 { - return false - } - - // no update if the file already exists and has not been modified - if !stat.ModTime().After(fw.lastModTime) { - return false - } - - info, err := parseUpgradeInfoFile(fw.filename, fw.disableRecase) - if err != nil { - panic(fmt.Errorf("failed to parse upgrade info file: %w", err)) - } - - // file exist but too early in height - currentHeight, err := fw.checkHeight() - if (err != nil || currentHeight < info.Height) && !errors.Is(err, errUntestAble) { // ignore this check for tests - return false - } - - if !fw.initialized { - // daemon has restarted - fw.initialized = true - fw.currentInfo = info - fw.lastModTime = stat.ModTime() - - // Heuristic: Daemon has restarted, so we don't know if we successfully - // downloaded the upgrade or not. So we try to compare the running upgrade - // name (read from the cosmovisor file) with the upgrade info. - if !strings.EqualFold(currentUpgrade.Name, fw.currentInfo.Name) { - fw.needsUpdate = true - return true - } - } - - if info.Height > fw.currentInfo.Height { - fw.currentInfo = info - fw.lastModTime = stat.ModTime() - fw.needsUpdate = true - return true - } - - return false -} - // checkHeight checks if the current block height func (fw *fileWatcher) checkHeight() (int64, error) { if testing.Testing() { // we cannot test the command in the test environment From 14124eab895c67737ce3f2f72d991f387cbcca66 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 28 May 2025 12:02:17 -0400 Subject: [PATCH 011/115] WIP --- tools/cosmovisor/cmd/mock_node/main.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index b5f2d5bf67a0..afff8a8929c5 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -1,7 +1,11 @@ package main import ( + "context" "fmt" + "net/http" + "os" + "os/signal" "time" "github.com/spf13/cobra" @@ -48,3 +52,15 @@ x/upgrade upgrade-info.json behavior.`, panic(err) } } + +type MockNode struct { + height uint64 +} + +func (m *MockNode) Run(ctx context.Context) error { + ctx, _ = signal.NotifyContext(ctx, os.Interrupt, os.Kill) + http.HandleFunc("/block", func(w http.ResponseWriter, r *http.Request) { + panic(fmt.Errorf("not implemented")) + }) + return nil +} From 7177cbb6ea71f0f8404a567e335a6605ec4e2cbc Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 28 May 2025 13:52:23 -0400 Subject: [PATCH 012/115] working mock node --- tools/cosmovisor/cmd/mock_node/main.go | 145 ++++++++++++++++-- tools/cosmovisor/go.mod | 4 +- tools/cosmovisor/go.sum | 28 ++++ .../internal/checkers/http_block.go | 29 ++-- 4 files changed, 177 insertions(+), 29 deletions(-) diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index afff8a8929c5..256af7a33426 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -1,16 +1,24 @@ package main import ( + "bytes" "context" + "encoding/json" + "errors" "fmt" "net/http" "os" "os/signal" + "path" "time" + "cosmossdk.io/log" + "github.com/cosmos/gogoproto/jsonpb" "github.com/spf13/cobra" + "cosmossdk.io/tools/cosmovisor/internal/checkers" "github.com/cosmos/cosmos-sdk/server" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) func main() { @@ -24,12 +32,20 @@ The --upgrade-plan and --halt-height flags are mutually exclusive. It is an erro Based on which flag is specified the node will either exhibit --halt-height before or x/upgrade upgrade-info.json behavior.`, } - var haltInterval time.Duration + var startHeight uint64 + var blockTime time.Duration var upgradePlan string var haltHeight uint64 - cmd.Flags().DurationVar(&haltInterval, "halt-interval", 0, "Interval to wait before halting the node") + var homePath string + var httpAddr string + var blockUrl string + cmd.Flags().Uint64Var(&startHeight, "start-height", 1, "Block height at which to start the mock node.") + cmd.Flags().DurationVar(&blockTime, "block-time", 0, "Duration of time between blocks. This is required to simulate a progression of blocks over time.") cmd.Flags().StringVar(&upgradePlan, "upgrade-plan", "", "upgrade-info.json to create after the halt duration is reached. Either this flag or --halt-height must be specified but not both.") cmd.Flags().Uint64Var(&haltHeight, server.FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node. E") + cmd.Flags().StringVar(&homePath, "home", "", "Home directory for the mock node. upgrade-info.json will be written to the data sub-directory of this directory. Defaults to the current directory.") + cmd.Flags().StringVar(&httpAddr, "http-addr", ":8080", "HTTP server address to serve block information. Defaults to :8080.") + cmd.Flags().StringVar(&blockUrl, "block-url", "/block", "URL at which the latest block information is served. Defaults to /block.") cmd.RunE = func(cmd *cobra.Command, args []string) error { if upgradePlan != "" && haltHeight > 0 { return fmt.Errorf("cannot specify both --upgrade-plan and --halt-height") @@ -37,16 +53,43 @@ x/upgrade upgrade-info.json behavior.`, if upgradePlan == "" && haltHeight == 0 { return fmt.Errorf("must specify either --upgrade-plan or --halt-height") } - if haltInterval == 0 { - return fmt.Errorf("must specify --halt-interval") + if blockTime == 0 { + return fmt.Errorf("must specify --block-time") } - time.Sleep(haltInterval) - if haltHeight > 0 { - panic(fmt.Errorf("halt per configuration height %d time %d", haltHeight, 0)) - } else if upgradePlan != "" { - panic("upgrade-info.json not implemented") + if homePath == "" { + var err error + homePath, err = os.Getwd() // Default to current working directory if not specified + if err != nil { + return fmt.Errorf("unable to determine current working directory: %w", err) + } } - return nil + node := &MockNode{ + height: startHeight, + blockTime: blockTime, + haltHeight: haltHeight, + homePath: homePath, + httpAddr: httpAddr, + blockUrl: blockUrl, + logger: log.NewLogger(os.Stdout), + } + if upgradePlan != "" { + node.upgradePlan = &upgradetypes.Plan{} + err := jsonpb.Unmarshal(bytes.NewBufferString(upgradePlan), node.upgradePlan) + if err != nil { + return fmt.Errorf("unable to parse upgrade plan: %w", err) + } + if err := node.upgradePlan.ValidateBasic(); err != nil { + return fmt.Errorf("invalid upgrade plan: %w", err) + } + if node.upgradePlan.Height < int64(startHeight) { + return fmt.Errorf("upgrade plan height %d must be greater than or equal to start height %d", node.upgradePlan.Height, startHeight) + } + } else { + if haltHeight < startHeight { + return fmt.Errorf("halt height %d must be greater than or equal to start height %d", haltHeight, startHeight) + } + } + return node.Run(cmd.Context()) } if err := cmd.Execute(); err != nil { panic(err) @@ -54,13 +97,85 @@ x/upgrade upgrade-info.json behavior.`, } type MockNode struct { - height uint64 + height uint64 + blockTime time.Duration + upgradePlan *upgradetypes.Plan + haltHeight uint64 + homePath string + httpAddr string + blockUrl string + logger log.Logger } -func (m *MockNode) Run(ctx context.Context) error { +func (n *MockNode) Run(ctx context.Context) error { ctx, _ = signal.NotifyContext(ctx, os.Interrupt, os.Kill) - http.HandleFunc("/block", func(w http.ResponseWriter, r *http.Request) { - panic(fmt.Errorf("not implemented")) - }) + upgradeHeight := n.haltHeight + if upgradeHeight == 0 { + upgradeHeight = uint64(n.upgradePlan.Height) + } + + n.logger.Info("Starting mock node", "start_height", n.height, "block_time", n.blockTime, "upgrade_plan", n.upgradePlan, "halt_height", upgradeHeight) + srv := n.startHTTPServer() + for n.height < upgradeHeight { + n.logger.Info("Processed mock block", "height", n.height) + timer := time.NewTimer(n.blockTime) + select { + case <-ctx.Done(): + n.logger.Info("Received shutdown signal, stopping node") + timer.Stop() + if err := srv.Shutdown(ctx); err != nil { + n.logger.Error("Error shutting down HTTP server", "err", err) + } + return nil + case <-timer.C: + n.height++ + } + } + if n.haltHeight > 0 { + n.logger.Error(fmt.Sprintf("halt per configuration height %d", n.height)) + } else { + n.logger.Info("Mock node reached upgrade height, writing upgrade-info.json", "upgrade_plan", n.upgradePlan) + upgradeInfoPath := path.Join(n.homePath, "data", upgradetypes.UpgradeInfoFilename) + out, err := (&jsonpb.Marshaler{}).MarshalToString(n.upgradePlan) + if err != nil { + return fmt.Errorf("failed to marshal upgrade plan: %w", err) + } + err = os.MkdirAll(path.Dir(upgradeInfoPath), 0o755) + if err != nil { + return fmt.Errorf("failed to create directory for upgrade-info.json: %w", err) + } + err = os.WriteFile(upgradeInfoPath, []byte(out), 0o644) + if err != nil { + return fmt.Errorf("failed to write upgrade-info.json: %w", err) + } + } return nil } + +func (n *MockNode) startHTTPServer() *http.Server { + http.HandleFunc(n.blockUrl, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(checkers.Response{ + Result: checkers.Result{ + Block: checkers.Block{ + Header: checkers.Header{ + Height: fmt.Sprintf("%d", n.height), + }, + }, + }, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + srv := &http.Server{ + Addr: n.httpAddr, + } + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + n.logger.Error("HTTP server error", "err", err) + } + }() + return srv +} diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index 4bf07208ec01..c34fa1bb2c52 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -7,6 +7,7 @@ require ( github.com/cometbft/cometbft v1.0.1 github.com/cometbft/cometbft-db v1.0.4 github.com/cosmos/cosmos-sdk v0.53.0 + github.com/cosmos/gogoproto v1.7.0 github.com/fsnotify/fsnotify v1.9.0 github.com/otiai10/copy v1.14.1 github.com/pelletier/go-toml/v2 v2.2.4 @@ -65,7 +66,6 @@ require ( github.com/cosmos/cosmos-proto v1.0.0-beta.5 // 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 github.com/cosmos/iavl v1.2.2 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect @@ -131,9 +131,11 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/linxGnu/grocksdb v1.9.8 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/highwayhash v1.0.3 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mtibben/percent v0.2.1 // indirect diff --git a/tools/cosmovisor/go.sum b/tools/cosmovisor/go.sum index 36ce0a0f843c..efcb15bc3a9c 100644 --- a/tools/cosmovisor/go.sum +++ b/tools/cosmovisor/go.sum @@ -635,6 +635,8 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -654,10 +656,14 @@ github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/adlio/schema v1.3.6 h1:k1/zc2jNfeiZBA5aFTRy37jlBIuCkXCm0XmvpzCKI9I= +github.com/adlio/schema v1.3.6/go.mod h1:qkxwLgPBd1FgLRHYVCmQT/rrBr3JH38J9LjmVzWNudg= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= @@ -712,6 +718,7 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4 github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -752,6 +759,8 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= +github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= @@ -774,6 +783,8 @@ github.com/cometbft/cometbft-db v1.0.4 h1:cezb8yx/ZWcF124wqUtAFjAuDksS1y1yXedvtp github.com/cometbft/cometbft-db v1.0.4/go.mod h1:M+BtHAGU2XLrpUxo3Nn1nOCcnVCiLM9yx5OuT0u5SCA= github.com/cometbft/cometbft/api v1.0.0 h1:gGBwvsJi/gnHJEtwYfjPIGs2AKg/Vfa1ZuKCPD1/Ko4= github.com/cometbft/cometbft/api v1.0.0/go.mod h1:EkQiqVSu/p2ebrZEnB2z6Re7r8XNe//M7ylR0qEwWm0= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -820,6 +831,10 @@ github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4Typ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -1212,6 +1227,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -1307,6 +1324,12 @@ github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9 github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= +github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1315,6 +1338,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= +github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= @@ -1420,6 +1445,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -1879,6 +1906,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/tools/cosmovisor/internal/checkers/http_block.go b/tools/cosmovisor/internal/checkers/http_block.go index b2165500478b..087ac17dd3e9 100644 --- a/tools/cosmovisor/internal/checkers/http_block.go +++ b/tools/cosmovisor/internal/checkers/http_block.go @@ -9,7 +9,9 @@ import ( ) func NewHTTPRPCBLockChecker(url string) LatestBlockChecker { - panic("implement me") + return httpRPCBlockChecker{ + url: url, + } } type httpRPCBlockChecker struct { @@ -33,19 +35,20 @@ func (j httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { var _ LatestBlockChecker = httpRPCBlockChecker{} +type Header struct { + Height string `json:"height"` +} +type Block struct { + Header Header `json:"header"` +} +type Result struct { + Block Block `json:"block"` +} +type Response struct { + Result Result `json:"result"` +} + func getHeightFromRPCBlockResponse(bz []byte) (uint64, error) { - type Header struct { - Height string `json:"height"` - } - type Block struct { - Header Header `json:"header"` - } - type Result struct { - Block Block `json:"block"` - } - type Response struct { - Result Result `json:"result"` - } var response Response err := json.Unmarshal(bz, &response) From 37f7c9ff02a2c4941759fc09542c1c5199cfd109 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 28 May 2025 13:52:37 -0400 Subject: [PATCH 013/115] switch to jsonpb --- x/upgrade/keeper/keeper.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x/upgrade/keeper/keeper.go b/x/upgrade/keeper/keeper.go index 1b996c1a18d4..b8b424aba1d5 100644 --- a/x/upgrade/keeper/keeper.go +++ b/x/upgrade/keeper/keeper.go @@ -13,9 +13,10 @@ import ( "github.com/hashicorp/go-metrics" - corestore "cosmossdk.io/core/store" errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" + + corestore "cosmossdk.io/core/store" "cosmossdk.io/store/prefix" storetypes "cosmossdk.io/store/types" @@ -40,7 +41,7 @@ type Keeper struct { homePath string // root directory of app config skipUpgradeHeights map[int64]bool // map of heights to skip for an upgrade storeService corestore.KVStoreService // key to access x/upgrade store - cdc codec.BinaryCodec // App-wide binary codec + cdc codec.Codec // App-wide binary codec upgradeHandlers map[string]types.UpgradeHandler // map of plan name to upgrade handler versionSetter xp.ProtocolVersionSetter // implements setting the protocol version field on BaseApp downgradeVerified bool // tells if we've already sanity checked that this binary version isn't being used against an old state. @@ -54,7 +55,7 @@ type Keeper struct { // cdc - the app-wide binary codec // homePath - root directory of the application's config // vs - the interface implemented by baseapp which allows setting baseapp's protocol version field -func NewKeeper(skipUpgradeHeights map[int64]bool, storeService corestore.KVStoreService, cdc codec.BinaryCodec, homePath string, vs xp.ProtocolVersionSetter, authority string) *Keeper { +func NewKeeper(skipUpgradeHeights map[int64]bool, storeService corestore.KVStoreService, cdc codec.Codec, homePath string, vs xp.ProtocolVersionSetter, authority string) *Keeper { k := &Keeper{ homePath: homePath, skipUpgradeHeights: skipUpgradeHeights, @@ -535,7 +536,7 @@ func (k Keeper) DumpUpgradeInfoToDisk(height int64, p types.Plan) error { Height: height, Info: p.Info, } - info, err := json.Marshal(upgradeInfo) + info, err := k.cdc.MarshalJSON(&upgradeInfo) if err != nil { return err } From 5a01495d84a1baab23e8db8be4a5313868a0537e Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 28 May 2025 15:50:20 -0400 Subject: [PATCH 014/115] WIP on refactoring --- tools/cosmovisor/args.go | 30 +- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 3 +- tools/cosmovisor/cmd/mock_node/main.go | 6 +- .../internal/watchers/data_watcher.go | 5 +- tools/cosmovisor/manual.go | 39 +- tools/cosmovisor/pre_upgrade.go | 103 ++++ tools/cosmovisor/process.go | 465 ++++++------------ tools/cosmovisor/skip.go | 47 ++ 8 files changed, 350 insertions(+), 348 deletions(-) create mode 100644 tools/cosmovisor/pre_upgrade.go create mode 100644 tools/cosmovisor/skip.go diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index 4e529459c50b..72bf3fe2076c 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -1,7 +1,7 @@ package cosmovisor import ( - "encoding/json" + "bytes" "errors" "fmt" "io" @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/cosmos/gogoproto/jsonpb" "github.com/pelletier/go-toml/v2" "github.com/spf13/viper" @@ -71,9 +72,6 @@ type Config struct { TimeFormatLogs string `toml:"cosmovisor_timeformat_logs" mapstructure:"cosmovisor_timeformat_logs" default:"kitchen"` CustomPreUpgrade string `toml:"cosmovisor_custom_preupgrade" mapstructure:"cosmovisor_custom_preupgrade" default:""` DisableRecase bool `toml:"cosmovisor_disable_recase" mapstructure:"cosmovisor_disable_recase" default:"false"` - - // currently running upgrade - currentUpgrade upgradetypes.Plan } // Root returns the root directory where all info lives @@ -413,7 +411,6 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) { return fmt.Errorf("creating current symlink: %w", err) } - cfg.currentUpgrade = u f, err := os.Create(filepath.Join(cfg.Root(), upgrade, upgradetypes.UpgradeInfoFilename)) if err != nil { return err @@ -425,39 +422,30 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) { } }() - bz, err := json.Marshal(u) + out, err := (&jsonpb.Marshaler{}).MarshalToString(&u) if err != nil { return err } - _, err = f.Write(bz) + _, err = f.Write([]byte(out)) return err } // UpgradeInfo returns the current upgrade info func (cfg *Config) UpgradeInfo() (upgradetypes.Plan, error) { - if cfg.currentUpgrade.Name != "" { - return cfg.currentUpgrade, nil - } - filename := filepath.Join(cfg.Root(), currentLink, upgradetypes.UpgradeInfoFilename) _, err := os.Lstat(filename) var u upgradetypes.Plan var bz []byte if err != nil { // no current directory - goto returnError + return upgradetypes.Plan{}, fmt.Errorf("failed to read %q: %w", filename, err) } if bz, err = os.ReadFile(filename); err != nil { - goto returnError + return upgradetypes.Plan{}, fmt.Errorf("failed to read %q: %w", filename, err) } - if err = json.Unmarshal(bz, &u); err != nil { - goto returnError + if err = jsonpb.Unmarshal(bytes.NewReader(bz), &u); err != nil { + return upgradetypes.Plan{}, fmt.Errorf("error unmarshalling %q: %w", filename, err) } - cfg.currentUpgrade = u - return cfg.currentUpgrade, nil - -returnError: - cfg.currentUpgrade.Name = "_" - return cfg.currentUpgrade, fmt.Errorf("failed to read %q: %w", filename, err) + return u, nil } // BooleanOption checks and validate env option diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 0d6d08d734e2..4ea62544bea4 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -9,7 +9,6 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) func NewAddUpgradeCmd() *cobra.Command { @@ -64,7 +63,7 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) if upgradeHeight > 0 { - plan := upgradetypes.Plan{ + plan := cosmovisor.ManualUpgradePlan{ Name: upgradeName, Height: upgradeHeight, } diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 256af7a33426..f502e07a88c1 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -116,18 +116,18 @@ func (n *MockNode) Run(ctx context.Context) error { n.logger.Info("Starting mock node", "start_height", n.height, "block_time", n.blockTime, "upgrade_plan", n.upgradePlan, "halt_height", upgradeHeight) srv := n.startHTTPServer() + ticker := time.NewTicker(n.blockTime) + defer ticker.Stop() for n.height < upgradeHeight { n.logger.Info("Processed mock block", "height", n.height) - timer := time.NewTimer(n.blockTime) select { case <-ctx.Done(): n.logger.Info("Received shutdown signal, stopping node") - timer.Stop() if err := srv.Shutdown(ctx); err != nil { n.logger.Error("Error shutting down HTTP server", "err", err) } return nil - case <-timer.C: + case <-ticker.C: n.height++ } } diff --git a/tools/cosmovisor/internal/watchers/data_watcher.go b/tools/cosmovisor/internal/watchers/data_watcher.go index d3886de71c2a..5ae2b10e0f7f 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher.go +++ b/tools/cosmovisor/internal/watchers/data_watcher.go @@ -2,7 +2,6 @@ package watchers import ( "context" - "encoding/json" ) type DataWatcher[T any] struct { @@ -10,7 +9,7 @@ type DataWatcher[T any] struct { errChan chan error } -func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte]) *DataWatcher[T] { +func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte], unmarshal func([]byte) (T, error)) *DataWatcher[T] { outChan := make(chan T, 1) errChan := make(chan error, 1) go func() { @@ -25,7 +24,7 @@ func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte]) *DataWa return } var data T - err := json.Unmarshal(contents, &data) + data, err := unmarshal(contents) // ignore errors because failing JSON unmarshal probably just means the file is incomplete if err == nil { outChan <- data diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 046fb6b7abec..2cfa5a9ccf33 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -5,12 +5,10 @@ import ( "fmt" "os" "sort" - - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) // ReadManualUpgrades reads the manual upgrade data. -func ReadManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { +func ReadManualUpgrades(cfg *Config) (ManualUpgradeBatch, error) { bz, err := os.ReadFile(cfg.UpgradeInfoBatchFilePath()) if err != nil { if os.IsNotExist(err) { @@ -18,7 +16,7 @@ func ReadManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { } return nil, err } - var manualUpgrades []upgradetypes.Plan + var manualUpgrades ManualUpgradeBatch if err := json.Unmarshal(bz, &manualUpgrades); err != nil { return nil, err } @@ -30,13 +28,13 @@ func ReadManualUpgrades(cfg *Config) ([]upgradetypes.Plan, error) { // AddManualUpgrade adds a manual upgrade plan. // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. -func AddManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) error { +func AddManualUpgrade(cfg *Config, plan ManualUpgradePlan, forceOverwrite bool) error { manualUpgrades, err := ReadManualUpgrades(cfg) if err != nil { return err } - var newUpgrades []upgradetypes.Plan + var newUpgrades ManualUpgradeBatch for _, existing := range manualUpgrades { if existing.Name == plan.Name { if !forceOverwrite { @@ -58,8 +56,35 @@ func AddManualUpgrade(cfg *Config, plan upgradetypes.Plan, forceOverwrite bool) return os.WriteFile(cfg.UpgradeInfoBatchFilePath(), manualUpgradesData, 0644) } -func sortUpgrades(upgrades []upgradetypes.Plan) { +func sortUpgrades(upgrades []ManualUpgradePlan) { sort.Slice(upgrades, func(i, j int) bool { return upgrades[i].Height < upgrades[j].Height }) } + +type ManualUpgradeBatch []ManualUpgradePlan + +func (m ManualUpgradeBatch) ValidateBasic() error { + for _, upgrade := range m { + if err := upgrade.ValidateBasic(); err != nil { + return fmt.Errorf("invalid upgrade plan %s: %w", upgrade.Name, err) + } + } + return nil +} + +type ManualUpgradePlan struct { + Name string `json:"name"` + Height int64 `json:"height"` + Info string `json:"info"` +} + +func (m ManualUpgradePlan) ValidateBasic() error { + if m.Name == "" { + return fmt.Errorf("name cannot be empty") + } + if m.Height <= 0 { + return fmt.Errorf("height must be greater than 0") + } + return nil +} diff --git a/tools/cosmovisor/pre_upgrade.go b/tools/cosmovisor/pre_upgrade.go new file mode 100644 index 000000000000..741eb06c22cf --- /dev/null +++ b/tools/cosmovisor/pre_upgrade.go @@ -0,0 +1,103 @@ +package cosmovisor + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// doCustomPreUpgrade executes the custom preupgrade script if provided. +func (l Launcher) doCustomPreUpgrade() error { + if l.cfg.CustomPreUpgrade == "" { + return nil + } + + // check if preupgradeFile is executable file + preupgradeFile := filepath.Join(l.cfg.Home, "cosmovisor", l.cfg.CustomPreUpgrade) + l.logger.Info("looking for COSMOVISOR_CUSTOM_PREUPGRADE file", "file", preupgradeFile) + info, err := os.Stat(preupgradeFile) + if err != nil { + l.logger.Error("COSMOVISOR_CUSTOM_PREUPGRADE file missing", "file", preupgradeFile) + return err + } + if !info.Mode().IsRegular() { + _, f := filepath.Split(preupgradeFile) + return fmt.Errorf("COSMOVISOR_CUSTOM_PREUPGRADE: %s is not a regular file", f) + } + + // Set the execute bit for only the current user + // Given: Current user - Group - Everyone + // 0o RWX - RWX - RWX + oldMode := info.Mode().Perm() + newMode := oldMode | 0o100 + if oldMode != newMode { + if err := os.Chmod(preupgradeFile, newMode); err != nil { + l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") + return errors.New("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") + } + } + + // Run preupgradeFile + cmd := exec.Command(preupgradeFile, l.upgradePlan.Name, fmt.Sprintf("%d", l.upgradePlan.Height)) + cmd.Dir = l.cfg.Home + result, err := cmd.Output() + if err != nil { + return err + } + + l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE result", "command", preupgradeFile, "argv1", l.upgradePlan.Name, "argv2", fmt.Sprintf("%d", l.upgradePlan.Height), "result", result) + + return nil +} + +// doPreUpgrade runs the pre-upgrade command defined by the application and handles respective error codes. +// cfg contains the cosmovisor config from env var. +// doPreUpgrade runs the new APP binary in order to process the upgrade (post-upgrade for cosmovisor). +func (l *Launcher) doPreUpgrade() error { + counter := 0 + for { + if counter > l.cfg.PreUpgradeMaxRetries { + return fmt.Errorf("pre-upgrade command failed. reached max attempt of retries - %d", l.cfg.PreUpgradeMaxRetries) + } + + if err := l.executePreUpgradeCmd(); err != nil { + counter++ + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + switch exitErr.ExitCode() { + case 1: + l.logger.Info("pre-upgrade command does not exist. continuing the upgrade.") + return nil + case 30: + return fmt.Errorf("pre-upgrade command failed : %w", err) + case 31: + l.logger.Error("pre-upgrade command failed. retrying", "error", err, "attempt", counter) + continue + } + } + } + + l.logger.Info("pre-upgrade successful. continuing the upgrade.") + return nil + } +} + +// executePreUpgradeCmd runs the pre-upgrade command defined by the application +// cfg contains the cosmovisor config from the env vars +func (l *Launcher) executePreUpgradeCmd() error { + bin, err := l.cfg.CurrentBin() + if err != nil { + return fmt.Errorf("error while getting current binary path: %w", err) + } + + result, err := exec.Command(bin, "pre-upgrade").Output() + if err != nil { + return err + } + + l.logger.Info("pre-upgrade result", "result", result) + return nil +} diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index fde9b34cad9b..62fbc9230878 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -10,21 +10,14 @@ import ( "os/exec" "os/signal" "path/filepath" - "strconv" "strings" - "sync" "syscall" "time" - "github.com/fsnotify/fsnotify" - "github.com/otiai10/copy" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "cosmossdk.io/log" + "github.com/otiai10/copy" "cosmossdk.io/tools/cosmovisor/internal/watchers" - "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -38,11 +31,11 @@ type Launcher struct { nodeUpgradeWatcher watchers.Watcher[upgradetypes.Plan] // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator manualUpgradesWatcher watchers.Watcher[[]upgradetypes.Plan] + upgradePlan *upgradetypes.Plan } func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { - ctx, cancel := context.WithCancel(context.Background()) - + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGQUIT, syscall.SIGTERM) dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, cfg.UpgradeInfoDir(), []string{ cfg.UpgradeInfoFilePath(), cfg.UpgradeInfoBatchFilePath(), @@ -51,6 +44,7 @@ func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) } + // TODO the watchers should do data validation in additional to json unmarshaling nodeUpgradeWatcher := initWatcher[upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoFilePath()) manualUpgradesWatcher := initWatcher[[]upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoBatchFilePath()) @@ -64,123 +58,123 @@ func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { }, nil } -// BatchUpgradeWatcher starts a watcher loop that swaps upgrade manifests at the correct -// height, given the batch upgrade file. It watches the current state of the chain -// via the websocket API. -func BatchUpgradeWatcher(ctx context.Context, cfg *Config, logger log.Logger) { - // load batch file in memory - uInfos, err := ReadManualUpgrades(cfg) - if err != nil { - logger.Warn("failed to load batch upgrade file", "error", err) - uInfos = []upgradetypes.Plan{} - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - logger.Warn("failed to init watcher", "error", err) - return - } - defer watcher.Close() - err = watcher.Add(filepath.Dir(cfg.UpgradeInfoBatchFilePath())) - if err != nil { - logger.Warn("watcher failed to add upgrade directory", "error", err) - return - } - - var conn *grpc.ClientConn - var grpcErr error - - defer func() { - if conn != nil { - if err := conn.Close(); err != nil { - logger.Warn("couldn't stop gRPC client", "error", err) - } - } - }() - - // Wait for the chain process to be ready -pollLoop: - for { - select { - case <-ctx.Done(): - return - default: - conn, grpcErr = grpc.NewClient(cfg.GRPCAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) - if grpcErr == nil { - break pollLoop - } - time.Sleep(time.Second) - } - } - - client := cmtservice.NewServiceClient(conn) - - var prevUpgradeHeight int64 = -1 - - logger.Info("starting the batch watcher loop") - for { - select { - case event := <-watcher.Events: - if event.Op&(fsnotify.Write|fsnotify.Create) != 0 { - uInfos, err = loadBatchUpgradeFile(cfg) - if err != nil { - logger.Warn("failed to load batch upgrade file", "error", err) - continue - } - } - case <-ctx.Done(): - return - default: - if len(uInfos) == 0 { - // prevent spending extra CPU cycles - time.Sleep(time.Second) - continue - } - resp, err := client.GetLatestBlock(ctx, &cmtservice.GetLatestBlockRequest{}) - if err != nil { - logger.Warn("error getting latest block", "error", err) - time.Sleep(time.Second) - continue - } - - h := resp.SdkBlock.Header.Height - upcomingUpgrade := uInfos[0].Height - // replace upgrade-info and upgrade-info batch file - if h > prevUpgradeHeight && h < upcomingUpgrade { - jsonBytes, err := json.Marshal(uInfos[0]) - if err != nil { - logger.Warn("error marshaling JSON for upgrade-info.json", "error", err, "upgrade", uInfos[0]) - continue - } - if err := os.WriteFile(cfg.UpgradeInfoFilePath(), jsonBytes, 0o600); err != nil { - logger.Warn("error writing upgrade-info.json", "error", err) - continue - } - uInfos = uInfos[1:] - - jsonBytes, err = json.Marshal(uInfos) - if err != nil { - logger.Warn("error marshaling JSON for upgrade-info.json.batch", "error", err, "upgrades", uInfos) - continue - } - if err := os.WriteFile(cfg.UpgradeInfoBatchFilePath(), jsonBytes, 0o600); err != nil { - logger.Warn("error writing upgrade-info.json.batch", "error", err) - // remove the upgrade-info.json.batch file to avoid non-deterministic behavior - err := os.Remove(cfg.UpgradeInfoBatchFilePath()) - if err != nil && !os.IsNotExist(err) { - logger.Warn("error removing upgrade-info.json.batch", "error", err) - return - } - continue - } - prevUpgradeHeight = upcomingUpgrade - } - - // Add a small delay to avoid hammering the gRPC endpoint - time.Sleep(time.Second) - } - } -} +//// BatchUpgradeWatcher starts a watcher loop that swaps upgrade manifests at the correct +//// height, given the batch upgrade file. It watches the current state of the chain +//// via the websocket API. +//func BatchUpgradeWatcher(ctx context.Context, cfg *Config, logger log.Logger) { +// // load batch file in memory +// uInfos, err := ReadManualUpgrades(cfg) +// if err != nil { +// logger.Warn("failed to load batch upgrade file", "error", err) +// uInfos = []upgradetypes.Plan{} +// } +// +// watcher, err := fsnotify.NewWatcher() +// if err != nil { +// logger.Warn("failed to init watcher", "error", err) +// return +// } +// defer watcher.Close() +// err = watcher.Add(filepath.Dir(cfg.UpgradeInfoBatchFilePath())) +// if err != nil { +// logger.Warn("watcher failed to add upgrade directory", "error", err) +// return +// } +// +// var conn *grpc.ClientConn +// var grpcErr error +// +// defer func() { +// if conn != nil { +// if err := conn.Close(); err != nil { +// logger.Warn("couldn't stop gRPC client", "error", err) +// } +// } +// }() +// +// // Wait for the chain process to be ready +//pollLoop: +// for { +// select { +// case <-ctx.Done(): +// return +// default: +// conn, grpcErr = grpc.NewClient(cfg.GRPCAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) +// if grpcErr == nil { +// break pollLoop +// } +// time.Sleep(time.Second) +// } +// } +// +// client := cmtservice.NewServiceClient(conn) +// +// var prevUpgradeHeight int64 = -1 +// +// logger.Info("starting the batch watcher loop") +// for { +// select { +// case event := <-watcher.Events: +// if event.Op&(fsnotify.Write|fsnotify.Create) != 0 { +// uInfos, err = loadBatchUpgradeFile(cfg) +// if err != nil { +// logger.Warn("failed to load batch upgrade file", "error", err) +// continue +// } +// } +// case <-ctx.Done(): +// return +// default: +// if len(uInfos) == 0 { +// // prevent spending extra CPU cycles +// time.Sleep(time.Second) +// continue +// } +// resp, err := client.GetLatestBlock(ctx, &cmtservice.GetLatestBlockRequest{}) +// if err != nil { +// logger.Warn("error getting latest block", "error", err) +// time.Sleep(time.Second) +// continue +// } +// +// h := resp.SdkBlock.Header.Height +// upcomingUpgrade := uInfos[0].Height +// // replace upgrade-info and upgrade-info batch file +// if h > prevUpgradeHeight && h < upcomingUpgrade { +// jsonBytes, err := json.Marshal(uInfos[0]) +// if err != nil { +// logger.Warn("error marshaling JSON for upgrade-info.json", "error", err, "upgrade", uInfos[0]) +// continue +// } +// if err := os.WriteFile(cfg.UpgradeInfoFilePath(), jsonBytes, 0o600); err != nil { +// logger.Warn("error writing upgrade-info.json", "error", err) +// continue +// } +// uInfos = uInfos[1:] +// +// jsonBytes, err = json.Marshal(uInfos) +// if err != nil { +// logger.Warn("error marshaling JSON for upgrade-info.json.batch", "error", err, "upgrades", uInfos) +// continue +// } +// if err := os.WriteFile(cfg.UpgradeInfoBatchFilePath(), jsonBytes, 0o600); err != nil { +// logger.Warn("error writing upgrade-info.json.batch", "error", err) +// // remove the upgrade-info.json.batch file to avoid non-deterministic behavior +// err := os.Remove(cfg.UpgradeInfoBatchFilePath()) +// if err != nil && !os.IsNotExist(err) { +// logger.Warn("error removing upgrade-info.json.batch", "error", err) +// return +// } +// continue +// } +// prevUpgradeHeight = upcomingUpgrade +// } +// +// // Add a small delay to avoid hammering the gRPC endpoint +// time.Sleep(time.Second) +// } +// } +//} // Run launches the app in a subprocess and returns when the subprocess (app) // exits (either when it dies, or *after* a successful upgrade.) and upgrade finished. @@ -204,32 +198,31 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) return false, fmt.Errorf("launching process %s %s failed: %w", bin, strings.Join(args, " "), err) } - ctx, cancel := context.WithCancel(context.Background()) - var wg sync.WaitGroup - wg.Add(1) - // TODO: replace BatchUpgradeWatcher - go func() { - defer wg.Done() - BatchUpgradeWatcher(ctx, l.cfg, l.logger) - }() - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGQUIT, syscall.SIGTERM) - go func() { - sig := <-sigs - cancel() - wg.Wait() - if err := cmd.Process.Signal(sig); err != nil { - l.logger.Error("terminated", "error", err, "bin", bin) - os.Exit(1) - } - }() + //var wg sync.WaitGroup + //wg.Add(1) + //// TODO: replace BatchUpgradeWatcher + //go func() { + // defer wg.Done() + // BatchUpgradeWatcher(ctx, l.cfg, l.logger) + //}() + + //sigs := make(chan os.Signal, 1) + //signal.Notify(sigs, syscall.SIGQUIT, syscall.SIGTERM) + //go func() { + // sig := <-sigs + // cancel() + // wg.Wait() + // if err := cmd.Process.Signal(sig); err != nil { + // l.logger.Error("terminated", "error", err, "bin", bin) + // os.Exit(1) + // } + //}() if needsUpdate, err := l.WaitForUpgradeOrExit(cmd); err != nil || !needsUpdate { return false, err } - if !IsSkipUpgradeHeight(args, l.nodeUpgradeWatcher.currentInfo) { + if !IsSkipUpgradeHeight(args, *l.upgradePlan) { l.cfg.WaitRestartDelay() if err := l.doBackup(); err != nil { @@ -240,7 +233,7 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) return false, err } - if err := UpgradeBinary(l.logger, l.cfg, l.nodeUpgradeWatcher.currentInfo); err != nil { + if err := UpgradeBinary(l.logger, l.cfg, *l.upgradePlan); err != nil { return false, err } @@ -251,8 +244,8 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) return true, nil } - cancel() - wg.Wait() + //cancel() + //wg.Wait() return false, nil } @@ -265,13 +258,13 @@ func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) // It returns (false, nil) if the process exited normally without triggering an upgrade. This is very unlikely // to happen with "start" but may happen with short-lived commands like `simd genesis export ...` func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { - // TODO we shouldn't be getting any current upgrade because we're only using upgrade-info.json to receive signals from the node - currentUpgrade, err := l.cfg.UpgradeInfo() - if err != nil { - // upgrade info not found do nothing - currentUpgrade = upgradetypes.Plan{} - } - + //// TODO we shouldn't be getting any current upgrade because we're only using upgrade-info.json to receive signals from the node + //currentUpgrade, err := l.cfg.UpgradeInfo() + //if err != nil { + // // upgrade info not found do nothing + // currentUpgrade = upgradetypes.Plan{} + //} + // cmdDone := make(chan error) go func() { cmdDone <- cmd.Wait() @@ -280,7 +273,8 @@ func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { select { // TODO add manual-upgrades watcher // TODO replace with upgrade-info.json watcher - case <-l.nodeUpgradeWatcher.MonitorUpdate(currentUpgrade): + case upgradePlan := <-l.nodeUpgradeWatcher.Updated(): + l.upgradePlan = &upgradePlan // upgrade - kill the process and restart l.logger.Info("daemon shutting down in an attempt to restart") @@ -311,16 +305,10 @@ func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { _ = cmd.Process.Kill() } case err := <-cmdDone: - l.nodeUpgradeWatcher.Stop() // no error -> command exits normally (eg. short command like `gaiad version`) if err == nil { return false, nil } - // the app x/upgrade causes a panic and the app can die before the filwatcher finds the - // update, so we need to recheck update-info file. - if !l.nodeUpgradeWatcher.CheckUpdate(currentUpgrade) { - return false, err - } } return true, nil } @@ -362,150 +350,3 @@ func (l Launcher) doBackup() error { return nil } - -// doCustomPreUpgrade executes the custom preupgrade script if provided. -func (l Launcher) doCustomPreUpgrade() error { - if l.cfg.CustomPreUpgrade == "" { - return nil - } - - // check if upgrade-info.json is not empty. - var upgradePlan upgradetypes.Plan - upgradeInfoFile, err := os.ReadFile(l.cfg.UpgradeInfoFilePath()) - if err != nil { - return fmt.Errorf("error while reading upgrade-info.json: %w", err) - } - - if err = json.Unmarshal(upgradeInfoFile, &upgradePlan); err != nil { - return err - } - - if err = upgradePlan.ValidateBasic(); err != nil { - return fmt.Errorf("invalid upgrade plan: %w", err) - } - - // check if preupgradeFile is executable file - preupgradeFile := filepath.Join(l.cfg.Home, "cosmovisor", l.cfg.CustomPreUpgrade) - l.logger.Info("looking for COSMOVISOR_CUSTOM_PREUPGRADE file", "file", preupgradeFile) - info, err := os.Stat(preupgradeFile) - if err != nil { - l.logger.Error("COSMOVISOR_CUSTOM_PREUPGRADE file missing", "file", preupgradeFile) - return err - } - if !info.Mode().IsRegular() { - _, f := filepath.Split(preupgradeFile) - return fmt.Errorf("COSMOVISOR_CUSTOM_PREUPGRADE: %s is not a regular file", f) - } - - // Set the execute bit for only the current user - // Given: Current user - Group - Everyone - // 0o RWX - RWX - RWX - oldMode := info.Mode().Perm() - newMode := oldMode | 0o100 - if oldMode != newMode { - if err := os.Chmod(preupgradeFile, newMode); err != nil { - l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") - return errors.New("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") - } - } - - // Run preupgradeFile - cmd := exec.Command(preupgradeFile, upgradePlan.Name, fmt.Sprintf("%d", upgradePlan.Height)) - cmd.Dir = l.cfg.Home - result, err := cmd.Output() - if err != nil { - return err - } - - l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE result", "command", preupgradeFile, "argv1", upgradePlan.Name, "argv2", fmt.Sprintf("%d", upgradePlan.Height), "result", result) - - return nil -} - -// doPreUpgrade runs the pre-upgrade command defined by the application and handles respective error codes. -// cfg contains the cosmovisor config from env var. -// doPreUpgrade runs the new APP binary in order to process the upgrade (post-upgrade for cosmovisor). -func (l *Launcher) doPreUpgrade() error { - counter := 0 - for { - if counter > l.cfg.PreUpgradeMaxRetries { - return fmt.Errorf("pre-upgrade command failed. reached max attempt of retries - %d", l.cfg.PreUpgradeMaxRetries) - } - - if err := l.executePreUpgradeCmd(); err != nil { - counter++ - - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - switch exitErr.ExitCode() { - case 1: - l.logger.Info("pre-upgrade command does not exist. continuing the upgrade.") - return nil - case 30: - return fmt.Errorf("pre-upgrade command failed : %w", err) - case 31: - l.logger.Error("pre-upgrade command failed. retrying", "error", err, "attempt", counter) - continue - } - } - } - - l.logger.Info("pre-upgrade successful. continuing the upgrade.") - return nil - } -} - -// executePreUpgradeCmd runs the pre-upgrade command defined by the application -// cfg contains the cosmovisor config from the env vars -func (l *Launcher) executePreUpgradeCmd() error { - bin, err := l.cfg.CurrentBin() - if err != nil { - return fmt.Errorf("error while getting current binary path: %w", err) - } - - result, err := exec.Command(bin, "pre-upgrade").Output() - if err != nil { - return err - } - - l.logger.Info("pre-upgrade result", "result", result) - return nil -} - -// IsSkipUpgradeHeight checks if pre-upgrade script must be run. -// If the height in the upgrade plan matches any of the heights provided in --unsafe-skip-upgrades, the script is not run. -func IsSkipUpgradeHeight(args []string, upgradeInfo upgradetypes.Plan) bool { - skipUpgradeHeights := UpgradeSkipHeights(args) - for _, h := range skipUpgradeHeights { - if h == int(upgradeInfo.Height) { - return true - } - } - return false -} - -// UpgradeSkipHeights gets all the heights provided when -// simd start --unsafe-skip-upgrades ... -func UpgradeSkipHeights(args []string) []int { - var heights []int - for i, arg := range args { - if arg == fmt.Sprintf("--%s", FlagSkipUpgradeHeight) { - j := i + 1 - - for j < len(args) { - tArg := args[j] - if strings.HasPrefix(tArg, "-") { - break - } - h, err := strconv.Atoi(tArg) - if err == nil { - heights = append(heights, h) - } - j++ - } - - break - } - } - return heights -} diff --git a/tools/cosmovisor/skip.go b/tools/cosmovisor/skip.go new file mode 100644 index 000000000000..677eed56ac73 --- /dev/null +++ b/tools/cosmovisor/skip.go @@ -0,0 +1,47 @@ +package cosmovisor + +import ( + "fmt" + "strconv" + "strings" + + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +// IsSkipUpgradeHeight checks if pre-upgrade script must be run. +// If the height in the upgrade plan matches any of the heights provided in --unsafe-skip-upgrades, the script is not run. +func IsSkipUpgradeHeight(args []string, upgradeInfo upgradetypes.Plan) bool { + skipUpgradeHeights := UpgradeSkipHeights(args) + for _, h := range skipUpgradeHeights { + if h == int(upgradeInfo.Height) { + return true + } + } + return false +} + +// UpgradeSkipHeights gets all the heights provided when +// simd start --unsafe-skip-upgrades ... +func UpgradeSkipHeights(args []string) []int { + var heights []int + for i, arg := range args { + if arg == fmt.Sprintf("--%s", FlagSkipUpgradeHeight) { + j := i + 1 + + for j < len(args) { + tArg := args[j] + if strings.HasPrefix(tArg, "-") { + break + } + h, err := strconv.Atoi(tArg) + if err == nil { + heights = append(heights, h) + } + j++ + } + + break + } + } + return heights +} From 71c03a91ed422225e5b36d145276710effaaa6a3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 28 May 2025 17:54:52 -0400 Subject: [PATCH 015/115] WIP on watchers --- tools/cosmovisor/args.go | 20 ++- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 2 +- tools/cosmovisor/internal/checkers/checker.go | 2 +- .../internal/checkers/http_block.go | 4 +- .../internal/watchers/data_watcher.go | 2 +- .../internal/watchers/file_poll_watcher.go | 28 +++ .../internal/watchers/height_watcher.go | 12 ++ .../internal/watchers/log_watcher.go | 84 +++++++++ .../internal/watchers/poll_watcher.go | 29 ++- tools/cosmovisor/manual.go | 19 +- tools/cosmovisor/process.go | 63 ++++++- tools/cosmovisor/scanner.go | 165 ++++++++---------- 12 files changed, 303 insertions(+), 127 deletions(-) create mode 100644 tools/cosmovisor/internal/watchers/file_poll_watcher.go create mode 100644 tools/cosmovisor/internal/watchers/height_watcher.go create mode 100644 tools/cosmovisor/internal/watchers/log_watcher.go diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index 72bf3fe2076c..1a76cfb375d7 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -432,9 +432,8 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) { // UpgradeInfo returns the current upgrade info func (cfg *Config) UpgradeInfo() (upgradetypes.Plan, error) { - filename := filepath.Join(cfg.Root(), currentLink, upgradetypes.UpgradeInfoFilename) + filename := cfg.UpgradeInfoFilePath() _, err := os.Lstat(filename) - var u upgradetypes.Plan var bz []byte if err != nil { // no current directory return upgradetypes.Plan{}, fmt.Errorf("failed to read %q: %w", filename, err) @@ -442,10 +441,21 @@ func (cfg *Config) UpgradeInfo() (upgradetypes.Plan, error) { if bz, err = os.ReadFile(filename); err != nil { return upgradetypes.Plan{}, fmt.Errorf("failed to read %q: %w", filename, err) } - if err = jsonpb.Unmarshal(bytes.NewReader(bz), &u); err != nil { - return upgradetypes.Plan{}, fmt.Errorf("error unmarshalling %q: %w", filename, err) + return cfg.ParseUpgradeInfo(bz) +} + +func (cfg *Config) ParseUpgradeInfo(bz []byte) (upgradetypes.Plan, error) { + var upgradePlan upgradetypes.Plan + if err := jsonpb.Unmarshal(bytes.NewReader(bz), &upgradePlan); err != nil { + return upgradetypes.Plan{}, fmt.Errorf("error unmarshalling upgrade info: %w", err) + } + if err := upgradePlan.ValidateBasic(); err != nil { + return upgradetypes.Plan{}, fmt.Errorf("upgrade info failed validation upgrade inof: %w", err) + } + if !cfg.DisableRecase { + upgradePlan.Name = strings.ToLower(upgradePlan.Name) } - return u, nil + return upgradePlan, nil } // BooleanOption checks and validate env option diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 4ea62544bea4..6d3c599ee222 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -63,7 +63,7 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) if upgradeHeight > 0 { - plan := cosmovisor.ManualUpgradePlan{ + plan := &cosmovisor.ManualUpgradePlan{ Name: upgradeName, Height: upgradeHeight, } diff --git a/tools/cosmovisor/internal/checkers/checker.go b/tools/cosmovisor/internal/checkers/checker.go index 925cd332c2ce..03888f68dd49 100644 --- a/tools/cosmovisor/internal/checkers/checker.go +++ b/tools/cosmovisor/internal/checkers/checker.go @@ -1,5 +1,5 @@ package checkers -type LatestBlockChecker interface { +type HeightChecker interface { GetLatestBlockHeight() (uint64, error) } diff --git a/tools/cosmovisor/internal/checkers/http_block.go b/tools/cosmovisor/internal/checkers/http_block.go index 087ac17dd3e9..97ffc73bf6c0 100644 --- a/tools/cosmovisor/internal/checkers/http_block.go +++ b/tools/cosmovisor/internal/checkers/http_block.go @@ -8,7 +8,7 @@ import ( "strconv" ) -func NewHTTPRPCBLockChecker(url string) LatestBlockChecker { +func NewHTTPRPCBLockChecker(url string) HeightChecker { return httpRPCBlockChecker{ url: url, } @@ -33,7 +33,7 @@ func (j httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { return getHeightFromRPCBlockResponse(bz) } -var _ LatestBlockChecker = httpRPCBlockChecker{} +var _ HeightChecker = httpRPCBlockChecker{} type Header struct { Height string `json:"height"` diff --git a/tools/cosmovisor/internal/watchers/data_watcher.go b/tools/cosmovisor/internal/watchers/data_watcher.go index 5ae2b10e0f7f..7305c2dc3af9 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher.go +++ b/tools/cosmovisor/internal/watchers/data_watcher.go @@ -9,7 +9,7 @@ type DataWatcher[T any] struct { errChan chan error } -func NewDataWatcher[T any](ctx context.Context, watcher Watcher[[]byte], unmarshal func([]byte) (T, error)) *DataWatcher[T] { +func NewDataWatcher[T any, I any](ctx context.Context, watcher Watcher[I], unmarshal func(I) (T, error)) *DataWatcher[T] { outChan := make(chan T, 1) errChan := make(chan error, 1) go func() { diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher.go b/tools/cosmovisor/internal/watchers/file_poll_watcher.go new file mode 100644 index 000000000000..f4fd40db30f5 --- /dev/null +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher.go @@ -0,0 +1,28 @@ +package watchers + +import ( + "context" + "fmt" + "os" + "time" +) + +func NewFilePollWatcher(ctx context.Context, filename string, pollInterval time.Duration) Watcher[[]byte] { + check := func() ([]byte, error) { + stat, err := os.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("failed to stat file %s: %w", filename, err) + } + } else if stat.Size() > 0 { + bz, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", filename, err) + } else { + return bz, nil + } + } + return nil, os.ErrNotExist + } + return NewPollWatcher[[]byte](ctx, check, pollInterval) +} diff --git a/tools/cosmovisor/internal/watchers/height_watcher.go b/tools/cosmovisor/internal/watchers/height_watcher.go new file mode 100644 index 000000000000..a96abfffbd1b --- /dev/null +++ b/tools/cosmovisor/internal/watchers/height_watcher.go @@ -0,0 +1,12 @@ +package watchers + +import ( + "context" + "time" + + "cosmossdk.io/tools/cosmovisor/internal/checkers" +) + +func NewHeightWatcher(ctx context.Context, checker checkers.HeightChecker, pollInterval time.Duration) Watcher[uint64] { + return NewPollWatcher[uint64](ctx, checker.GetLatestBlockHeight, pollInterval) +} diff --git a/tools/cosmovisor/internal/watchers/log_watcher.go b/tools/cosmovisor/internal/watchers/log_watcher.go new file mode 100644 index 000000000000..8e1c84b9e021 --- /dev/null +++ b/tools/cosmovisor/internal/watchers/log_watcher.go @@ -0,0 +1,84 @@ +package watchers + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "regexp" + + "cosmossdk.io/tools/cosmovisor/internal/checkers" +) + +func NewHaltHeightLogWatcher(ctx context.Context, log io.Reader, checker checkers.HeightChecker) Watcher[uint64] { + check := func(line string) (uint64, error) { + height, err := parseHaltHeightLogMessage(line) + if err != nil { + return 0, err + } + actualHeight, err := checker.GetLatestBlockHeight() + if err != nil { + return 0, err + } + if actualHeight < height { + // false positive, ignore this log line + return 0, os.ErrNotExist + } + return actualHeight, nil + } + return NewDataWatcher[uint64](ctx, NewLogWatcher(ctx, log, haltHeightRegex), check) +} + +var haltHeightRegex = regexp.MustCompile(`halt per configuration height (\d+)`) + +func parseHaltHeightLogMessage(line string) (uint64, error) { + matches := haltHeightRegex.FindStringSubmatch(line) + if len(matches) < 2 { + return 0, os.ErrNotExist // No match found + } + var height uint64 + _, err := fmt.Sscanf(matches[1], "%d", &height) + if err != nil { + return 0, err // Error parsing height + } + return height, nil +} + +type LogWatcher struct { + outChan chan string + errChan chan error +} + +func NewLogWatcher(ctx context.Context, log io.Reader, regex *regexp.Regexp) Watcher[string] { + out := make(chan string, 1) + err := make(chan error, 1) + go func() { + defer close(out) + defer close(err) + scanner := bufio.NewScanner(log) + for scanner.Scan() { + select { + case <-ctx.Done(): + return + default: + line := scanner.Text() + if regex.MatchString(line) { + out <- line + } + } + } + }() + return &LogWatcher{ + outChan: out, + errChan: err, + } +} + +func (l *LogWatcher) Updated() <-chan string { + return l.outChan +} + +func (l *LogWatcher) Errors() <-chan error { + return l.errChan +} diff --git a/tools/cosmovisor/internal/watchers/poll_watcher.go b/tools/cosmovisor/internal/watchers/poll_watcher.go index 1c4a10b8cae3..01d33c9304f7 100644 --- a/tools/cosmovisor/internal/watchers/poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/poll_watcher.go @@ -7,15 +7,13 @@ import ( "time" ) -type PollWatcher struct { - outChan chan []byte +type PollWatcher[T any] struct { + outChan chan T errChan chan error } -var _ Watcher[[]byte] = (*PollWatcher)(nil) - -func NewPollWatcher(ctx context.Context, filename string, pollInterval time.Duration) *PollWatcher { - outChan := make(chan []byte, 1) +func NewPollWatcher[T any](ctx context.Context, checker func() (T, error), pollInterval time.Duration) *PollWatcher[T] { + outChan := make(chan T, 1) errChan := make(chan error, 1) ticker := time.NewTicker(pollInterval) go func() { @@ -28,32 +26,29 @@ func NewPollWatcher(ctx context.Context, filename string, pollInterval time.Dura case <-ctx.Done(): return case <-ticker.C: - stat, err := os.Stat(filename) + x, err := checker() if err != nil { if !os.IsNotExist(err) { - errChan <- fmt.Errorf("failed to stat upgrade info file: %w", err) + errChan <- fmt.Errorf("failed to check for updates: %w", err) } - } else if stat.Size() > 0 { - bz, err := os.ReadFile(filename) - if err != nil { - errChan <- fmt.Errorf("failed to read file %s: %w", filename, err) - } else { - outChan <- bz + } else { + if x != nil { + outChan <- x } } } } }() - return &PollWatcher{ + return &PollWatcher[T]{ outChan: outChan, errChan: errChan, } } -func (w *PollWatcher) Updated() <-chan []byte { +func (w *PollWatcher[T]) Updated() <-chan T { return w.outChan } -func (w *PollWatcher) Errors() <-chan error { +func (w *PollWatcher[T]) Errors() <-chan error { return w.errChan } diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 2cfa5a9ccf33..8254b01cf2ca 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -8,7 +8,7 @@ import ( ) // ReadManualUpgrades reads the manual upgrade data. -func ReadManualUpgrades(cfg *Config) (ManualUpgradeBatch, error) { +func (cfg *Config) ReadManualUpgrades() (ManualUpgradeBatch, error) { bz, err := os.ReadFile(cfg.UpgradeInfoBatchFilePath()) if err != nil { if os.IsNotExist(err) { @@ -16,20 +16,29 @@ func ReadManualUpgrades(cfg *Config) (ManualUpgradeBatch, error) { } return nil, err } + return cfg.ParseManualUpgrades(bz) +} + +func (cfg *Config) ParseManualUpgrades(bz []byte) (ManualUpgradeBatch, error) { var manualUpgrades ManualUpgradeBatch if err := json.Unmarshal(bz, &manualUpgrades); err != nil { return nil, err } sortUpgrades(manualUpgrades) + + if err := manualUpgrades.ValidateBasic(); err != nil { + return nil, fmt.Errorf("invalid manual upgrade batch: %w", err) + } + return manualUpgrades, nil } // AddManualUpgrade adds a manual upgrade plan. // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. -func AddManualUpgrade(cfg *Config, plan ManualUpgradePlan, forceOverwrite bool) error { - manualUpgrades, err := ReadManualUpgrades(cfg) +func AddManualUpgrade(cfg *Config, plan *ManualUpgradePlan, forceOverwrite bool) error { + manualUpgrades, err := cfg.ReadManualUpgrades() if err != nil { return err } @@ -56,13 +65,13 @@ func AddManualUpgrade(cfg *Config, plan ManualUpgradePlan, forceOverwrite bool) return os.WriteFile(cfg.UpgradeInfoBatchFilePath(), manualUpgradesData, 0644) } -func sortUpgrades(upgrades []ManualUpgradePlan) { +func sortUpgrades(upgrades ManualUpgradeBatch) { sort.Slice(upgrades, func(i, j int) bool { return upgrades[i].Height < upgrades[j].Height }) } -type ManualUpgradeBatch []ManualUpgradePlan +type ManualUpgradeBatch []*ManualUpgradePlan func (m ManualUpgradeBatch) ValidateBasic() error { for _, upgrade := range m { diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index 62fbc9230878..f994a5f58b7e 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -17,6 +17,7 @@ import ( "cosmossdk.io/log" "github.com/otiai10/copy" + "cosmossdk.io/tools/cosmovisor/internal/checkers" "cosmossdk.io/tools/cosmovisor/internal/watchers" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" @@ -27,11 +28,15 @@ type Launcher struct { cfg *Config ctx context.Context cancel context.CancelFunc - // nodeUpgradeWatcher watches for data in an upgrade-info.json created by the running node - nodeUpgradeWatcher watchers.Watcher[upgradetypes.Plan] + // upgradePlanWatcher watches for data in an upgrade-info.json created by the running node + upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator - manualUpgradesWatcher watchers.Watcher[[]upgradetypes.Plan] + manualUpgradesWatcher watchers.Watcher[ManualUpgradeBatch] + haltHeightWatcher watchers.Watcher[uint64] + actualHeightWatcher watchers.Watcher[uint64] + heightChecker checkers.HeightChecker upgradePlan *upgradetypes.Plan + manualUpgrade *ManualUpgradePlan } func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { @@ -45,19 +50,63 @@ func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { } // TODO the watchers should do data validation in additional to json unmarshaling - nodeUpgradeWatcher := initWatcher[upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoFilePath()) - manualUpgradesWatcher := initWatcher[[]upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoBatchFilePath()) + nodeUpgradeWatcher := initWatcher[upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoFilePath(), cfg.ParseUpgradeInfo) + manualUpgradesWatcher := initWatcher[ManualUpgradeBatch](ctx, cfg, dirWatcher, cfg.UpgradeInfoBatchFilePath(), cfg.ParseManualUpgrades) return Launcher{ logger: logger, cfg: cfg, ctx: ctx, cancel: cancel, - nodeUpgradeWatcher: nodeUpgradeWatcher, + upgradePlanWatcher: nodeUpgradeWatcher, manualUpgradesWatcher: manualUpgradesWatcher, }, nil } +func (l *Launcher) Watch() { + errChan := joinChannels(l.upgradePlanWatcher.Errors(), + l.manualUpgradesWatcher.Errors(), + l.haltHeightWatcher.Errors(), + l.actualHeightWatcher.Errors()) + for { + select { + case <-l.ctx.Done(): + + // TODO handle cosmovisor shutdown + return + case upgradePlan := <-l.upgradePlanWatcher.Updated(): + l.upgradePlan = &upgradePlan + // TODO upgrade plan received, positive signal to perform upgrade, no additional checks needed + case <-l.manualUpgradesWatcher.Updated(): + l.logger.Info("manual upgrades watcher updated") + // TODO received new manual upgrades batch: + // must establish current node height and select the first manual upgrade after current height, if any + // if one is found, node must be restarted with --halt-height + case <-l.haltHeightWatcher.Updated(): + // TODO check against manual upgrade height + case <-l.actualHeightWatcher.Updated(): + // TODO check against manual upgrade height + case err := <-errChan: + // for now just log errors + l.logger.Error("error in upgrade plan watcher", "error", err) + } + } +} + +// TODO fix this with WaitGroup +func joinChannels[T any](ch ...<-chan T) <-chan T { + out := make(chan T) + go func() { + defer close(out) + for _, c := range ch { + for msg := range c { + out <- msg + } + } + }() + return out +} + //// BatchUpgradeWatcher starts a watcher loop that swaps upgrade manifests at the correct //// height, given the batch upgrade file. It watches the current state of the chain //// via the websocket API. @@ -273,7 +322,7 @@ func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { select { // TODO add manual-upgrades watcher // TODO replace with upgrade-info.json watcher - case upgradePlan := <-l.nodeUpgradeWatcher.Updated(): + case upgradePlan := <-l.upgradePlanWatcher.Updated(): l.upgradePlan = &upgradePlan // upgrade - kill the process and restart l.logger.Info("daemon shutting down in an attempt to restart") diff --git a/tools/cosmovisor/scanner.go b/tools/cosmovisor/scanner.go index 8bb43250c90a..46d486404fc4 100644 --- a/tools/cosmovisor/scanner.go +++ b/tools/cosmovisor/scanner.go @@ -2,33 +2,22 @@ package cosmovisor import ( "context" - "encoding/json" "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "testing" "time" - dbm "github.com/cometbft/cometbft-db" - "github.com/cometbft/cometbft/store" - "cosmossdk.io/tools/cosmovisor/internal/watchers" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) var errUntestAble = errors.New("untestable") -func initWatcher[T any](ctx context.Context, cfg *Config, dirWatcher *watchers.FSNotifyWatcher, filename string) watchers.Watcher[T] { +func initWatcher[T any](ctx context.Context, cfg *Config, dirWatcher *watchers.FSNotifyWatcher, filename string, unmarshal func([]byte) (T, error)) watchers.Watcher[T] { if dirWatcher != nil { hybridWatcher := watchers.NewHybridWatcher(ctx, dirWatcher, filename, cfg.PollInterval) - return watchers.NewDataWatcher[T](ctx, hybridWatcher) + return watchers.NewDataWatcher[T](ctx, hybridWatcher, unmarshal) } else { pollWatcher := watchers.NewPollWatcher(ctx, filename, cfg.PollInterval) - return watchers.NewDataWatcher[T](ctx, pollWatcher) + return watchers.NewDataWatcher[T](ctx, pollWatcher, unmarshal) } } @@ -48,77 +37,77 @@ type fileWatcher struct { disableRecase bool } -// checkHeight checks if the current block height -func (fw *fileWatcher) checkHeight() (int64, error) { - if testing.Testing() { // we cannot test the command in the test environment - return 0, errUntestAble - } - - if fw.IsStop() { - result, err := exec.Command(fw.currentBin, "config", "get", "config", "db_backend", "--home", fw.daemonHome).CombinedOutput() //nolint:gosec // we want to execute the config command - if err != nil { - result = []byte("goleveldb") // set default value, old version may not have config command - } - blockStoreDB, err := dbm.NewDB("blockstore", dbm.BackendType(result), filepath.Join(fw.daemonHome, "data")) - if err != nil { - return 0, err - } - defer blockStoreDB.Close() - return store.NewBlockStore(blockStoreDB).Height(), nil - } - - result, err := exec.Command(fw.currentBin, "status", "--home", fw.daemonHome).CombinedOutput() //nolint:gosec // we want to execute the status command - if err != nil { - return 0, err - } - - type response struct { - SyncInfo struct { - LatestBlockHeight string `json:"latest_block_height"` - } `json:"sync_info"` - AnotherCasingSyncInfo struct { - LatestBlockHeight string `json:"latest_block_height"` - } `json:"SyncInfo"` - } - - var resp response - if err := json.Unmarshal(result, &resp); err != nil { - return 0, err - } - - if resp.SyncInfo.LatestBlockHeight != "" { - return strconv.ParseInt(resp.SyncInfo.LatestBlockHeight, 10, 64) - } else if resp.AnotherCasingSyncInfo.LatestBlockHeight != "" { - return strconv.ParseInt(resp.AnotherCasingSyncInfo.LatestBlockHeight, 10, 64) - } - - return 0, errors.New("latest block height is empty") -} - -func parseUpgradeInfoFile(filename string, disableRecase bool) (upgradetypes.Plan, error) { - f, err := os.ReadFile(filename) - if err != nil { - return upgradetypes.Plan{}, err - } - - if len(f) == 0 { - return upgradetypes.Plan{}, fmt.Errorf("empty upgrade-info.json in %q", filename) - } - - var upgradePlan upgradetypes.Plan - if err := json.Unmarshal(f, &upgradePlan); err != nil { - return upgradetypes.Plan{}, err - } - - // required values must be set - if err := upgradePlan.ValidateBasic(); err != nil { - return upgradetypes.Plan{}, fmt.Errorf("invalid upgrade-info.json content: %w, got: %v", err, upgradePlan) - } - - // normalize name to prevent operator error in upgrade name case sensitivity errors. - if !disableRecase { - upgradePlan.Name = strings.ToLower(upgradePlan.Name) - } - - return upgradePlan, nil -} +//// checkHeight checks if the current block height +//func (fw *fileWatcher) checkHeight() (int64, error) { +// if testing.Testing() { // we cannot test the command in the test environment +// return 0, errUntestAble +// } +// +// if fw.IsStop() { +// result, err := exec.Command(fw.currentBin, "config", "get", "config", "db_backend", "--home", fw.daemonHome).CombinedOutput() //nolint:gosec // we want to execute the config command +// if err != nil { +// result = []byte("goleveldb") // set default value, old version may not have config command +// } +// blockStoreDB, err := dbm.NewDB("blockstore", dbm.BackendType(result), filepath.Join(fw.daemonHome, "data")) +// if err != nil { +// return 0, err +// } +// defer blockStoreDB.Close() +// return store.NewBlockStore(blockStoreDB).Height(), nil +// } +// +// result, err := exec.Command(fw.currentBin, "status", "--home", fw.daemonHome).CombinedOutput() //nolint:gosec // we want to execute the status command +// if err != nil { +// return 0, err +// } +// +// type response struct { +// SyncInfo struct { +// LatestBlockHeight string `json:"latest_block_height"` +// } `json:"sync_info"` +// AnotherCasingSyncInfo struct { +// LatestBlockHeight string `json:"latest_block_height"` +// } `json:"SyncInfo"` +// } +// +// var resp response +// if err := json.Unmarshal(result, &resp); err != nil { +// return 0, err +// } +// +// if resp.SyncInfo.LatestBlockHeight != "" { +// return strconv.ParseInt(resp.SyncInfo.LatestBlockHeight, 10, 64) +// } else if resp.AnotherCasingSyncInfo.LatestBlockHeight != "" { +// return strconv.ParseInt(resp.AnotherCasingSyncInfo.LatestBlockHeight, 10, 64) +// } +// +// return 0, errors.New("latest block height is empty") +//} +// +//func parseUpgradeInfoFile(filename string, disableRecase bool) (upgradetypes.Plan, error) { +// f, err := os.ReadFile(filename) +// if err != nil { +// return upgradetypes.Plan{}, err +// } +// +// if len(f) == 0 { +// return upgradetypes.Plan{}, fmt.Errorf("empty upgrade-info.json in %q", filename) +// } +// +// var upgradePlan upgradetypes.Plan +// if err := json.Unmarshal(f, &upgradePlan); err != nil { +// return upgradetypes.Plan{}, err +// } +// +// // required values must be set +// if err := upgradePlan.ValidateBasic(); err != nil { +// return upgradetypes.Plan{}, fmt.Errorf("invalid upgrade-info.json content: %w, got: %v", err, upgradePlan) +// } +// +// // normalize name to prevent operator error in upgrade name case sensitivity errors. +// if !disableRecase { +// upgradePlan.Name = strings.ToLower(upgradePlan.Name) +// } +// +// return upgradePlan, nil +//} From 37ae97a370363cd295e2056df4b0f5aa35605f93 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 29 May 2025 13:27:00 -0400 Subject: [PATCH 016/115] WIP --- tools/cosmovisor/cmd/cosmovisor/init.go | 1 + tools/cosmovisor/cmd/mock_node/main.go | 41 ++++++++++------- tools/cosmovisor/internal/process.go | 59 +++++++++++++++++++++++++ tools/cosmovisor/manual.go | 11 +++++ tools/cosmovisor/process.go | 53 ++++++++++++++++++++-- tools/cosmovisor/scanner.go | 2 +- tools/cosmovisor/upgrade.go | 4 +- 7 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 tools/cosmovisor/internal/process.go diff --git a/tools/cosmovisor/cmd/cosmovisor/init.go b/tools/cosmovisor/cmd/cosmovisor/init.go index 56717ec28a6f..98d9fcbc8e73 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init.go +++ b/tools/cosmovisor/cmd/cosmovisor/init.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/log" + "cosmossdk.io/tools/cosmovisor" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index f502e07a88c1..3396219756e6 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -10,6 +10,7 @@ import ( "os" "os/signal" "path" + "syscall" "time" "cosmossdk.io/log" @@ -39,6 +40,7 @@ x/upgrade upgrade-info.json behavior.`, var homePath string var httpAddr string var blockUrl string + var shutdownDelay time.Duration cmd.Flags().Uint64Var(&startHeight, "start-height", 1, "Block height at which to start the mock node.") cmd.Flags().DurationVar(&blockTime, "block-time", 0, "Duration of time between blocks. This is required to simulate a progression of blocks over time.") cmd.Flags().StringVar(&upgradePlan, "upgrade-plan", "", "upgrade-info.json to create after the halt duration is reached. Either this flag or --halt-height must be specified but not both.") @@ -46,6 +48,7 @@ x/upgrade upgrade-info.json behavior.`, cmd.Flags().StringVar(&homePath, "home", "", "Home directory for the mock node. upgrade-info.json will be written to the data sub-directory of this directory. Defaults to the current directory.") cmd.Flags().StringVar(&httpAddr, "http-addr", ":8080", "HTTP server address to serve block information. Defaults to :8080.") cmd.Flags().StringVar(&blockUrl, "block-url", "/block", "URL at which the latest block information is served. Defaults to /block.") + cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", 0, "Duration to wait before shutting down the node upon receiving a shutdown signal. Defaults to 0 (no delay).") cmd.RunE = func(cmd *cobra.Command, args []string) error { if upgradePlan != "" && haltHeight > 0 { return fmt.Errorf("cannot specify both --upgrade-plan and --halt-height") @@ -64,13 +67,14 @@ x/upgrade upgrade-info.json behavior.`, } } node := &MockNode{ - height: startHeight, - blockTime: blockTime, - haltHeight: haltHeight, - homePath: homePath, - httpAddr: httpAddr, - blockUrl: blockUrl, - logger: log.NewLogger(os.Stdout), + height: startHeight, + blockTime: blockTime, + haltHeight: haltHeight, + homePath: homePath, + httpAddr: httpAddr, + blockUrl: blockUrl, + shutdownDelay: shutdownDelay, + logger: log.NewLogger(os.Stdout), } if upgradePlan != "" { node.upgradePlan = &upgradetypes.Plan{} @@ -97,18 +101,19 @@ x/upgrade upgrade-info.json behavior.`, } type MockNode struct { - height uint64 - blockTime time.Duration - upgradePlan *upgradetypes.Plan - haltHeight uint64 - homePath string - httpAddr string - blockUrl string - logger log.Logger + height uint64 + blockTime time.Duration + upgradePlan *upgradetypes.Plan + haltHeight uint64 + homePath string + httpAddr string + blockUrl string + logger log.Logger + shutdownDelay time.Duration } func (n *MockNode) Run(ctx context.Context) error { - ctx, _ = signal.NotifyContext(ctx, os.Interrupt, os.Kill) + ctx, _ = signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) upgradeHeight := n.haltHeight if upgradeHeight == 0 { upgradeHeight = uint64(n.upgradePlan.Height) @@ -126,6 +131,10 @@ func (n *MockNode) Run(ctx context.Context) error { if err := srv.Shutdown(ctx); err != nil { n.logger.Error("Error shutting down HTTP server", "err", err) } + if n.shutdownDelay > 0 { + n.logger.Info("Waiting for shutdown delay", "delay", n.shutdownDelay) + time.Sleep(n.shutdownDelay) + } return nil case <-ticker.C: n.height++ diff --git a/tools/cosmovisor/internal/process.go b/tools/cosmovisor/internal/process.go new file mode 100644 index 000000000000..2cfea9b9d4d8 --- /dev/null +++ b/tools/cosmovisor/internal/process.go @@ -0,0 +1,59 @@ +package internal + +import ( + "os/exec" + "sync" + "syscall" + "time" +) + +type ProcessRunner struct { + cmd *exec.Cmd + done chan error + wg *sync.WaitGroup +} + +func RunProcess(cmd *exec.Cmd) *ProcessRunner { + done := make(chan error, 1) + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + err := cmd.Run() + done <- err + close(done) + }() + return &ProcessRunner{ + cmd: cmd, + done: done, + wg: wg, + } +} + +func (pr *ProcessRunner) Done() chan error { + return pr.done +} + +func (pr *ProcessRunner) Shutdown(shutdownGrace time.Duration) error { + proc := pr.cmd.Process + if proc == nil { + return nil // process already exited + } + if err := proc.Signal(syscall.SIGTERM); err != nil { + return err + } + done := make(chan struct{}) + go func() { + pr.wg.Wait() + close(done) + }() + go func() { + select { + case <-done: + case <-time.After(shutdownGrace): + // TODO handle the error + proc.Kill() + } + }() + return nil +} diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 8254b01cf2ca..5b3de7edb027 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -82,6 +82,17 @@ func (m ManualUpgradeBatch) ValidateBasic() error { return nil } +func (m ManualUpgradeBatch) FirstUpgradeAfter(height int64) *ManualUpgradePlan { + // ensure the upgrades are sorted before searching + sortUpgrades(m) + for _, upgrade := range m { + if upgrade.Height > height { + return upgrade + } + } + return nil +} + type ManualUpgradePlan struct { Name string `json:"name"` Height int64 `json:"height"` diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index f994a5f58b7e..a7feb758b544 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -23,6 +23,30 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) +/* +Execution Flow: +* Cosmovisor started +* load manual upgrade batches +* if have manual upgrade: + * load last known height + if have last known height: + * find proper manual upgrade plan + * add --halt-height to the args before running the app + else: + * find last manual upgrade plan?? + * start the app --halt-height + * check height +* start the app +* check height, find proper manual upgrade and restart app with proper --halt-height if needed +* initialize watchers for + * on upgrade-info.json found, stop app, signal upgrade + * on upgrade-info.json.batch found, start height checking and restart app with --halt-height if needed + * if halt height signal received (by log watching and checking height), stop app, signal upgrade + * if past halt height for a manual upgrade, stop app and report error (likely not safe to upgrade) + * process exit + * if process exits without signaling an upgrade plan, we manually check for upgrade-info.json +*/ + type Launcher struct { logger log.Logger cfg *Config @@ -49,18 +73,36 @@ func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) } - // TODO the watchers should do data validation in additional to json unmarshaling nodeUpgradeWatcher := initWatcher[upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoFilePath(), cfg.ParseUpgradeInfo) manualUpgradesWatcher := initWatcher[ManualUpgradeBatch](ctx, cfg, dirWatcher, cfg.UpgradeInfoBatchFilePath(), cfg.ParseManualUpgrades) - return Launcher{ + l := Launcher{ logger: logger, cfg: cfg, ctx: ctx, cancel: cancel, upgradePlanWatcher: nodeUpgradeWatcher, manualUpgradesWatcher: manualUpgradesWatcher, - }, nil + } + + err = l.loadManualUpgrade() + if err != nil { + return Launcher{}, fmt.Errorf("error loading manual upgrades: %w", err) + } + + return l, nil +} + +func (l *Launcher) loadManualUpgrade() error { + //manualUpgradeBatch, err := l.cfg.ReadManualUpgrades() + //if err != nil { + // return Launcher{}, err + //} + //if manualUpgradeBatch == nil || len(manualUpgradeBatch) == 0 { + // return nil + //} + // + panic("TODO") } func (l *Launcher) Watch() { @@ -87,12 +129,17 @@ func (l *Launcher) Watch() { case <-l.actualHeightWatcher.Updated(): // TODO check against manual upgrade height case err := <-errChan: + // TODO move error handling to a separate goroutine // for now just log errors l.logger.Error("error in upgrade plan watcher", "error", err) } } } +func (l *Launcher) watchErrors() { + +} + // TODO fix this with WaitGroup func joinChannels[T any](ch ...<-chan T) <-chan T { out := make(chan T) diff --git a/tools/cosmovisor/scanner.go b/tools/cosmovisor/scanner.go index 46d486404fc4..0d26d09d427d 100644 --- a/tools/cosmovisor/scanner.go +++ b/tools/cosmovisor/scanner.go @@ -16,7 +16,7 @@ func initWatcher[T any](ctx context.Context, cfg *Config, dirWatcher *watchers.F hybridWatcher := watchers.NewHybridWatcher(ctx, dirWatcher, filename, cfg.PollInterval) return watchers.NewDataWatcher[T](ctx, hybridWatcher, unmarshal) } else { - pollWatcher := watchers.NewPollWatcher(ctx, filename, cfg.PollInterval) + pollWatcher := watchers.NewFilePollWatcher(ctx, filename, cfg.PollInterval) return watchers.NewDataWatcher[T](ctx, pollWatcher, unmarshal) } } diff --git a/tools/cosmovisor/upgrade.go b/tools/cosmovisor/upgrade.go index c2bba046d8de..8fb25b3446a1 100644 --- a/tools/cosmovisor/upgrade.go +++ b/tools/cosmovisor/upgrade.go @@ -12,9 +12,9 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -// UpgradeBinary will be called after the log message has been parsed and the process has terminated. +// UpgradeBinary will be called after an upgrade has been confirmed and the process has terminated. // We can now make any changes to the underlying directory without interference and leave it -// in a state, so we can make a proper restart +// in the upgraded state so that the app can restart with the new binary. func UpgradeBinary(logger log.Logger, cfg *Config, p upgradetypes.Plan) error { // simplest case is to switch the link err := plan.EnsureBinary(cfg.UpgradeBin(p.Name)) From 4745c6329390bbd17e15966259f03ec3f5b6a808 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 29 May 2025 17:39:01 -0400 Subject: [PATCH 017/115] WIP on state machine --- tools/cosmovisor/internal/state_machine.go | 46 ++++++++++++++++++++++ tools/cosmovisor/state_machine.mermaid | 28 +++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tools/cosmovisor/internal/state_machine.go create mode 100644 tools/cosmovisor/state_machine.mermaid diff --git a/tools/cosmovisor/internal/state_machine.go b/tools/cosmovisor/internal/state_machine.go new file mode 100644 index 000000000000..a97ad60ee3e9 --- /dev/null +++ b/tools/cosmovisor/internal/state_machine.go @@ -0,0 +1,46 @@ +package internal + +type Event interface{} + +type UpgradePlanEvent struct{} +type UpgradeBatchEvent struct{} +type ProcessExistEvent struct{} +type Height struct{} + +type StateMachine struct { +} + +type State interface { + OnEvent(event Event) State +} + +//func (sm *StateMachine) WaitingForXUpgrade(event Event) { +// +//} +// +//func (sm *StateMachine) WaitingForManualUpgrade(event Event) { +// +//} + +// type WaitingForManualUpgrade struct { +// manualUpgradePlan *cosmovisor.ManualUpgradePlan +// } +// +// type WaitingForXUpgrade struct{} +// +// type WaitingForShutdown struct{} +func Init() { + // check if have upgrade-info.json + // check if have upgrade-info.json.batch +} + +func WaitingForXUpgrade(event Event) { + switch _ := event.(type) { + case UpgradePlanEvent: + // handle received upgrade plan event + case UpgradeBatchEvent: + // handle received upgrade batch event + case ProcessExistEvent: + // check for upgrade-info.json + } +} diff --git a/tools/cosmovisor/state_machine.mermaid b/tools/cosmovisor/state_machine.mermaid new file mode 100644 index 000000000000..bbf9ed0378ba --- /dev/null +++ b/tools/cosmovisor/state_machine.mermaid @@ -0,0 +1,28 @@ +stateDiagram-v2 + [*] --> Init + Init --> WaitForXUpgrade: no upgrade-info.json.batch + Init --> ??: have upgrade-info.json + Init --> StartWithHaltHeight: have upgrade-info.json.batch + WaitForXUpgrade --> KillProcessAndUpgrade: got upgrade-info.json + WaitForXUpgrade --> KillProcessAndAddHaltHeight: got upgrade-info.json.batch + WaitForXUpgrade --> CheckForUpgradeInfo: process exited + CheckForUpgradeInfo --> ShutdownAndError: no upgrade-info.json + StartWithHaltHeight --> WaitForManualUpgrade + WaitForManualUpgrade --> KillProcessAndUpgrade: got upgrade-info.json + WaitForManualUpgrade --> KillProcessAndAddHaltHeight: got upgrade-info.json.batch + WaitForManualUpgrade --> KillProcessAndUpgrade: height == halt-height + WaitForManualUpgrade --> ShutdownAndError: height > halt-height + WaitForManualUpgrade --> CheckForManualUpgrade: process exited + CheckForManualUpgrade --> ShutdownAndError: can't confirm upgrade + CheckForManualUpgrade --> DoUpgrade: confirmed halt-height or upgrade-info.json + KillProcessAndAddHaltHeight --> StartWithHaltHeight: process existed + KillProcessAndUpgrade --> DoUpgrade: process exited + DoUpgrade --> CheckSkipUpgradeHeight + CheckSkipUpgradeHeight --> DoBackup + CheckSkipUpgradeHeight --> Init: skip upgrade + DoBackup --> CustomPreUpgrade + CustomPreUpgrade --> SwitchToNewBinary + SwitchToNewBinary --> RunPreUpgrade + RunPreUpgrade --> Init: have new binary + RunPreUpgrade --> ShutdownAndError: no new binary + RunPreUpgrade --> GracefulShutdown: daemon restart disabled From ddc0e33c10fcf9bd06081c8b050ca6eefa3a10e4 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 30 May 2025 10:41:29 -0400 Subject: [PATCH 018/115] WIP on state machine diagrams --- tools/cosmovisor/state_machine2.mermaid | 25 ++++++++++++ tools/cosmovisor/state_machine3.puml | 53 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tools/cosmovisor/state_machine2.mermaid create mode 100644 tools/cosmovisor/state_machine3.puml diff --git a/tools/cosmovisor/state_machine2.mermaid b/tools/cosmovisor/state_machine2.mermaid new file mode 100644 index 000000000000..69001d04a789 --- /dev/null +++ b/tools/cosmovisor/state_machine2.mermaid @@ -0,0 +1,25 @@ +stateDiagram-v2 + [*] --> ComputeRunPlan + state ComputeRunPlan { + ReadUpgradeInfo --> ReadManualUpgradesBatch + ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json + ReadManualUpgradesBatch --> Run: no manual upgrades + ReadManualUpgradesBatch --> RunWithHaltHeight: have manual-upgrades-batch.json + ReadLastKnownHeight + } + + state DoUpgrade { + CheckSkipUpgradeHeight --> DoBackup + DoBackup --> CustomPreUpgrade + CustomPreUpgrade --> SwitchToNewBinary + SwitchToNewBinary --> RunPreUpgrade + RunPreUpgrade + } + + DoUpgrade --> ComputeRunPlan + +%% state Run { +%% } +%% +%% state RunWithHaltHeight { +%% } \ No newline at end of file diff --git a/tools/cosmovisor/state_machine3.puml b/tools/cosmovisor/state_machine3.puml new file mode 100644 index 000000000000..c40cb018689a --- /dev/null +++ b/tools/cosmovisor/state_machine3.puml @@ -0,0 +1,53 @@ +@startuml + +[*] -> ComputeRunPlan + +state ComputeRunPlan { + state ReadUpgradeInfo { + } + state ReadManualUpgradeBatch { + } + ReadUpgradeInfo --> ReadManualUpgradeBatch + ReadManualUpgradeBatch --> ReadLatestHeight +} + +ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json +ReadLatestHeight --> RunWithHaltHeight: have manual-upgrade-batch.json +ReadLatestHeight --> Error: have unapplied upgrades before current height? +ReadManualUpgradeBatch --> Run: no manual-upgrade-batch.json + +'ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json +'ComputeRunPlan --> Run +'ComputeRunPlan --> RunWithHaltHeight +' +state DoUpgrade { + DoBackup --> CustomPreUpgrade + CustomPreUpgrade --> SwitchToNewBinary + SwitchToNewBinary --> RunPreUpgrade + RunPreUpgrade --> ComputeRunPlan +} +' +state Run { + state ConfirmDesiredHaltHeight { + } + ConfirmDesiredHaltHeight --> WatchForSignals: true + state WatchForSignals { + } +} + +WatchForSignals --> CheckHaveUpgrade: got upgrade-info.json + +state CheckHaveUpgrade { + CheckSkipUpgradeHeight --> Run +} + +CheckSkipUpgradeHeight --> ShutdownNode +ConfirmDesiredHaltHeight --> ShutdownNode: false + +state ShutdownNode { + SendShutdownSignal --> WaitForShutdown +} + +WaitForShutdown --> ComputeRunPlan + +@enduml From 4953995144d4a59d3856010ef0859d486c147a84 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 30 May 2025 12:29:44 -0400 Subject: [PATCH 019/115] WIP on state machine --- tools/cosmovisor/go.mod | 6 +- tools/cosmovisor/go.sum | 2 + tools/cosmovisor/internal/runner.mock.go | 140 ++++++++++ tools/cosmovisor/internal/state_machine.dot | 43 +++ tools/cosmovisor/internal/state_machine.go | 249 +++++++++++++++--- tools/cosmovisor/internal/state_machine.png | Bin 0 -> 194734 bytes .../cosmovisor/internal/state_machine_test.go | 38 +++ .../internal/watchers/hybrid_watcher.go | 2 +- .../internal/watchers/poll_watcher.go | 9 +- 9 files changed, 448 insertions(+), 41 deletions(-) create mode 100644 tools/cosmovisor/internal/runner.mock.go create mode 100644 tools/cosmovisor/internal/state_machine.dot create mode 100644 tools/cosmovisor/internal/state_machine.png create mode 100644 tools/cosmovisor/internal/state_machine_test.go diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index c34fa1bb2c52..e0c66450025f 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -4,16 +4,16 @@ go 1.23.5 require ( cosmossdk.io/log v1.6.0 - github.com/cometbft/cometbft v1.0.1 - github.com/cometbft/cometbft-db v1.0.4 github.com/cosmos/cosmos-sdk v0.53.0 github.com/cosmos/gogoproto v1.7.0 github.com/fsnotify/fsnotify v1.9.0 github.com/otiai10/copy v1.14.1 github.com/pelletier/go-toml/v2 v2.2.4 + github.com/qmuntal/stateless v1.7.2 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 + go.uber.org/mock v0.5.2 google.golang.org/grpc v1.72.0 ) @@ -60,6 +60,8 @@ require ( github.com/cockroachdb/pebble v1.1.5 // indirect github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/cometbft/cometbft v1.0.1 // indirect + github.com/cometbft/cometbft-db v1.0.4 // indirect github.com/cometbft/cometbft/api v1.0.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.1 // indirect diff --git a/tools/cosmovisor/go.sum b/tools/cosmovisor/go.sum index efcb15bc3a9c..4121847ac67a 100644 --- a/tools/cosmovisor/go.sum +++ b/tools/cosmovisor/go.sum @@ -1412,6 +1412,8 @@ github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/qmuntal/stateless v1.7.2 h1:FqCErOP+Hf+/FByJt/S4UOLHFJeTf8CbMrEE0AkYT8k= +github.com/qmuntal/stateless v1.7.2/go.mod h1:n1HjRBM/cq4uCr3rfUjaMkgeGcd+ykAZwkjLje6jGBM= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= diff --git a/tools/cosmovisor/internal/runner.mock.go b/tools/cosmovisor/internal/runner.mock.go new file mode 100644 index 000000000000..af301774d3da --- /dev/null +++ b/tools/cosmovisor/internal/runner.mock.go @@ -0,0 +1,140 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: state_machine.go +// +// Generated by this command: +// +// mockgen -source=state_machine.go -package=internal -destination=runner.mock.go +// + +// Package internal is a generated GoMock package. +package internal + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockRunner is a mock of Runner interface. +type MockRunner struct { + ctrl *gomock.Controller + recorder *MockRunnerMockRecorder + isgomock struct{} +} + +// MockRunnerMockRecorder is the mock recorder for MockRunner. +type MockRunnerMockRecorder struct { + mock *MockRunner +} + +// NewMockRunner creates a new mock instance. +func NewMockRunner(ctrl *gomock.Controller) *MockRunner { + mock := &MockRunner{ctrl: ctrl} + mock.recorder = &MockRunnerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRunner) EXPECT() *MockRunnerMockRecorder { + return m.recorder +} + +// CheckHeightSync mocks base method. +func (m *MockRunner) CheckHeightSync(ctx context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CheckHeightSync", ctx) +} + +// CheckHeightSync indicates an expected call of CheckHeightSync. +func (mr *MockRunnerMockRecorder) CheckHeightSync(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckHeightSync", reflect.TypeOf((*MockRunner)(nil).CheckHeightSync), ctx) +} + +// ReadManualUpgradeBatchSync mocks base method. +func (m *MockRunner) ReadManualUpgradeBatchSync(ctx context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ReadManualUpgradeBatchSync", ctx) +} + +// ReadManualUpgradeBatchSync indicates an expected call of ReadManualUpgradeBatchSync. +func (mr *MockRunnerMockRecorder) ReadManualUpgradeBatchSync(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadManualUpgradeBatchSync", reflect.TypeOf((*MockRunner)(nil).ReadManualUpgradeBatchSync), ctx) +} + +// ReadUpgradeInfoJsonSync mocks base method. +func (m *MockRunner) ReadUpgradeInfoJsonSync(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadUpgradeInfoJsonSync", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// ReadUpgradeInfoJsonSync indicates an expected call of ReadUpgradeInfoJsonSync. +func (mr *MockRunnerMockRecorder) ReadUpgradeInfoJsonSync(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadUpgradeInfoJsonSync", reflect.TypeOf((*MockRunner)(nil).ReadUpgradeInfoJsonSync), ctx) +} + +// StartProcess mocks base method. +func (m *MockRunner) StartProcess(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartProcess", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// StartProcess indicates an expected call of StartProcess. +func (mr *MockRunnerMockRecorder) StartProcess(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartProcess", reflect.TypeOf((*MockRunner)(nil).StartProcess), ctx) +} + +// StartWatchers mocks base method. +func (m *MockRunner) StartWatchers(ctx context.Context, watchers ...Watcher) error { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range watchers { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "StartWatchers", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// StartWatchers indicates an expected call of StartWatchers. +func (mr *MockRunnerMockRecorder) StartWatchers(ctx any, watchers ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, watchers...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartWatchers", reflect.TypeOf((*MockRunner)(nil).StartWatchers), varargs...) +} + +// StopProcess mocks base method. +func (m *MockRunner) StopProcess(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopProcess", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// StopProcess indicates an expected call of StopProcess. +func (mr *MockRunnerMockRecorder) StopProcess(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopProcess", reflect.TypeOf((*MockRunner)(nil).StopProcess), ctx) +} + +// StopWatchers mocks base method. +func (m *MockRunner) StopWatchers(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopWatchers", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// StopWatchers indicates an expected call of StopWatchers. +func (mr *MockRunnerMockRecorder) StopWatchers(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopWatchers", reflect.TypeOf((*MockRunner)(nil).StopWatchers), ctx) +} diff --git a/tools/cosmovisor/internal/state_machine.dot b/tools/cosmovisor/internal/state_machine.dot new file mode 100644 index 000000000000..ca076621f29b --- /dev/null +++ b/tools/cosmovisor/internal/state_machine.dot @@ -0,0 +1,43 @@ +digraph { + compound=true; + node [shape=Mrecord]; + rankdir="LR"; + + ComputeRunPlan [label="ComputeRunPlan"]; + subgraph cluster_ComputeRunPlan { + label="Substates of\nComputeRunPlan"; + style="dashed"; + "cluster_ComputeRunPlan-init" [label="", shape=point]; + ReadUpgradeInfoJson [label="ReadUpgradeInfoJson|activated / ReadUpgradeInfoJsonSync-fm"]; + ReadManualUpgradeBatch [label="ReadManualUpgradeBatch"]; + CheckHeight [label="CheckHeight"]; + } + DoUpgrade [label="DoUpgrade"]; + Run [label="Run|entry / func1\nexit / func2"]; + RunWithHaltHeight [label="RunWithHaltHeight"]; + subgraph cluster_RunWithHaltHeight { + label="Substates of\nRunWithHaltHeight"; + style="dashed"; + "cluster_RunWithHaltHeight-init" [label="", shape=point]; + ConfirmHaltHeight [label="ConfirmHaltHeight"]; + WatchForHaltHeight [label="WatchForHaltHeight"]; + } + ShutdownAndRestart [label="ShutdownAndRestart|activated / StopProcess-fm"]; + "cluster_ComputeRunPlan-init" -> ReadUpgradeInfoJson [label=""]; + "cluster_RunWithHaltHeight-init" -> ConfirmHaltHeight [label=""]; + CheckHeight -> RunWithHaltHeight [label=<
TriggerReadLastKnownHeight [beforeManualUpgradeHeight]
>]; + CheckHeight -> DoUpgrade [label=<
TriggerReadLastKnownHeight [atManualUpgradeHeight]
>]; + CheckHeight -> FatalError [label=<
TriggerReadLastKnownHeight [pastManualUpgradeHeight]
>]; + ConfirmHaltHeight -> WatchForHaltHeight [label=<
TriggerGotActualHeight [haveCorrectHaltHeight]
>]; + ConfirmHaltHeight -> ShutdownAndRestart [label=<
TriggerGotActualHeight [haveWrongHaltHeight]
>]; + DoUpgrade -> ComputeRunPlan [label=<
TriggerUpgradeSuccess
>]; + ReadManualUpgradeBatch -> Run [label=<
TriggerReadManualUpgradeBatch [isNil]
>]; + ReadManualUpgradeBatch -> CheckHeight [label=<
TriggerReadManualUpgradeBatch [isNotNil]
>]; + ReadUpgradeInfoJson -> ReadManualUpgradeBatch [label=<
TriggerGotUpgradeInfoJSON [isNil]
>]; + ReadUpgradeInfoJson -> DoUpgrade [label=<
TriggerGotUpgradeInfoJSON [isNotNil]
>]; + Run -> ShutdownAndRestart [label=<
TriggerGotNewManualUpgrade
TriggerGotUpgradeInfoJSON
>]; + ShutdownAndRestart -> ComputeRunPlan [label=<
TriggerProcessExit
>]; + WatchForHaltHeight -> ShutdownAndRestart [label=<
TriggerGotNewManualUpgrade
TriggerGotUpgradeInfoJSON
TriggerReachedHaltHeight
>]; + init [label="", shape=point]; + init -> ComputeRunPlan +} diff --git a/tools/cosmovisor/internal/state_machine.go b/tools/cosmovisor/internal/state_machine.go index a97ad60ee3e9..6c5e284a815d 100644 --- a/tools/cosmovisor/internal/state_machine.go +++ b/tools/cosmovisor/internal/state_machine.go @@ -1,46 +1,227 @@ package internal -type Event interface{} +import ( + "context" + "reflect" -type UpgradePlanEvent struct{} -type UpgradeBatchEvent struct{} -type ProcessExistEvent struct{} -type Height struct{} + "github.com/qmuntal/stateless" -type StateMachine struct { + "cosmossdk.io/tools/cosmovisor" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +// triggers +var ( + triggerGotUpgradeInfoJSON = "TriggerGotUpgradeInfoJSON" + triggerReadManualUpgradeBatch = "TriggerReadManualUpgradeBatch" + triggerReadLastKnownHeight = "TriggerReadLastKnownHeight" + triggerGotActualHeight = "TriggerGotActualHeight" + triggerGotNewManualUpgrade = "TriggerGotNewManualUpgrade" + triggerReachedHaltHeight = "TriggerReachedHaltHeight" + triggerProcessExit = "TriggerProcessExit" + triggerUpgradeSuccess = "TriggerUpgradeSuccess" +) + +// states +var ( + computeRunPlan = "ComputeRunPlan" + readUpgradeInfoJSON = "ReadUpgradeInfoJson" + readManualUpgradeBatch = "ReadManualUpgradeBatch" + checkLastKnownHeight = "CheckHeight" + doUpgrade = "DoUpgrade" + run = "Run" + runWithHaltHeight = "RunWithHaltHeight" + confirmHaltHeight = "ConfirmHaltHeight" + watchForHaltHeight = "WatchForHaltHeight" + shutdownAndRestart = "ShutdownAndRestart" + fatalError = "FatalError" +) + +func isNil(_ context.Context, args ...any) bool { + // read manual upgrade batch uf upgrade-info.json is nil + return args[0] == nil } -type State interface { - OnEvent(event Event) State +func isNotNil(_ context.Context, args ...any) bool { + // read manual upgrade batch uf upgrade-info.json is nil + return args[0] != nil } -//func (sm *StateMachine) WaitingForXUpgrade(event Event) { -// -//} -// -//func (sm *StateMachine) WaitingForManualUpgrade(event Event) { +func beforeManualUpgradeHeight(_ context.Context, args ...any) bool { + return false +} + +func atManualUpgradeHeight(_ context.Context, args ...any) bool { + return false +} + +func pastManualUpgradeHeight(_ context.Context, args ...any) bool { + return false +} + +func haveCorrectHaltHeight(_ context.Context, args ...any) bool { + return false +} + +func haveWrongHaltHeight(_ context.Context, args ...any) bool { + return false +} + +func StateMachine(runner Runner) *stateless.StateMachine { + fsm := stateless.NewStateMachine(computeRunPlan) + + // configure triggers for the state machine + fsm.SetTriggerParameters(triggerGotUpgradeInfoJSON, reflect.TypeOf(&upgradetypes.Plan{})) + fsm.SetTriggerParameters(triggerReadManualUpgradeBatch, reflect.TypeOf(cosmovisor.ManualUpgradeBatch{})) + fsm.SetTriggerParameters(triggerReadLastKnownHeight, reflect.TypeOf(uint64(0))) + fsm.SetTriggerParameters(triggerProcessExit, reflect.TypeOf(error(nil))) + + // configure ComputeRunPlan state + fsm.Configure(computeRunPlan). + InitialTransition(readUpgradeInfoJSON) + + fsm.Configure(readUpgradeInfoJSON). + SubstateOf(computeRunPlan). + Permit(triggerGotUpgradeInfoJSON, readManualUpgradeBatch, isNil). + Permit(triggerGotUpgradeInfoJSON, doUpgrade, isNotNil). + OnActive(runner.ReadUpgradeInfoJsonSync) + + fsm.Configure(readManualUpgradeBatch). + SubstateOf(computeRunPlan). + Permit(triggerReadManualUpgradeBatch, run, isNil). + Permit(triggerReadManualUpgradeBatch, checkLastKnownHeight, isNotNil) + + fsm.Configure(checkLastKnownHeight). + SubstateOf(computeRunPlan). + Permit(triggerReadLastKnownHeight, runWithHaltHeight, beforeManualUpgradeHeight). + Permit(triggerReadLastKnownHeight, doUpgrade, atManualUpgradeHeight). + Permit(triggerReadLastKnownHeight, fatalError, pastManualUpgradeHeight) + + // configure Run state + + fsm.Configure(run). + OnEntry(func(ctx context.Context, args ...any) error { + err := runner.StartWatchers(ctx, UpgradeInfoJsonWatcher, ManualUpgradeBatchWatcher) + if err != nil { + return err + } + return runner.StartProcess(ctx) + }). + OnExit(func(ctx context.Context, args ...any) error { + return runner.StopWatchers(ctx) + }). + Permit(triggerGotUpgradeInfoJSON, shutdownAndRestart). + Permit(triggerGotNewManualUpgrade, shutdownAndRestart) + + // configure RunWithHaltHeight state + + fsm.Configure(runWithHaltHeight). + InitialTransition(confirmHaltHeight) + + fsm.Configure(confirmHaltHeight). + SubstateOf(runWithHaltHeight). + Permit(triggerGotActualHeight, watchForHaltHeight, haveCorrectHaltHeight). + Permit(triggerGotActualHeight, shutdownAndRestart, haveWrongHaltHeight) + + fsm.Configure(watchForHaltHeight). + SubstateOf(runWithHaltHeight). + Permit(triggerGotUpgradeInfoJSON, shutdownAndRestart). + Permit(triggerReachedHaltHeight, shutdownAndRestart). + Permit(triggerGotNewManualUpgrade, shutdownAndRestart) + + // configure ShutdownAndRestart state + fsm.Configure(shutdownAndRestart). + OnActive(runner.StopProcess). + Permit(triggerProcessExit, computeRunPlan) + + // configure DoUpgrade state + fsm.Configure(doUpgrade). + Permit(triggerUpgradeSuccess, computeRunPlan) + + return fsm + + //OnEntry(func(ctx context.Context, args ...any) error { + // // TODO read upgrade-info.json + // // if upgrade-info.json exists, read it and return the state + // panic("ReadUpgradeInfoJson state entered") + //}) +} + +type Watcher int + +const ( + UpgradeInfoJsonWatcher Watcher = iota + ManualUpgradeBatchWatcher + HeightWatcher +) + +type Runner interface { + ReadUpgradeInfoJsonSync(ctx context.Context) error + ReadManualUpgradeBatchSync(ctx context.Context) + CheckHeightSync(ctx context.Context) + StartWatchers(ctx context.Context, watchers ...Watcher) error + StopWatchers(ctx context.Context) error + StartProcess(ctx context.Context) error + StopProcess(ctx context.Context) error +} + +//type MockRunner struct{} // +//func (m MockRunner) ReadUpgradeInfoJsonSync(ctx context.Context) error { +// return nil //} - -// type WaitingForManualUpgrade struct { -// manualUpgradePlan *cosmovisor.ManualUpgradePlan -// } // -// type WaitingForXUpgrade struct{} +////func (m MockRunner) ReadManualUpgradeBatchSync(ctx context.Context) {} +//// +////func (m MockRunner) CheckHeightSync(ctx context.Context) {} +//// +////func (m MockRunner) WatchUpgradeInfoJson(ctx context.Context) {} +//// +////func (m MockRunner) WatchManualUpgradeBatch(ctx context.Context) {} +//// +////var _ Runner = &MockRunner{} // -// type WaitingForShutdown struct{} -func Init() { - // check if have upgrade-info.json - // check if have upgrade-info.json.batch -} - -func WaitingForXUpgrade(event Event) { - switch _ := event.(type) { - case UpgradePlanEvent: - // handle received upgrade plan event - case UpgradeBatchEvent: - // handle received upgrade batch event - case ProcessExistEvent: - // check for upgrade-info.json - } -} +////type Event interface{} +//// +////type UpgradePlanEvent struct{} +////type UpgradeBatchEvent struct{} +////type ProcessExistEvent struct{} +////type Height struct{} +//// +////type StateMachine struct { +////} +//// +////type State interface { +//// OnEvent(event Event) State +////} +//// +//////func (sm *StateMachine) WaitingForXUpgrade(event Event) { +////// +//////} +////// +//////func (sm *StateMachine) WaitingForManualUpgrade(event Event) { +////// +//////} +//// +////// type WaitingForManualUpgrade struct { +////// manualUpgradePlan *cosmovisor.ManualUpgradePlan +////// } +////// +////// type WaitingForXUpgrade struct{} +////// +////// type WaitingForShutdown struct{} +////func Init() { +//// // check if have upgrade-info.json +//// // check if have upgrade-info.json.batch +////} +//// +////func WaitingForXUpgrade(event Event) { +//// switch _ := event.(type) { +//// case UpgradePlanEvent: +//// // handle received upgrade plan event +//// case UpgradeBatchEvent: +//// // handle received upgrade batch event +//// case ProcessExistEvent: +//// // check for upgrade-info.json +//// } +////} diff --git a/tools/cosmovisor/internal/state_machine.png b/tools/cosmovisor/internal/state_machine.png new file mode 100644 index 0000000000000000000000000000000000000000..6654fd63c5478bc29f461ece57c8702427f5b933 GIT binary patch literal 194734 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 ?u|FliC-I6pND2vbSq1OVw<3yzX)aEE+J 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
    L!bRkZv~b6mBmBZo}b`!u~dF=uyQZ;!@5 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 Date: Fri, 30 May 2025 12:56:06 -0400 Subject: [PATCH 020/115] WIP on state machine --- tools/cosmovisor/internal/runner.mock.go | 122 +++++++++++------- tools/cosmovisor/internal/state_machine.dot | 25 ++-- tools/cosmovisor/internal/state_machine.go | 68 ++++------ tools/cosmovisor/internal/state_machine.png | Bin 194734 -> 165656 bytes .../cosmovisor/internal/state_machine_test.go | 18 ++- 5 files changed, 126 insertions(+), 107 deletions(-) diff --git a/tools/cosmovisor/internal/runner.mock.go b/tools/cosmovisor/internal/runner.mock.go index af301774d3da..5f2535df0eae 100644 --- a/tools/cosmovisor/internal/runner.mock.go +++ b/tools/cosmovisor/internal/runner.mock.go @@ -40,101 +40,135 @@ func (m *MockRunner) EXPECT() *MockRunnerMockRecorder { return m.recorder } -// CheckHeightSync mocks base method. -func (m *MockRunner) CheckHeightSync(ctx context.Context) { +// CheckActualHeight mocks base method. +func (m *MockRunner) CheckActualHeight(ctx context.Context, args ...any) error { m.ctrl.T.Helper() - m.ctrl.Call(m, "CheckHeightSync", ctx) + varargs := []any{ctx} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CheckActualHeight", varargs...) + ret0, _ := ret[0].(error) + return ret0 } -// CheckHeightSync indicates an expected call of CheckHeightSync. -func (mr *MockRunnerMockRecorder) CheckHeightSync(ctx any) *gomock.Call { +// CheckActualHeight indicates an expected call of CheckActualHeight. +func (mr *MockRunnerMockRecorder) CheckActualHeight(ctx any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckHeightSync", reflect.TypeOf((*MockRunner)(nil).CheckHeightSync), ctx) + varargs := append([]any{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckActualHeight", reflect.TypeOf((*MockRunner)(nil).CheckActualHeight), varargs...) } -// ReadManualUpgradeBatchSync mocks base method. -func (m *MockRunner) ReadManualUpgradeBatchSync(ctx context.Context) { +// CheckForManualUpgradeBatch mocks base method. +func (m *MockRunner) CheckForManualUpgradeBatch(ctx context.Context, args ...any) error { m.ctrl.T.Helper() - m.ctrl.Call(m, "ReadManualUpgradeBatchSync", ctx) + varargs := []any{ctx} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CheckForManualUpgradeBatch", varargs...) + ret0, _ := ret[0].(error) + return ret0 } -// ReadManualUpgradeBatchSync indicates an expected call of ReadManualUpgradeBatchSync. -func (mr *MockRunnerMockRecorder) ReadManualUpgradeBatchSync(ctx any) *gomock.Call { +// CheckForManualUpgradeBatch indicates an expected call of CheckForManualUpgradeBatch. +func (mr *MockRunnerMockRecorder) CheckForManualUpgradeBatch(ctx any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadManualUpgradeBatchSync", reflect.TypeOf((*MockRunner)(nil).ReadManualUpgradeBatchSync), ctx) + varargs := append([]any{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckForManualUpgradeBatch", reflect.TypeOf((*MockRunner)(nil).CheckForManualUpgradeBatch), varargs...) } -// ReadUpgradeInfoJsonSync mocks base method. -func (m *MockRunner) ReadUpgradeInfoJsonSync(ctx context.Context) error { +// CheckForUpgradeInfoJSON mocks base method. +func (m *MockRunner) CheckForUpgradeInfoJSON(ctx context.Context, args ...any) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReadUpgradeInfoJsonSync", ctx) + varargs := []any{ctx} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CheckForUpgradeInfoJSON", varargs...) ret0, _ := ret[0].(error) return ret0 } -// ReadUpgradeInfoJsonSync indicates an expected call of ReadUpgradeInfoJsonSync. -func (mr *MockRunnerMockRecorder) ReadUpgradeInfoJsonSync(ctx any) *gomock.Call { +// CheckForUpgradeInfoJSON indicates an expected call of CheckForUpgradeInfoJSON. +func (mr *MockRunnerMockRecorder) CheckForUpgradeInfoJSON(ctx any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadUpgradeInfoJsonSync", reflect.TypeOf((*MockRunner)(nil).ReadUpgradeInfoJsonSync), ctx) + varargs := append([]any{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckForUpgradeInfoJSON", reflect.TypeOf((*MockRunner)(nil).CheckForUpgradeInfoJSON), varargs...) } -// StartProcess mocks base method. -func (m *MockRunner) StartProcess(ctx context.Context) error { +// CheckLastKnownHeight mocks base method. +func (m *MockRunner) CheckLastKnownHeight(ctx context.Context, args ...any) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StartProcess", ctx) + varargs := []any{ctx} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CheckLastKnownHeight", varargs...) ret0, _ := ret[0].(error) return ret0 } -// StartProcess indicates an expected call of StartProcess. -func (mr *MockRunnerMockRecorder) StartProcess(ctx any) *gomock.Call { +// CheckLastKnownHeight indicates an expected call of CheckLastKnownHeight. +func (mr *MockRunnerMockRecorder) CheckLastKnownHeight(ctx any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartProcess", reflect.TypeOf((*MockRunner)(nil).StartProcess), ctx) + varargs := append([]any{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckLastKnownHeight", reflect.TypeOf((*MockRunner)(nil).CheckLastKnownHeight), varargs...) } -// StartWatchers mocks base method. -func (m *MockRunner) StartWatchers(ctx context.Context, watchers ...Watcher) error { +// Start mocks base method. +func (m *MockRunner) Start(ctx context.Context, args ...any) error { m.ctrl.T.Helper() varargs := []any{ctx} - for _, a := range watchers { + for _, a := range args { varargs = append(varargs, a) } - ret := m.ctrl.Call(m, "StartWatchers", varargs...) + ret := m.ctrl.Call(m, "Start", varargs...) ret0, _ := ret[0].(error) return ret0 } -// StartWatchers indicates an expected call of StartWatchers. -func (mr *MockRunnerMockRecorder) StartWatchers(ctx any, watchers ...any) *gomock.Call { +// Start indicates an expected call of Start. +func (mr *MockRunnerMockRecorder) Start(ctx any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, watchers...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartWatchers", reflect.TypeOf((*MockRunner)(nil).StartWatchers), varargs...) + varargs := append([]any{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockRunner)(nil).Start), varargs...) } -// StopProcess mocks base method. -func (m *MockRunner) StopProcess(ctx context.Context) error { +// StartWithHaltHeight mocks base method. +func (m *MockRunner) StartWithHaltHeight(ctx context.Context, args ...any) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StopProcess", ctx) + varargs := []any{ctx} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "StartWithHaltHeight", varargs...) ret0, _ := ret[0].(error) return ret0 } -// StopProcess indicates an expected call of StopProcess. -func (mr *MockRunnerMockRecorder) StopProcess(ctx any) *gomock.Call { +// StartWithHaltHeight indicates an expected call of StartWithHaltHeight. +func (mr *MockRunnerMockRecorder) StartWithHaltHeight(ctx any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopProcess", reflect.TypeOf((*MockRunner)(nil).StopProcess), ctx) + varargs := append([]any{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartWithHaltHeight", reflect.TypeOf((*MockRunner)(nil).StartWithHaltHeight), varargs...) } -// StopWatchers mocks base method. -func (m *MockRunner) StopWatchers(ctx context.Context) error { +// Stop mocks base method. +func (m *MockRunner) Stop(ctx context.Context, args ...any) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StopWatchers", ctx) + varargs := []any{ctx} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Stop", varargs...) ret0, _ := ret[0].(error) return ret0 } -// StopWatchers indicates an expected call of StopWatchers. -func (mr *MockRunnerMockRecorder) StopWatchers(ctx any) *gomock.Call { +// Stop indicates an expected call of Stop. +func (mr *MockRunnerMockRecorder) Stop(ctx any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopWatchers", reflect.TypeOf((*MockRunner)(nil).StopWatchers), ctx) + varargs := append([]any{ctx}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockRunner)(nil).Stop), varargs...) } diff --git a/tools/cosmovisor/internal/state_machine.dot b/tools/cosmovisor/internal/state_machine.dot index ca076621f29b..53644cef0e22 100644 --- a/tools/cosmovisor/internal/state_machine.dot +++ b/tools/cosmovisor/internal/state_machine.dot @@ -8,36 +8,27 @@ digraph { label="Substates of\nComputeRunPlan"; style="dashed"; "cluster_ComputeRunPlan-init" [label="", shape=point]; - ReadUpgradeInfoJson [label="ReadUpgradeInfoJson|activated / ReadUpgradeInfoJsonSync-fm"]; - ReadManualUpgradeBatch [label="ReadManualUpgradeBatch"]; - CheckHeight [label="CheckHeight"]; + ReadUpgradeInfoJson [label="ReadUpgradeInfoJson|entry / CheckForUpgradeInfoJSON-fm"]; + ReadManualUpgradeBatch [label="ReadManualUpgradeBatch|entry / CheckForManualUpgradeBatch-fm"]; + CheckHeight [label="CheckHeight|entry / CheckLastKnownHeight-fm"]; } DoUpgrade [label="DoUpgrade"]; - Run [label="Run|entry / func1\nexit / func2"]; - RunWithHaltHeight [label="RunWithHaltHeight"]; - subgraph cluster_RunWithHaltHeight { - label="Substates of\nRunWithHaltHeight"; - style="dashed"; - "cluster_RunWithHaltHeight-init" [label="", shape=point]; - ConfirmHaltHeight [label="ConfirmHaltHeight"]; - WatchForHaltHeight [label="WatchForHaltHeight"]; - } - ShutdownAndRestart [label="ShutdownAndRestart|activated / StopProcess-fm"]; + Run [label="Run|entry / Start-fm"]; + RunWithHaltHeight [label="RunWithHaltHeight|entry / StartWithHaltHeight-fm\nentry / CheckActualHeight-fm"]; + ShutdownAndRestart [label="ShutdownAndRestart|entry / Stop-fm"]; "cluster_ComputeRunPlan-init" -> ReadUpgradeInfoJson [label=""]; - "cluster_RunWithHaltHeight-init" -> ConfirmHaltHeight [label=""]; CheckHeight -> RunWithHaltHeight [label=<
    TriggerReadLastKnownHeight [beforeManualUpgradeHeight]
    >]; CheckHeight -> DoUpgrade [label=<
    TriggerReadLastKnownHeight [atManualUpgradeHeight]
    >]; CheckHeight -> FatalError [label=<
    TriggerReadLastKnownHeight [pastManualUpgradeHeight]
    >]; - ConfirmHaltHeight -> WatchForHaltHeight [label=<
    TriggerGotActualHeight [haveCorrectHaltHeight]
    >]; - ConfirmHaltHeight -> ShutdownAndRestart [label=<
    TriggerGotActualHeight [haveWrongHaltHeight]
    >]; + DoUpgrade -> FatalError [label=<
    TriggerUpgradeError
    >]; DoUpgrade -> ComputeRunPlan [label=<
    TriggerUpgradeSuccess
    >]; ReadManualUpgradeBatch -> Run [label=<
    TriggerReadManualUpgradeBatch [isNil]
    >]; ReadManualUpgradeBatch -> CheckHeight [label=<
    TriggerReadManualUpgradeBatch [isNotNil]
    >]; ReadUpgradeInfoJson -> ReadManualUpgradeBatch [label=<
    TriggerGotUpgradeInfoJSON [isNil]
    >]; ReadUpgradeInfoJson -> DoUpgrade [label=<
    TriggerGotUpgradeInfoJSON [isNotNil]
    >]; Run -> ShutdownAndRestart [label=<
    TriggerGotNewManualUpgrade
    TriggerGotUpgradeInfoJSON
    >]; + RunWithHaltHeight -> ShutdownAndRestart [label=<
    TriggerGotActualHeight [haveWrongHaltHeight]
    TriggerGotNewManualUpgrade
    TriggerGotUpgradeInfoJSON
    TriggerReachedHaltHeight
    >]; ShutdownAndRestart -> ComputeRunPlan [label=<
    TriggerProcessExit
    >]; - WatchForHaltHeight -> ShutdownAndRestart [label=<
    TriggerGotNewManualUpgrade
    TriggerGotUpgradeInfoJSON
    TriggerReachedHaltHeight
    >]; init [label="", shape=point]; init -> ComputeRunPlan } diff --git a/tools/cosmovisor/internal/state_machine.go b/tools/cosmovisor/internal/state_machine.go index 6c5e284a815d..acb75bb66618 100644 --- a/tools/cosmovisor/internal/state_machine.go +++ b/tools/cosmovisor/internal/state_machine.go @@ -20,6 +20,7 @@ var ( triggerReachedHaltHeight = "TriggerReachedHaltHeight" triggerProcessExit = "TriggerProcessExit" triggerUpgradeSuccess = "TriggerUpgradeSuccess" + triggerUpgradeError = "TriggerUpgradeError" ) // states @@ -31,8 +32,6 @@ var ( doUpgrade = "DoUpgrade" run = "Run" runWithHaltHeight = "RunWithHaltHeight" - confirmHaltHeight = "ConfirmHaltHeight" - watchForHaltHeight = "WatchForHaltHeight" shutdownAndRestart = "ShutdownAndRestart" fatalError = "FatalError" ) @@ -59,10 +58,6 @@ func pastManualUpgradeHeight(_ context.Context, args ...any) bool { return false } -func haveCorrectHaltHeight(_ context.Context, args ...any) bool { - return false -} - func haveWrongHaltHeight(_ context.Context, args ...any) bool { return false } @@ -74,6 +69,7 @@ func StateMachine(runner Runner) *stateless.StateMachine { fsm.SetTriggerParameters(triggerGotUpgradeInfoJSON, reflect.TypeOf(&upgradetypes.Plan{})) fsm.SetTriggerParameters(triggerReadManualUpgradeBatch, reflect.TypeOf(cosmovisor.ManualUpgradeBatch{})) fsm.SetTriggerParameters(triggerReadLastKnownHeight, reflect.TypeOf(uint64(0))) + fsm.SetTriggerParameters(triggerGotActualHeight, reflect.TypeOf(uint64(0))) fsm.SetTriggerParameters(triggerProcessExit, reflect.TypeOf(error(nil))) // configure ComputeRunPlan state @@ -82,17 +78,19 @@ func StateMachine(runner Runner) *stateless.StateMachine { fsm.Configure(readUpgradeInfoJSON). SubstateOf(computeRunPlan). + OnEntry(runner.CheckForUpgradeInfoJSON). Permit(triggerGotUpgradeInfoJSON, readManualUpgradeBatch, isNil). - Permit(triggerGotUpgradeInfoJSON, doUpgrade, isNotNil). - OnActive(runner.ReadUpgradeInfoJsonSync) + Permit(triggerGotUpgradeInfoJSON, doUpgrade, isNotNil) fsm.Configure(readManualUpgradeBatch). SubstateOf(computeRunPlan). + OnEntry(runner.CheckForManualUpgradeBatch). Permit(triggerReadManualUpgradeBatch, run, isNil). Permit(triggerReadManualUpgradeBatch, checkLastKnownHeight, isNotNil) fsm.Configure(checkLastKnownHeight). SubstateOf(computeRunPlan). + OnEntry(runner.CheckLastKnownHeight). Permit(triggerReadLastKnownHeight, runWithHaltHeight, beforeManualUpgradeHeight). Permit(triggerReadLastKnownHeight, doUpgrade, atManualUpgradeHeight). Permit(triggerReadLastKnownHeight, fatalError, pastManualUpgradeHeight) @@ -100,43 +98,29 @@ func StateMachine(runner Runner) *stateless.StateMachine { // configure Run state fsm.Configure(run). - OnEntry(func(ctx context.Context, args ...any) error { - err := runner.StartWatchers(ctx, UpgradeInfoJsonWatcher, ManualUpgradeBatchWatcher) - if err != nil { - return err - } - return runner.StartProcess(ctx) - }). - OnExit(func(ctx context.Context, args ...any) error { - return runner.StopWatchers(ctx) - }). + OnEntry(runner.Start). Permit(triggerGotUpgradeInfoJSON, shutdownAndRestart). Permit(triggerGotNewManualUpgrade, shutdownAndRestart) // configure RunWithHaltHeight state fsm.Configure(runWithHaltHeight). - InitialTransition(confirmHaltHeight) - - fsm.Configure(confirmHaltHeight). - SubstateOf(runWithHaltHeight). - Permit(triggerGotActualHeight, watchForHaltHeight, haveCorrectHaltHeight). - Permit(triggerGotActualHeight, shutdownAndRestart, haveWrongHaltHeight) - - fsm.Configure(watchForHaltHeight). - SubstateOf(runWithHaltHeight). + Permit(triggerGotActualHeight, shutdownAndRestart, haveWrongHaltHeight). + OnEntry(runner.StartWithHaltHeight). + OnEntry(runner.CheckActualHeight). Permit(triggerGotUpgradeInfoJSON, shutdownAndRestart). Permit(triggerReachedHaltHeight, shutdownAndRestart). Permit(triggerGotNewManualUpgrade, shutdownAndRestart) // configure ShutdownAndRestart state fsm.Configure(shutdownAndRestart). - OnActive(runner.StopProcess). + OnEntry(runner.Stop). Permit(triggerProcessExit, computeRunPlan) // configure DoUpgrade state fsm.Configure(doUpgrade). - Permit(triggerUpgradeSuccess, computeRunPlan) + Permit(triggerUpgradeSuccess, computeRunPlan). + Permit(triggerUpgradeError, fatalError) return fsm @@ -149,31 +133,25 @@ func StateMachine(runner Runner) *stateless.StateMachine { type Watcher int -const ( - UpgradeInfoJsonWatcher Watcher = iota - ManualUpgradeBatchWatcher - HeightWatcher -) - type Runner interface { - ReadUpgradeInfoJsonSync(ctx context.Context) error - ReadManualUpgradeBatchSync(ctx context.Context) - CheckHeightSync(ctx context.Context) - StartWatchers(ctx context.Context, watchers ...Watcher) error - StopWatchers(ctx context.Context) error - StartProcess(ctx context.Context) error - StopProcess(ctx context.Context) error + CheckForUpgradeInfoJSON(ctx context.Context, args ...any) error + CheckForManualUpgradeBatch(ctx context.Context, args ...any) error + CheckLastKnownHeight(ctx context.Context, args ...any) error + CheckActualHeight(ctx context.Context, args ...any) error + Start(ctx context.Context, args ...any) error + StartWithHaltHeight(ctx context.Context, args ...any) error + Stop(ctx context.Context, args ...any) error } //type MockRunner struct{} // -//func (m MockRunner) ReadUpgradeInfoJsonSync(ctx context.Context) error { +//func (m MockRunner) CheckForUpgradeInfoJSON(ctx context.Context) error { // return nil //} // -////func (m MockRunner) ReadManualUpgradeBatchSync(ctx context.Context) {} +////func (m MockRunner) CheckForManualUpgradeBatch(ctx context.Context) {} //// -////func (m MockRunner) CheckHeightSync(ctx context.Context) {} +////func (m MockRunner) CheckLastKnownHeight(ctx context.Context) {} //// ////func (m MockRunner) WatchUpgradeInfoJson(ctx context.Context) {} //// diff --git a/tools/cosmovisor/internal/state_machine.png b/tools/cosmovisor/internal/state_machine.png index 6654fd63c5478bc29f461ece57c8702427f5b933..bda01916f44606c525a274a12eaa5d7038b6a3d6 100644 GIT binary patch literal 165656 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
      L!bRkZv~b6mBmBZo}b`!u~dF=uyQZ;!@5 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 Date: Fri, 30 May 2025 13:08:53 -0400 Subject: [PATCH 021/115] simplify state machine --- tools/cosmovisor/internal/state_machine.dot | 40 ++++----- tools/cosmovisor/internal/state_machine.go | 90 ++++++++++---------- tools/cosmovisor/internal/state_machine.png | Bin 165656 -> 192656 bytes 3 files changed, 63 insertions(+), 67 deletions(-) diff --git a/tools/cosmovisor/internal/state_machine.dot b/tools/cosmovisor/internal/state_machine.dot index 53644cef0e22..6770d731574b 100644 --- a/tools/cosmovisor/internal/state_machine.dot +++ b/tools/cosmovisor/internal/state_machine.dot @@ -3,32 +3,26 @@ digraph { node [shape=Mrecord]; rankdir="LR"; - ComputeRunPlan [label="ComputeRunPlan"]; - subgraph cluster_ComputeRunPlan { - label="Substates of\nComputeRunPlan"; - style="dashed"; - "cluster_ComputeRunPlan-init" [label="", shape=point]; - ReadUpgradeInfoJson [label="ReadUpgradeInfoJson|entry / CheckForUpgradeInfoJSON-fm"]; - ReadManualUpgradeBatch [label="ReadManualUpgradeBatch|entry / CheckForManualUpgradeBatch-fm"]; - CheckHeight [label="CheckHeight|entry / CheckLastKnownHeight-fm"]; - } + CheckForManualUpgradeBatch [label="CheckForManualUpgradeBatch|entry / CheckForManualUpgradeBatch-fm"]; + CheckForUpgradeInfoJSON [label="CheckForUpgradeInfoJSON|entry / CheckForUpgradeInfoJSON-fm"]; + CheckLastKnownHeight [label="CheckLastKnownHeight|entry / CheckLastKnownHeight-fm"]; DoUpgrade [label="DoUpgrade"]; Run [label="Run|entry / Start-fm"]; RunWithHaltHeight [label="RunWithHaltHeight|entry / StartWithHaltHeight-fm\nentry / CheckActualHeight-fm"]; ShutdownAndRestart [label="ShutdownAndRestart|entry / Stop-fm"]; - "cluster_ComputeRunPlan-init" -> ReadUpgradeInfoJson [label=""]; - CheckHeight -> RunWithHaltHeight [label=<
      TriggerReadLastKnownHeight [beforeManualUpgradeHeight]
      >]; - CheckHeight -> DoUpgrade [label=<
      TriggerReadLastKnownHeight [atManualUpgradeHeight]
      >]; - CheckHeight -> FatalError [label=<
      TriggerReadLastKnownHeight [pastManualUpgradeHeight]
      >]; - DoUpgrade -> FatalError [label=<
      TriggerUpgradeError
      >]; - DoUpgrade -> ComputeRunPlan [label=<
      TriggerUpgradeSuccess
      >]; - ReadManualUpgradeBatch -> Run [label=<
      TriggerReadManualUpgradeBatch [isNil]
      >]; - ReadManualUpgradeBatch -> CheckHeight [label=<
      TriggerReadManualUpgradeBatch [isNotNil]
      >]; - ReadUpgradeInfoJson -> ReadManualUpgradeBatch [label=<
      TriggerGotUpgradeInfoJSON [isNil]
      >]; - ReadUpgradeInfoJson -> DoUpgrade [label=<
      TriggerGotUpgradeInfoJSON [isNotNil]
      >]; - Run -> ShutdownAndRestart [label=<
      TriggerGotNewManualUpgrade
      TriggerGotUpgradeInfoJSON
      >]; - RunWithHaltHeight -> ShutdownAndRestart [label=<
      TriggerGotActualHeight [haveWrongHaltHeight]
      TriggerGotNewManualUpgrade
      TriggerGotUpgradeInfoJSON
      TriggerReachedHaltHeight
      >]; - ShutdownAndRestart -> ComputeRunPlan [label=<
      TriggerProcessExit
      >]; + CheckForManualUpgradeBatch -> CheckLastKnownHeight [label=<
      haveManualUpgrades
      >]; + CheckForManualUpgradeBatch -> Run [label=<
      noManualUpgrades
      >]; + CheckForUpgradeInfoJSON -> CheckForManualUpgradeBatch [label=<
      haveUpgradePlan
      >]; + CheckForUpgradeInfoJSON -> DoUpgrade [label=<
      noUpgradePlan
      >]; + CheckLastKnownHeight -> RunWithHaltHeight [label=<
      onReadLastKnownHeight [beforeManualUpgradeHeight]
      >]; + CheckLastKnownHeight -> DoUpgrade [label=<
      onReadLastKnownHeight [atManualUpgradeHeight]
      >]; + CheckLastKnownHeight -> FatalError [label=<
      onReadLastKnownHeight [pastManualUpgradeHeight]
      >]; + DoUpgrade -> FatalError [label=<
      onUpgradeError
      >]; + DoUpgrade -> CheckForManualUpgradeBatch [label=<
      onUpgradeSuccess [allowDaemonRestart]
      >]; + DoUpgrade -> Done [label=<
      onUpgradeSuccess [disableDaemonRestart]
      >]; + Run -> ShutdownAndRestart [label=<
      haveUpgradePlan
      onGotNewManualUpgrade
      >]; + RunWithHaltHeight -> ShutdownAndRestart [label=<
      haveUpgradePlan
      onGotActualHeight [haveWrongHaltHeight]
      onGotNewManualUpgrade
      onReachedHaltHeight
      >]; + ShutdownAndRestart -> CheckForUpgradeInfoJSON [label=<
      onProcessExit
      >]; init [label="", shape=point]; - init -> ComputeRunPlan + init -> CheckForUpgradeInfoJSON } diff --git a/tools/cosmovisor/internal/state_machine.go b/tools/cosmovisor/internal/state_machine.go index acb75bb66618..53bd74e7c533 100644 --- a/tools/cosmovisor/internal/state_machine.go +++ b/tools/cosmovisor/internal/state_machine.go @@ -12,40 +12,33 @@ import ( // triggers var ( - triggerGotUpgradeInfoJSON = "TriggerGotUpgradeInfoJSON" - triggerReadManualUpgradeBatch = "TriggerReadManualUpgradeBatch" - triggerReadLastKnownHeight = "TriggerReadLastKnownHeight" - triggerGotActualHeight = "TriggerGotActualHeight" - triggerGotNewManualUpgrade = "TriggerGotNewManualUpgrade" - triggerReachedHaltHeight = "TriggerReachedHaltHeight" - triggerProcessExit = "TriggerProcessExit" - triggerUpgradeSuccess = "TriggerUpgradeSuccess" - triggerUpgradeError = "TriggerUpgradeError" + triggerGotUpgradeInfoJSON = "haveUpgradePlan" + triggerNoUpgradeInfoJSON = "noUpgradePlan" + triggerGotManualUpgradeBatch = "haveManualUpgrades" + triggerNoManualUpgradeBatch = "noManualUpgrades" + triggerReadLastKnownHeight = "onReadLastKnownHeight" + triggerGotActualHeight = "onGotActualHeight" + triggerGotNewManualUpgrade = "onGotNewManualUpgrade" + triggerReachedHaltHeight = "onReachedHaltHeight" + triggerProcessExit = "onProcessExit" + triggerUpgradeSuccess = "onUpgradeSuccess" + triggerUpgradeError = "onUpgradeError" ) // states var ( - computeRunPlan = "ComputeRunPlan" - readUpgradeInfoJSON = "ReadUpgradeInfoJson" - readManualUpgradeBatch = "ReadManualUpgradeBatch" - checkLastKnownHeight = "CheckHeight" - doUpgrade = "DoUpgrade" - run = "Run" - runWithHaltHeight = "RunWithHaltHeight" - shutdownAndRestart = "ShutdownAndRestart" - fatalError = "FatalError" + //computeRunPlan = "ComputeRunPlan" + checkForUpgradeInfoJSON = "CheckForUpgradeInfoJSON" + checkForManualUpgradeBatch = "CheckForManualUpgradeBatch" + checkLastKnownHeight = "CheckLastKnownHeight" + doUpgrade = "DoUpgrade" + run = "Run" + runWithHaltHeight = "RunWithHaltHeight" + shutdownAndRestart = "ShutdownAndRestart" + fatalError = "FatalError" + done = "Done" ) -func isNil(_ context.Context, args ...any) bool { - // read manual upgrade batch uf upgrade-info.json is nil - return args[0] == nil -} - -func isNotNil(_ context.Context, args ...any) bool { - // read manual upgrade batch uf upgrade-info.json is nil - return args[0] != nil -} - func beforeManualUpgradeHeight(_ context.Context, args ...any) bool { return false } @@ -62,34 +55,42 @@ func haveWrongHaltHeight(_ context.Context, args ...any) bool { return false } +func allowDaemonRestart(_ context.Context, args ...any) bool { + return true +} + +func disableDaemonRestart(_ context.Context, args ...any) bool { + return false +} + func StateMachine(runner Runner) *stateless.StateMachine { - fsm := stateless.NewStateMachine(computeRunPlan) + fsm := stateless.NewStateMachine(checkForUpgradeInfoJSON) // configure triggers for the state machine fsm.SetTriggerParameters(triggerGotUpgradeInfoJSON, reflect.TypeOf(&upgradetypes.Plan{})) - fsm.SetTriggerParameters(triggerReadManualUpgradeBatch, reflect.TypeOf(cosmovisor.ManualUpgradeBatch{})) + fsm.SetTriggerParameters(triggerGotManualUpgradeBatch, reflect.TypeOf(cosmovisor.ManualUpgradeBatch{})) fsm.SetTriggerParameters(triggerReadLastKnownHeight, reflect.TypeOf(uint64(0))) fsm.SetTriggerParameters(triggerGotActualHeight, reflect.TypeOf(uint64(0))) fsm.SetTriggerParameters(triggerProcessExit, reflect.TypeOf(error(nil))) // configure ComputeRunPlan state - fsm.Configure(computeRunPlan). - InitialTransition(readUpgradeInfoJSON) - - fsm.Configure(readUpgradeInfoJSON). - SubstateOf(computeRunPlan). + //fsm.Configure(computeRunPlan). + // InitialTransition(checkForUpgradeInfoJSON) + // + fsm.Configure(checkForUpgradeInfoJSON). + //SubstateOf(computeRunPlan). OnEntry(runner.CheckForUpgradeInfoJSON). - Permit(triggerGotUpgradeInfoJSON, readManualUpgradeBatch, isNil). - Permit(triggerGotUpgradeInfoJSON, doUpgrade, isNotNil) + Permit(triggerGotUpgradeInfoJSON, checkForManualUpgradeBatch). + Permit(triggerNoUpgradeInfoJSON, doUpgrade) - fsm.Configure(readManualUpgradeBatch). - SubstateOf(computeRunPlan). + fsm.Configure(checkForManualUpgradeBatch). + //SubstateOf(computeRunPlan). OnEntry(runner.CheckForManualUpgradeBatch). - Permit(triggerReadManualUpgradeBatch, run, isNil). - Permit(triggerReadManualUpgradeBatch, checkLastKnownHeight, isNotNil) + Permit(triggerNoManualUpgradeBatch, run). + Permit(triggerGotManualUpgradeBatch, checkLastKnownHeight) fsm.Configure(checkLastKnownHeight). - SubstateOf(computeRunPlan). + //SubstateOf(computeRunPlan). OnEntry(runner.CheckLastKnownHeight). Permit(triggerReadLastKnownHeight, runWithHaltHeight, beforeManualUpgradeHeight). Permit(triggerReadLastKnownHeight, doUpgrade, atManualUpgradeHeight). @@ -115,11 +116,12 @@ func StateMachine(runner Runner) *stateless.StateMachine { // configure ShutdownAndRestart state fsm.Configure(shutdownAndRestart). OnEntry(runner.Stop). - Permit(triggerProcessExit, computeRunPlan) + Permit(triggerProcessExit, checkForUpgradeInfoJSON) // configure DoUpgrade state fsm.Configure(doUpgrade). - Permit(triggerUpgradeSuccess, computeRunPlan). + Permit(triggerUpgradeSuccess, checkForManualUpgradeBatch, allowDaemonRestart). + Permit(triggerUpgradeSuccess, done, disableDaemonRestart). Permit(triggerUpgradeError, fatalError) return fsm diff --git a/tools/cosmovisor/internal/state_machine.png b/tools/cosmovisor/internal/state_machine.png index bda01916f44606c525a274a12eaa5d7038b6a3d6..b675d0e9ab7d19564173170372d597336b318a8f 100644 GIT binary patch literal 192656 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
      pUwzNHQGeLDtkX2SwWOIVkhe9j4f6Uc(R6IFr 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�nkZwRvQ`fUp%}?xAS1Zl-DBM1(vkqi}`n*+6HJEaq?A)N?n_C zCu8lsJ+IdNBVY*UEP5Z+7@b@q#7^vzSE^!;3!MAM((R`!VaTi7SA2T*kZ#tpdc>}$ z@7^tN*6KAFl@S=Y7E0WN%$dx>jJk&@>CWs!(gVwS+p0U4)%bpE&G##iezKN3Vnyk6 zr-igL8E?Dl>3LI1)=}%YW&Z9pr3(*Uyu}8{MN%7mih5aYK8N`S9i}btPCE1IOL?w~ z9}RUdVC%r7<}?~J_&|cV_+#dAqZPHt3vAH3#TC(W;l!j%&^C4Y>XiJ=z$`8E*14|* z-OvLQ98^S1c&yDUkV zX|XL6LoYUW(9-JKLcK9x@q!V=dm2KniDwsTLNW<{2bX_m{_vlHW8zcaKDo;G>`Nb~ z$K>U6`qkX)sGF8fnPhSZNUD9eZY5h?51z4i_GFZNuj{ZF^$yFMusrkQh(x8)Rf-a33+ZQGk+NuR=psf*esQd18 z`hG(cPWM_Id3`1Rdjus0HVjU{Z#*5T_rW#}@D2Q~e<&~S*%=Nc3<6enGjfF>F8@K7 z8&_7X`EmHi;yrG&$G;iwyLYR{>#0!T9g2olhPPk;u@Fz{>fg9sG0No zj00xIS6j1Z4yqa&x0tKELf?u|Ib>{zk?c*tt9s{|T<5e$CC0|jbas5%+=2VJ@)aS7 z1&n`y2%+10M*L}yMF3*z|Btmd0qZ$$`~H7fXY5OMG4>FptZ8Fpsc5r>?3J<&O;SY6 zWXY1qQc09H#3)-x87;I3MV4%pQrU_`QP1lP*FD#B-`8{e|IhKvaopG3*IcH4zwh_+ zIhXhQeZJ2&Eo$HY_gN-3dh$f4 zR3Xm3C6R<3w4?(Qeg2^WNu=LHy_*-jWZ>$%GnV*$hOMkSYHW$$AzC02g91;9a#ny@ zdId4jtm$X!-Q7?ooje0c0oBL#qVJ$kJbmumy4;5K|8capTt9yN+uaQu;aKn&)`1{w zHvr$$RE+Z@WE(li^^t`)c8hmDPHwU`5Jr^wBr~PcTej#p_3Pf22e~ys&#fcrsNd8R z*9jiJ*u``7=aDBv1Fn91b5S#B&SlIIn#e7vB-$o5iO^!He0GzE2Y%FF@4NNt)yw9X zHLZZmU!gM)n_{pNohUbC-!feJ@S*Mf3cEnJ24AN91fBrylZB5)$f*aEGk+BQmC z4dTiA*K{}w$!0*VK8y$Cy?#BBCi@%FRW15tvWH``a!c=Ne}1bFMuU1%;cg;g{Km2< zRK8acO|u8B5AbVk0FpG}PEv^`X7qWeyBa;e=tsq@n=c=Ys(_7aMrhoXWD3MR?#l0L zR0j?6rckpS)qvpj6DVcbb`x?Vg}Ucl_|CB5-3 zYbNp5tE(wFK>>NwKxV{|JZ}e#5?TinyG0$D0uIPe-Jfx0Uk!n9B${@qNDV@hCYHF7 zDF@SP;Xk^deFKEKx#%`QVRpk0T6!1rIJkieK~GuIpbot#p29GM7HNguu4(Z5gmOv$ z0#B<5eNR-=&whObc4{T0y~v->o$Jkyq9*27PF$+#YMMFIf1Io>VM$=(=`&@zUvg^R zSG)*(-6a{g;sN4gRt#KiZLR(-)_?oYGBeEFODigpDRenR<~Yo=(_rbF>D^89xgN48 zyt|>N3{R16YJv`Wfhah;u?V6k;|*cmS}&hg@&SU5&KIfRYhIFzk(dFdKOln1XHy547BzmZ{|{bQS34Ho_OurAKY)5b>+9HS{xnkxq>)&lP+#G zu0avgPCesPh~BPNqSL$^Aj%X2EyMvw2ZlLYwZOepxfr3ic2e{C(g{Nhznxq ze+5dS+5O<*LJvrMCocXWJo%-u3O+~yuI5OJ9 z;<7sDIn*M==v7Y49toYy5d$710cL|}IPSP+A4FZO12#qg0MwOErQ z-o(JKs^&rBuPe15xe#HnY=*1fNck|^6M3bK%H5Ffy}m|IMvuuu5)E>5cfYxag^ecp zho3WvRS5tw($U~6APyJ^Zt29Dijx5sXFhL+(hje~0}&Nxh~yFXdJmAMn`IZ`lLaFJ z>bHTN3{soXB+&Q>5GJ7lwqQZ@PfX<@aV5we<|M!`Td7 zwLA3?J?;DH*`EK*Fkkjy8o>W_l$$D#9G$zYJ*1U!syVJx!D*7cegJ;Y!6U#;HxZ|0 zuZF;n0RaJe$=$oppW1fQy!Hjn(3NyIeM1m{fzv_;fK7@0-D7Q_{Y@)u7t}^rZ#x_A zglxSL&sq9|ywj)RI5>F1Tzm4QdTXOLty{GM#a?@2@lzT{0P1lY>eTP@;0;Ip2=CY{ z;OFW}=N#NOjA&^3P7c4-y1dkE?Bd@MDbq28&x+Bckg9Pwkz5+SBGYwFlWXGM;M0HF zPSU98^NoB|y@Mw5Qt0o7mAC8C<tQ%p$pD;0|B;8b zY#Hx&LUWnK%Zxo>uvgH8V`I~wbe~8OpB z-AjjF_OzyYKb5{5VMz8gl?k(dkv5alj=Wn7$C#9T0~*K6eo>Y;C(=u$AJkYJiKVsN zVcy;Mn(+HKx^6io#HbA=&EN2Rq7Nb)F5s8@s9D{~xt-tU{cqXNi2*3%e`W26EQ+Nq z5d5k(9q;b;dW>54?qwT?W3n2*o&uT2jbv0^cg5}f(2&zmUo zd`C`dB$YEgE_%nkh*(%mG0}0{?sjQ}zEr-~eYVK}$4m}4+dze`g`o>0Iq53usgU`y z@{?M5j$iIe+Hi=h7MY7tqabg)5Wp>INm)=RM?LMIw8wg0LvyYLMJatiP4CW0w++oQLdZueW5Zr zM#rzrY{h~0Tvc0v-mG7*o(9g`k^yOKh4rKuyP9HDGwjrSMXriLFSn?sN^&L@3$cv4T$P-wXuAY)>K(>T=e{JA6l<&}e-}pD4J#qfg(j#P!CBckb*aAKt+h0yPy6 zaz{*$tvYSg7bumAVrj(+sSstR**JB7;o|xnIy4892)wGZE@<#;=P@Zqy?)S3uW}2w zcM$K}ZeJMfjevAkPnJW5g8&+>NCQIy3%58ieUN3#x4+_r8`_z?-<$N77eNdfix zj@)-yMmB|C?%C!$t}h>xty0js9ICu;+E1$_ z;K@HN0~2&OZ!*w_v;T9t%>=qv_LVv&?{1d`DJizRQY3Mr;4H-PL23%_p61tEGf5bQ zzUKxh)8RFP<9JRz0(c?EpK^un-u%eu`XE-dr|pI&EgGfc4g!%sam)fd5lKO^yHZ_V z!YPu_PXgn~RufE8t_SMt1DS@iQXfEo6woSEQuC|EfbH`9vN1c zJ6085d%NyS={Qc=-`r-g+TfLts!F#eugf~v^`t#rHzCBgh=pJ6#e|P_nE}&Qk z+1vYeU+!8GaQEM?=?M&tQt^dY#CmR?M`UZK6lnwi~loN3S*4{mBov25nRwBIy} zw<87^3NGvs5Fd$tz;)>8uR##X(kRfU&poP`nXrWwQJ`oVmKX!(cPDb@Fr)ngCz7AV zF}=m{nHJL%hD9BuGg& znhyB@*2tkE@wtPR5=Nyx>onOnD#*9#g*!Fs!7*R=%xlkOuDeTpjvkS)B4L5P!%grT zS#X11W+lfzbBIhRlBoFw8n$We>A8{?pNbtzHE8+Rz~_mp-HU%-ubw4mNe;Xjd~L78 zlyBo5(3ceK?3EF*`BJlX{iilTbxAYy(@#HXyw*NZhbqj)%BuaQc?U-Rxdr&804hVu zRT?0(sD%^Al||sogk(+U!QaC;c4nP95g-+l;2g=V1v`6|88PD_`X1Ki9io7tg;sJL z=X#V9V0Vvk%@{AU*S^OV0%XfFydaP}=)de@V9Wh2{ALw&D`|w4Pb6`Z?AX$zP771_ z?LcYLs$<6#-nc|gSuI2l-#2L~X%P+e&uEzHR`=6eifW0XTac#wjKDw&IZcL}6i^u= zs%F@3sP&w4ORV+{v~J{1UrFQ{^!k$yLTdp}DY-vC`=hyHH8xf>PEDv*d{Z=V{UXV2 z_44xS>rAJPtAeR#O;X#w7Jm{OEcMqr!$e56#o?d0c-QTmprD}Z`_;R0gXbdxR!E2- zn(Zdm%`beKtTHlq#M8?zd*lF96i_+UkLTPAXiM$Qau83Lc1s?2bwymwAQ~xmlSpUR zf4h~1vieVIHex!L#FB7EomFVSa0QB)A;=tMWjZd8!m&bbq;g1X->l}QW&F9fHg#S$ zA_0h>FH8%Dc;3|Nw?d5DojCDf{f*iyS?18s?688O0X(yHyLRKacpTSz%M!YqF2afk z=FtLv3*oK-FmRU#dXO2)JP?||J?QA{e9E437Q}s;maI==DLn*2-L|yDiLeAW0!9u~ zKm}-?UGdv*GFB2%d4_aJnVpc<`w!rFFzz38|1IoXFm`|y?U!!y9&o_Jq zyxe}%RW9+*@zCT9V%zcjLiMC|lEHMsak=t>uWqj>#!s)WsW!2>z~~0WWAe3YJq*_i zI!8E;D6j_PB6}Vv{=_G=h|CNw_}Ei(v>l{fVL?9I&;RvX(_=bGp95M z8l_L4K0@wEl4YAJW2r}&y9gahn_W+YA!=T;!V~cK4uDo;d~=*Z&d4?}}Coi`-Z?F8%d{g{xkromeyDtrO=J^<$op z0>`I$adWs+6JVse`j)&c8SszisooZIC0U^qYT_$B!3Z4bcx|Q?BO8d>r_nyO&rK{- zGIvwkk~V~{U?eYrMS3{5ZKp_Ils%6)77}vhxL{9FV>mwC2%iBn_fc}1A06KklP4pB z6<6O}fa2P}7RFGdVT3Gr`EHPIbTD)t5F8 zTR-{4^wON@DRt$ zcY?QH)I&mO`B^jNoknuzYOk(C&WH8YDmk?8KZ56`1CD?H!1A{Pfml}256d{ch|a-M zSmpj^@S`q3%Ji(PX8*F?(rBvZ_U#FvUtEOS5te=6W$!aW&wFgrX{bqkGIUvrWlJ8! z__h?k3xFsnf;Lg7-eI?yh)^i`X+6)?_UiI$ulElkwg>=58WoL z0^|Y!#x%bn+xT(bT6U8%KX3N9j*2S@B?P3DWw*fwShIqDWlTP|T~jA#sjFP&lT5SR z%x`pb-|ZaDvWm}oC$|?I?7~-~eo3M-r?Ig`fCJ*X2h^xfO6l@~e=oOr=fl~pM7#Ninc;pb$;&RYfv}dapvD1rih{m zpz<*BRUCxP80v!*M)+aBHvZ#brqG~()wAJt6p$VXCWuxL1m2v$Bn=@6Z}x$Wxsa3r zz)vk&wfdWAYLZMio&?!$k5)$^n3;lVCM<%DIo$=gPE7Qj{gevxZVrD&tn-AwL@A(9 zRyCvSZSW32E0Y}oLYh!+7GVcLJt)meD4Q6zA_Ve6)awDQ0&O%Nii{~!y6sJr9A)eS zlyaLuE}Jghi<0)nJ&or&j$tb>FR+8HAW;;`a{;zQjO-414060f(*QOlA->p~ED}NYOx>4saGx)WG;# zu%r?NG-Z7`f~s_XkMd7O5j@d@s)4 z3OYLKV?_D3yuP)cU+gE6i3l$sKn3h@09iz~ToFX6HKbyu@6DqL$11^}2G8vB>J=!g zXvW3=kcssbvVva3u&QR|1G9a@-i z=&{f!V-C&y7rI>5z0prU&82;j$v(;q>3nFl@caHzjT3v4dGI@l*HYF|QE)&Yp^Z61 z@)DO{F5MKg*C=Dr&Gr8$VA*eeyOQk- z|E*(5SPKd*t0alArcO7}9#aay$wX|-DjZLiTzTZA&d}$qhP0M z*?1oA-yniSgQKp`o)R5}#73xRYs9(y*Ixy#=sV#tV(4TDMiDpdXjL#i^$}36Nm2e= zR0vmzoS)bi&Um-16_?nK?BmSP1MmV2QP#kL-@WWR_4_X>v)D#G zO@WK;inOuZHUrX;3yq4<3>;HF^KQZF7qQm4tJ{Z`T(P5QP8450K3XoC7CH1(0KOwa zB!_?hOJ{N222JKdwb-|9p})}H{sy0X3^DG|JWZFEv5s9MotHryD?LNEX+zqO`~wEuZ*sT+@2CT-gqCT3kFN4Jm-@(1mG>tU;(fLd88v-p6Td}xp=jItoE1Y$2bvyMcDoA>USqMpfjOwYe#C4 zIUWerLCz-sc!DimJgl#*?LaYwA0Pd{>?fenL3nR*mQ7T1si*Kt&S^4X&N|s&BfTsi zVDhoaTd8Jm*S3nN`)WUDuzb#zY;1972;?sus?opQgd!?$ZvOojdiP;E@H(iAU|xtG zNrGDWDSI0ez#&2enn>lnN5)D3Ob8?KmkOTooJb4r>lHmKI{&R8)Ab0NhV(JuL^>P zGGHg6g4chIzgYQ6I&e)4mumo;ocy5zb-byMp4I# zF(==y2sTZiVv3mBUbZ$&m8D!H3gSj5^Ld?7Rxp#Xo12Y>#0z2$qaaz3LIoP1WBw0% z1=W)>L6k4Wfafo4uLT2!G*lAgS}I%ky}^ztg{HvaeJKoxN1^0MEZPQ4@mIg{sSe># z=KUB3oWIFOV8@-$3+l*Nxu- zx~*QlT6_S&j;>)*BA$AuVn6!**VDhQk9&nW;3j247b5DP2M=VRl*up2Bly<- z!Z|G12~k#dM`q&4#<_$K3JIx;mMl3Dv*1SKSAoM4xnc(af8qWYyjk=kr6@!YL^PGg zTf#h3ynQKA_#_>X<`nF#$YH1D3q-UBb=T~pIiVLbB8SntQ=cyP9Ki>MP)xjcZy)iy z6a~g^pi#$hK>qTV_p~fJuMTJp`B8HGMMsC!)T}nOuIbic(^cRN*&s&M(0$-Q8D`c% zAkCWQbd|Re42+HQWW-iYO%}F_^y1yL4QIW)nsZ;{=QPh*Od9354>5l7fzgD$!k4~& zozB_QbhD-%0#tr3v^>QE&MEHPRhAW-(rz4@Jy)x&yxa!xAT(Q6O%nzt-5nS?1W(29 z1Y;(Y@A|zxG&FXBTb1De%~Zze!H-B1QTP^Ok3ti&lyTdPzn*^MLU$=A)3ghwD2HHq zY{-HJdc8b>nDg{vEX-~l{1W8YeMLAZ0%5ocYbM8f2FHy@-95|cf)N(*Kz zc7;wtu_4aupOJ3YOcCec>Z+$TNI(ORy{tIGJcn2b5MjQ<#2r2^Zr?z9JDiPr&bhKy zibO7ku&Y2Z(p+HED6Y=r;G#L{&t1wYT-yBymUoEpuqeb80I{8|o=n5YujqLL0uxor zMiBOeH-;Cq0#2ov6AvA5Eny*G<*(+Pj@r%*09u!oppqm-<9PP;WDx|Yd!V=*y5xR5 zx#yMA&0V)aFY7Ll0%?YH5#Xne8VBE zCl~ZmU_ll@kot1G^V<*~Zz3DR`>`coS9as{USItM#^QT$RF6TPLxvK2?+(M3b*|x& zUiZ}si&+qRMtTtpCPa%3g1)}*+oEbLZX;%#?Z;PS4f3$`q1-ExpRYUFc=0%dsiZV5 z>oKq1yxECPk`|iN_V@2=Y1rw%zQ!CXi9-t3M7mEJO3fuz7Zz7uV5%!kaN1=p2gRct zQ{}ORZrjqeAL4#+)&lZN-<@x%Sx8XCsH>c64&Keyh3mVt;bqbLq48LxeGii8{$O3am`Wlu+04im{v&OeSfN~&5|573(R36q&{rS~iVv1r?1UCy@$T8E zHZ4-Mror{_>m};}Fv`UNBV?|mHe|Q--aj2wfF{6(mSgSL@@%*ftNID1Rges?|ol{+ExE1q@cy939BX@5NCnlr?$ z6ibNhsY#zX9ad2bZ#ZUWzc1h@ z$oC~(a(2t^v~5ur?0ydr?tA`JH0DP#WWn><=klDgD89{uzvi+m)uvcMR}F|5Lioa&Xsi@q%8(Lu*i(-1;pX6}&|?&&wXd3U$w&eX1edNPBmOMPw!cYdUv`EV?#A2rd4Ujl-95v0ju^p&MWGmPKPrhck)cUW+pm zjK`Lgs}k0NNTt%NP?CrWzDcz0-~x&T*<^)EU8+!!P(SYfEe61l<(pDD@?k`GDdTB% z>Nh(2vdX@Cp#E9|143rkH7c2|%PFJ&JD)%3+7OeHidgUu+KL|OS7!f?kopLqAwoa0 zOx!rV@aHyn`2JECQiT+uq9AUh+^Ig>T8cR{l!?f%!-yW-vjaTD-*A59&i-KI%RDcF z5i%8al;R$u&u;+u(MYkSC`x|x$e#h&12-APiNYx&^D_$V_ZWQo4_NLh=r&aqQYMqo zhYjo1JL-3yunLOOy<4}dSp$)5^I#y#H*VgXY-yyvTaW%nju3z1z)0tTqH-Zhh#i{1 z38EJ#o6s+B2@dW-|0M{P2v!>dbdpCg+(_#{#`O zlYFG`KN0+Vt5mNW7s#X@R57&aargTss6fOIU52fg@kNf<;{QrT(Ataf!O;MzzN{i8 z`4loXB=y0ER}ZudOA9HrBn@#8MNSLFSNd@ADf269T|`I$&dXjxC7#+m03GSV2xziR z6)!C@(BdBkQ+!-zScy{OT9z~ZXHp}eV&+8fn7i@d^bAf*n zMZP_F=7Ev>z8xW*hRwR@EW%swz9Ox1c_R5x>=qi#RUR-S3&rH`$A)r zV!v)SNiw%B?{F*;N%OAWh!GF&Gk>Pko_0r50t{5z4Wu~3C{7(jhK!(i?D+9p#2c-y z27jP@aM&+pr|1tlg1jbCn(d%=?#OK#Dw?CZ8j3vB#2Os-rD#`gL+*vG06%aX$N0;Q zTj&g$f|tmaw1S=8cZY&!wg)TMpuyixG3A*2Ej5#y6Tk=Lk79UrEysMeEK?YZ3DXV& zNeBLa+jOexcCt00?@lU|t9a=SqGxP+O=CNlPzQHXax+VR^b8$D`5Sv>McyW*_6Qv( zIG^C5ci_nmj+L>!1)wR9B%I7^ze)U+gORm2m|MBbjm)x%VQs{pbJ3u{6?JqkrFY55 z${?&XjTgIE`BzX|^xzTF4h0TEuWoq>(LpET-;mg9cJys0Fos;ek5EUsq9Ic%Sxht! zihUAo=cUx1+&fHVq+`K$bEa-Hox>%ovQubR&{K|jClq^qce)W_#v*03+c=N#&8xcG0Y$tYIHx?f&}w(&N)BmqO;r9(LFMXd2>E$Uqe%}2GVXru;Onv+>5>CDLdd0NK+Fy1^2rBV zT4{BBADy9RTz=^zlcw$6F`Tf`rkx*NSA%|gVsccm^XKQ6NuWXM4?u&bN*CF@=YNj zAuFJv)JiNz3Mk^3s?mh7I*z(oY`!RwMMsSCRtdBc!QhYi-Z6*jq7lkRHaf_;3FP-A zN(+TJ9Y8-B-|bC1BRFqCP4W}B&4w#ZHt$+v(RNIWF^2U!4ORZj-POb_tG$=1-}L$0 zy3e1W({JCRjX`5idym;ZC~5bCcdB!*&hFlM@`s)sjmMl$oZEfwwVn06f3hh2`iFC2 zq0vm^G3j4p1`d4eYj;R9I#o@}@u}(lfz0Dyc{#8+giy-@QM z<0YKQAONx=>9YCq^C-%xExic}xZ9?>s$%9{Pa%4AxER{lh^Ny$RzKh^pJozr9mySY zMC*HxTk^*++Qv(Fv4Od=m_yJOa`F0?y!rClesD*M7+D+%1JyTr9oA!ixxBZfB6r${ zLd`EN&7}OH55`Pp8o?*0Qt~2Hf2ehK`Lbm@F#zK%DNy&7v7fYV3l){7%4sVd@Fr!d zUm;@;(Cg4r<0Hpab^gk9jS>zZEqPSP)Q)L=O-e{OtlG%e*H2gm^xw!$FJ+a7lurr1%~WLl>dzVLTeWFZKOQno$!JzZ&kqm66kJ2?Q^t$q zj^>4nzP-BCYrPm_rqBl{{l4>R@CV&nB&sG)OE?jpe;}IIB>3};cfX?j6`wyjLzB1*%1^6pNBhF{^jg%pk713E^=sx2+xp!@FIO1$l zc~0Fgv-uFo5&HBV3kgGPb(xQ!3i(?^3!E?k6nXv@9A3@N9+;%Z1&a)YLm%dzrCv1tas2*V?Lki2Nq_Mh?O(R${o*MCvdSo{(AG2$R58Vk-wJ_! z9e=~$-(IBjQb;&Z)mGfYe5jB2Z?q!{_M{Cw7t6^!d+=V~+i^>OS;T0*S&QKu52o6h z03R;2v5|(8`X^w1$t{OR=LRrZV#kn|?5A6^X3db7Wx^5U=PO^F^LddqQP<$A(+fr< zlt&I!fR-|O=Yb*?35lJtdbQq;Cp#)Ay_?jk7H&53?98+2U0J7ohLO&sOJB*oPW#|)d{MOtFUS=Kr2j4A>z_6L)=n^R9`}If_g|bH zc!eEF0l;s1l`nfxLJ(^Ac04%ij<#D-wCOfWi>h8K$+^?5tmfI;hBmmZH!1Mz6hCaK zDQGcvo4RND{;cf6+qNqX7217XL2b5>mO`E|)c4D@dQ~UhUJBAlFJ1RoS}()}vcoR5 zUj3RtI8H8)$GSqj0Y7(V1%r73IN>$mKN|L-q9@`5Gx+b#^dj7g#l6R^zekeNMnd#S zOF?fZu0$BE7+4Qu;0J%ztHco^RZ}AWH1#u7#Jd5`G|||CgM`f9+uVEgW$Uq?_B)Y9R1DxSQVIDU5`gTB& zBI6qrv10PY43H`8Gsj7$q!D0fGCuuXMPl4}m;u|T7(wHvb)gSo_8}cDNJwTzn2Pi2 z)x#AqBPs615m1x_!yBbq|eyidBko25WUjswz-D477a$rok7`kAgfi9p^sFvir~pW*sf`JSR180ny&K{f-893$-ys62SYF1HW(hZg`T$lj%dMLGY^Xy*L1}0- zL`9gB-1A2y+7jm_9W3DtVfFo}H=e6aXt_GzFNGF<;GU0Eyn}`e8KC^~SkrpTaN>F5 zdF=E3BArL?E;2vpsHDVVd(AEOx@sd+WnVb4D2?&V7izZqG3GZg#O54t`TmPq zyAbL_5wdeL%5Z)=mKylNtwQznno4^wXLG066ajGs@&52Zt>on8$(SRKAnGy0oH1*E zy_W3IQPqOQ00dAX2}+MZqRS8*32hKbykE&1mXFk?gw^$56c)C|(gV$)uImS2w&~XG z5HxY)M1BX7D(o9@ze04wPNBzP9Ks)wQO!=y8i5Ok+u>)quOKDCbq|Bzazou|b4QOl zU)gcwDm5v~(37cp{tc8$&Q3^A-@%iOqKs1z$N}d>O1WroM*x(D@u)s!k&^W5a0g~3 zx~M3{eUU`pYiB1hD&}==VB(Afo`CBW5BFA4*p<9Vrly4mJv-~IJ;3pm2?si! zbuXqeB1RCI6p=xsHxw&^wNbK0v7r=Bv#-(+BG!j8T#C6DNesast{CbW)B?Z*VR8|o zYW|itr>1iM#Ve4$iF)oI28&61?jMtRut#~<8IiGtOG!WyPSL%-in&+y=|a;cO}@ML z$eaf4nuKj}Yyil};F$m3N>#Uch>Cw))JZP+o_+h0sD1;DtNvAoO=bQMp}E9B#&0N3 z8||GyTh8;~wDmdUU1sQVCt)!k6w6aed2X;IKq>(dDKJxccg=25^FbW*BVN`siS-ne zl^szuyqvo&-NmAc^Y9xXpxu*IWGvxQI5{FDHrxLeVE|}F?Jl?KPkGH10aYtwVq!Qu zFY6`2vM|Ze8B8a7^wM;=FFcsq_%$6lV89OU|FKg9=ZV0Lf?SDITxPF983uQ3inS=J zRQ%+RFWa+cPhI7--%m^d0&`O1M~mDCDcUYSKO-VOu81PuKm|a>=DzXiwlLGcvF6HX zQ^N6$;pPj66Uqz0@b3^RArQ3okl$wSW+idBKA~)NN@DUwn*@-=)YZ>olE^BH?%3@a zn#*7myjV9rFK(a^g!1o*Qxv?I2?Qks~j)$_B~ae?Obz2%%`!;ic4( zlyxvL6StNeV%*p=qH6H4VbdN@WBlK&V&00^`&V^Vx4u}ii(hkjmABg8jyHL(3 zH9Bkcu=>uGQ=WcSBm4K?MsU?+RGjpd0!H9|=P5eM$NkTyCs#$hZm3-SMR&#>LA;#{Q`64i&sP=|pG(dTDA}occQb;Tq$raq_(D!o*bq$;KRVF>pv-r#bcIRr1PYqu zOFTAL@pL|>%4oP8a1H_ZU%lD!7Ey?oVw=YzYLOlOa92lgU!q9vh18@`b z{Un@8XfM`!S54cJJ!;jebFJF73u7yWmDo<*K?5v3mnRXsjw;XIh$FPu`KjA9*ep-x zFM?p+OvIAl_g+$kw}F`Dfv+A6wYuq&jV$@C=Wb(3CXmP*`m+Uw<_*`+3Uo=*O!C6w%{Xe zUoD=nB{@KM%~T~H=O-><_cd|8HX{xNx4VD5PhnwUMMAa-1Wg<2iE<|+BhNcL%|z-M z)Mh?Gs-7!3h@&gs&o<=hSP(*|-h134k@P%`e`hbZS0eNu2jB2t5a-{1dQ7 zo`%z#(*8zQ-z#B;Yy`mJgYrVY*knd(_9E$vun~X{u49tY@9P}0Pe#g9g)!MC1HSdm zIK^S8Eyq5-_vq2gL9AZmC5L{2C7ucj5k>)UETaqN7sgzofI6zkCTo8O4OxHS1)(qY zqAl(#cX?Bj4ngM@+uKKAyllPnT575>8Z$3U;Eu`I1f`o_?yFZP#AiY~52exu<|bO# zHBs*~RV3cHF=4BxXTP&w@9fMYh3w_uw6xC( z_m*g4L%p3s9hX(|5#;HXy(4cULFMb%xCw&n%LG(}duQUY5m`EcQmW#z7weGFN^^vH$`H}}TnU`h`{COzNV07n@7*tcudI zZ61#(6iU&^d+9znHP!2CGY@_TGkgcv2<*gMeLZ)kDKQ$MXnIyqz_OCIJZ%91`<`p& zmgI)j5!eul7uVBYZ`#Y$CTdVpH$=*!DUj0~89R~D;fq^I{n zJS{*H0HQR-DB)#G!dCa_=%}TjK@Ha(lJ@orm2w1dD_ zi>ia6HeSWgvLz^#L+z2$v3>jB0M7xCw*V+nB2mBbU&gOnr`K`ECf;Kmv5)6tYiMZ+ z%rA8r2%#(yMEBu7HuwlzRt&B-sS8$92wN^^-|*&K|Gj%t9hKN| zt(KwyK$jREU8Dd@svW>40HWNjXFw&5e=TzDKN+LiWPOVauz@q?@ekNHWnS229i4v>Cbc(*@-b|H z1P$*E!y!TDIO&vSh!JVb-#2fby#FzhQ7&fo$tIxNZ7-gypppeso(NEnBN6{x6TrIy%fM7OfJf`0Q_5%9IM51nN|GHi6}p=`UDqlL2scerW$B|OaKsr{qun`V)ZL(jNPpH<^|tYpgX z+!uv_8%eq@BZjcFyHGDX(^dD%bK0=g_BM8Q(O|WVbIYNjCR>Xf|8muvOE!;({J@^p zYspXuP!DB;FZvFaXWScYLvs0>8-!g$Um@D*OdQ_*`SB@#H23-QBhl8buG($-O-5H< zht(%&VYj{JIeS+AHhKkn=h&_nLK0L!_IVS`L83$u3?zIM6d2gF&^IZ7UG=hb#Q0TX zADuULM>1Bl9IXr(6e>YvS**T04;50nDi=0K4wS&j*!& z)V`aYQ8@zqr*!2-M%p+XYlD``^eA-T14KLUPgDQc+_k35PYfR}lQkgmWC-fM24I81 zw9UfUr>e(=k4m6p0)iX}vX+UWPucJY#iH!XKp0p zX0~$~H$w`|Kb9@-Ov6)M{k`UG>^Q(qd#nj4zR=dRB&xE0!U#c=3ydg8rQ$Hr5}Kmk zjsM=O3Xk6^*N?s+;S7 zkpR?o%@XP8sU##Lv7PuRk3;qziIxi-O^_)1Y#}FjtOXpAv_Sp1>lZ)VW>yb%+J}Ma z-*K4S^04JWCwK4m(v2kYa2yiHb#*)1`^C!Nev7B{>9fI(FDxom?kL~r4uhZ6Zd61G z@$rYR_ngJC8xwrn=fLI0AB+vA&KP|gSvjL%Q>9|fv1 zihM+oSa^15i&As1>e*`jMs-u3r9QCV?bSpw8oYL#+X1K5z4rHg<9C*7XX}RSkVy4o zrvS6WURE~Ayo`Ng57-%a)Xi<@0%CmQUOTeKtw{c8UzC|$__!cgu+0fNMOBlGNgdky z#nH{nBTJ?P>$cOm(tM0kr%s)4H?2x5AD`mF#azD?Tvl|pKrteC6YY>#yu)ez7v!Gn zfoi}8k_En`c%jM^Wy7$8#3{Tl1;R(4t-<(_ll4DURunr+ErJ%Z+C^e3(XW&aN~CGRB#KQ78)p#E&qcjKrP)dKr^-sCr=OGct2e*|LNCd- zFDmq|>({U6DAB^c0OGOH(d3C_bRRI_^;WyI&rfTr#d<`18Ay!~Cp$=l-BSzxH>|0> zJCYJ**XhXwU5w0rb$gA7l9EILv~2l@PuN8Nv4|TWS!E;-596a|iuYSu_Q>+BL*BVg znGJf|n2a;kZk7y%gMSpqoZ$WmK0~b%(L9M&2BLZdY!b^&p1Q$R=i6Kjl!T+GXdhfy zvEaz*KD~O$dh&9m!(!TZ=+I4C{@+tRgKJMYrI&Z_BFqQleuDH;Tdag|!t>n(-jP28 z2?o}V>}e6J1ZAEQAe(sO3ARD8S##^yl>YegW(Z`%2RG6gitQ5Qlm|VVcz6>QWt75f z*Y*jQbd`JlQi)Xs-=u%kKsTnsy$NNpsWg4xpt@L(O`Di4pM?00iV^O?k@UrGrW@qR zyDQlUag^_Tl(j~Mg`0y^32#vZ%CKc?{@+8~Oj79p(apvDTueE6?plaU&c5OEVm>YP z6P-Hab(hTQM3IV#X^=ZkNGQaPk1PHgok6Q)oa34)&(iNZX~Ngciu;!r0pvPFEi7FL zz&z_nRsQ;h03S^99==~raOFG7>M>L^<6!~$2U4@6G(p+;uFjhNUBXlwvCf5!OsS4d zb3;D#yQ0r>0r!mwFM~8JEiIk8>Nc(=A4+FEzT)Ojq0aVF711=&zsRsNuf^4F>JPZ0 z+o8#0$8NeAXQsi__@riRmU7sa0amxm-#?WCcQ~FN>@~&K(~*G(r;&_5X=-Wlnau6# zY}%+6_@A()!!wpq_W^)~Yi-nZ03D*NJ!LlbIggEg{MZ!~<->qiEbKZqZPX|iES~Ym zBexf^14MLGR3}dUoGV4ZlJt-rkJ8e%6KCZdGpMHEBOb({*3f*skx|fSk47Oo8F}<) z&!t7(W;N_sRg{N@>m|P+(I{^HoN|*Mp--Q}m{~pBktBjC}QFsiH5|!Pk7g(mg$cNMBmcq4LGvuo7qSN zBO18K|36UqPe$8Zaw-X}<`-9z%a5Px8n7YksEyY}YEzg-k2(`^Y7ec}@6qEAYLIq~ zEO2EQjF3Qv!Bu~97&3V9LejWH2Uve7G3JRvqFB^DEUCC;~&xu|3 z616B9WIBf~C!da7yoGU_YI~`b~qFDv|*0 z?8uqP^aWYv#F3t0Zq~dNSF%I-Z^!1nOyXoCdBB(0QQpE6*&bJ)M=b=@6op$U3C-K}rz;5@+Si&cxdAn6Ma@ z$Xdo)-9frF!59T9r%W?fmaS463~fN%LLJK}yv%ldlggWjs19uP@V#t+A7uF_S}PxB zd*>Lw7%|}sVkr7U41#oA@aP3&0gwtjVV zcDAEEHW;P&$Zw*HpT{>SKK&K4nGU(Qb=>klKV8zIoTqV+osPFILidVM1L!$g33_T^ zj101SjBEKZZA%x*tZO=}Mzh}>{R9#N{^gC{GWjY3spi%vCi!8mOc0I9t=R)9yzRbZ~Bc*&^%U00^^I5;8KU4a2SQA<*4T!WT^06>Z6sn%GWqr@?r zMpNh)(2-ej>teI2*?0a85zR1OvGpQS<_2Hpl%3|v6fYUZ&4+>ho90$*e=W~1Wj`gl z5(Qd7smcZ60J+M;nu8}xmr?xm`agA3Vc|VzneDoqiY=Pz!OO<}?n}@oXuIK4?(5gt z#NR9}G12gGS25=BGkaw|cC1c@#pWMcfU$Bn!9plO@Gc7D4D{L=ceL8RqPg<=C9_wF zB_4w&c&oLhJL2Y;@_l1gmv$G)1L&-nkV?G*>``KUis#QsrT5#+R}t4>X`;)?S<-)P z@2Y0`8Xp4nSwH^Aj5pNGD-*kG->LGW#y#13>1{B`xg&6_v-snXTQeEhhZMH0)8 z{JC@VWfNq`tW#CnbB=Ct5`b(tIoS71&9s2jIel8mhW1?FMbWp>&3A#al*2DeCuhI> z4nqIbt=i$CnxEG()Gdsf-?9=hB~oM=xa+v1fow!)xr}xAO-@5wmwA`)fC4^^0-zwV zdIkqC@LpfHdFSD~NZu8^l8y+6rR{!Lg71MyC{wKUcmeQR)l$%7Uu?D5UoLBeHK&M$jQyqEm zz^G&n$c$6U;^^eY-Kw?edFZ4sT=%!5{E^8HlJ2fuyToK!UK2>>3-AwCnbxh}yh)oj zvt}``!LA6naktyo11|)!VM&xS{RTU}B{NEELuk(Y zx%YZlP{uzxCpWiF&xZjZl=kt}>^vy1R0QSHn{K z*2So3lw_F!sTi)C(Kc@|L&%lYV9%bBzaBrDZ(Z5*m&`s#4onq5DE8F!gLigsEAl`1 z+Qw$?TmzU%Cci<1HW3|5PCSiUug}{{!Yxwhdg(t-xw5m9GpmjcM;3}3HdxFg5dFe1 zDCfzqoP49iaE`!Il~T_mZdp(F=|7eBlPz744&qo#i}EK&?8>d5f37*^__<`v#x2k& zI)e<&K@pxoBF@8#osAD0`Ps7e4UWvp+4Ki2sCq9?YjK-422IR(RIN$`I*u6jY)HqZ z=);hKl-wPLe2C!QXn^VX@oW#<5C5@X$r2MD;aw+m31G-RGtTSfF?fEZvL^YD17|;U zsK~rkap9+KDTg8=#DdUJ@4BE5;9VCuT%=V8_fn95?qXArv-*v?^ghP=Yl81pC_c39 z$MwHYTNj_G4;?g{o^@Fp*I&wGE1vYgvX4qCw&e7gGd=tFH@5Q)QgvwYE*NoFtz2jt zUfpf{cJ$EK@2X&L0!R>(xPRTOq`6h_lfwp31fFwhEMYs5ORe0$!_flg&gE)dQ!041Acz8+gpKnW<2z%j65+mg=~VIQk??g&LXQS z8|c<*SJ57`S5{xFcW<+=$jcK?>hDtwNQT`I^<<5-I`+}+!=kc+poeFpqFl?G#Th(Y zc#97pgykr6Wf_Gej&BDRsrgc<58E0FLaP3%x@OLbf$Oc6Cw=ZtdQ$E|(f>_H6?C)C z2e5PbUNF;gQorY~)s6mtv{iq$NYolU_$auLJkIUy2W^Dh!_OSPwmlH6z`~6BQVEI2 zHYT{jheNnyw@0D zQUQ0!!_=%^qbqTGmFmhmNB#Hg3BLJ^u3iN{NaRXz_%e$Q3?_{czmPs-h_}gi@|yA@ zm^PR(HY~2{DChWV<3H^g1l2M|K=pqJ6dpaL^QMvshAAl4+%Z;^sZ!q2-!wdo-byG4 zOm&s;r3_e64M<{e)#TS15Tw$;DczPqBT|RQ?q`emo!jWnL>w4f;dWj*D>QN)?5%Bm~CCfgB${c`^g#~1!Z`>cL5v=Rn%mb0aJpp^XKPko!<*DLy^`r zo{r=IUc^*+9<6TLAVLRtV(^IEGLyZ|K=-D)LkUYV4b5vsZ^8(;#i{4C+R^y_n zzovhmKAQ=)Vrq%<#ByQOB}{l)ckY}fBfpGm9VzkT;#99Y^Ro?5E?&M#j~;cRNtA{J z`qt@6*yTm0YX%ZT|C_FKYDUBi^(q(h+PLl0Pggg9JZu@a&Q0&jyI*ONZ!n@QjVAsa z%l;9$SNHmFEzvV#oJWciuah3h2G<< zcABmoE^p4oPAjPpj$zw)6p2HVu6hUQ;v~|7*d&AKl11myH5Hp^QJj-gy-B7~c%&#B z&I6rbld71O!dozF*N|EkLGW#=V>)YDJVEs+Hw`|nDehVfz!)IfW`VSz@#2;PW==h) z4}C53n`qmq1qCL;rzgQ(sjfI}!mtTDNW_B9`aMY|!Q>%}i_5?&P=(bvAAo`fU0ebc z1+}Uo-Y{Y^Aprn2*WDFFi*3#IKDi57$%yyp=zd+13IUaOafiiuf3;!PcR*_UX1paN z8>v)q%*2%SMtZu~BVqICM@3tAE|NtiedIC0SAu%_^R&clS#&_mJg~KIKGRuD-Hi>n zTL4NUZt18gHQ>o922y#oibg7($a3FW%uu2SV^*4M4G`NVv>eIEG|4s%=kjA1K%4ql z-B87@8{L-(r+J7|*7I$~aj=L39$hro-X)j6+#VSoh}25QJOHo>wu{YMmF>!B$OS)j z@lGqJDUma#yv<*L0t@tHGq#l{%tq3TABHUkV|%G)YHXZG9ba5RGR*{tgcYf4fZ0vM zW}ScO+xKT+j2SaJ(z_CDpzn`wUlz}U$=Az#C{pe&$&xfcQ9eKuE{4|pBhv&ozaRw0`9otR? z2dt*0Gb~iWDer+@Oo5*0R)QfJ%c>^-R5~M5Z}J7i9(^97h4@aN5`WhhSB-w? zDmV*86;8up9K-{@z7aMw+Sk+5?^-peAAOXLKxiN_0HSiFddup&$Jh4;-kh6gl(ue& z0Lau{z#o6X4<^Fs-n~$5RJpxsf{cyOrHgFnjh&(Sby)X*QLP9gZ#_5}zJM(WCNu}P z7Y#NT_1FHhXPqM4RNoBWzmS(D8x@xiJMP^gw>(I-5&liGQIb~NgHd0AnaY2G6wn`{@@(Qs&h2idY|)Ki_NR913)LX7R&gI^;PA6jh>F>B=5Iiv7cA96W5*xE4csv-w~s zLg7&}1c2{|360o&D}=w5z;pWaJTjT=gB0uvHI%6R;84Y0Toifm%`}PaX)9&H0Wv_@ zr}XflA6WtS!3N4bw;?4D5v>JoNo1_!N`p}N*dpQau$_5@F)Mo47o1EKKU>A#7FcfI zeSL=SVs}mi#0e}}6Eql>3{|XopLgd56Y4>CW%+HJm~}*0X=^wOGJPfTm?1;%e%8w& zv;&5UUmZ|JKF>lP7CMijv_@aUh5rQuIR3>ly|vP+Qu=a>$_0DhUBfPtkx?` z%+)zn)hj9E%vpX~wgr{}jM_by)#5lt!Bl8rRvX);&W||IEn$vvohPt}(@@QD2rQ7G z%uj{B5rRqlT6m9l1DWdam50eWE_?s}ZW$^InRq5NV7Tu2GbgJ0B?0Ix=x{dDX&TV`eaxw=Xy44olci}OP4J9V$q<*AM5hgS;!a- zGHC5ol5m#QpVaE+`Kt_7U^_bnWLG{JsrF`g<8BEhH{;_KBzoxIh^dhs#VcI_!6-As zhsj_EbzGuuHE}-WNg|V_uYkGXdcS-Qhl1#1v}qc!?KiaqBHT%4X1l&v_aoqmjW2~^kfZAW1N&O39)hLaeM)WZ^f zK;{I4wxEpQNsW6cFDCo&#MM_VnZZ3!n{(wLg@nF&%?I6~r?g+cN(12jK0JvsD%{c* z`vm;qJj9#=!EcyDa!yWXjwBbwePfGKt(*6HbA@@NED=$jCOO@UZsl) zRPBG^UuTk^;nRjvnWByDF#}h+QG~*&Jm;m(y0mHsKlS^rmI)N1+@M0LC~Yl6bd=5D zo63d9#c?6L2K7(?B&r2_#M%up`PO+R^rxs9(`VN?;c`KT=_JvEy-R7(^4C#8TaxV_ zW>RUkg2dC-`lK5nw!xwh1^L!>Qjt#l)0#WOJ|7qxeD%SDnEQ)UycneGTUIrbd&Xo3 z_fUly7O=1rCmmO=?B2U~Xx7E@2~R>y>qG^$^3-i^>Obq1Q&7OtTZBMZ9gyfhPM=;j ze&uBs zkA3v?y0}yO>Azo-&PE5i7F|pEGAV~VJPfyLwOl;7BO|_|ccNnorGS_5Vy@=sjJmu@ z^?s)V7|EsQi+``HUcI;H7P|QA0s_I-?b{dPfh)x`C2iC<-QuC5u{;0yCND3M`9e%; zsOkrv&H?1Q3R!Vikw-Chd_ywDzr0hb@)Dg7Y@D7%q`!MOcU;Zg1}cvpJc!WJ{|(q)nPO z?J#2H(b*-Q(P$xv%SWqg$V++ElWR%0`ql(+pEp6h;0e8xb2|ThplB2`7 z7rjJQ60x+FRR7Y3dBH5-5>GLKCw@rf$8~mae@g>#6N>)lCQa6wTen&7{jV{&P+&3# zjkkag?R8zx%$U?kC;!t0Zss@}at`s{t8BH|EQd zZEz`6WP7M7u$(z)kt|+fc%x^~miEPJiTgDrJ0vWuZvE(f`?A`({}6Hw)YY}#eUjIq zh`)aQCqr`-szIc5;WY@J#|pjMy|&MI5#loC<~EOCk5Jiu833dv#>CWJ!nct&Xe1qT z6Mm-T=&mHufU92nA6w!g3I5Mtz1q&xBFTz%^>TJ`p&bwdGYV28RHk(M1XHLj=nu8!q7ZFnhU$u(ofw z)`oANBKOpLNF7i#l#urh@TR48XY0q<7|8%-oLfCe)izH)LD zGQL9xiLhI`Qc{r(9IgnVXzlhtZ0=6|C8p?n2~b)a`fH4Z#sqX5j_!&sLU1K~l*HkQ zwWt_pH+PpUTIh}$(V0Xk`}uzn_8xFO_iy|E$IPaT%n*?fAu}swR7A>186}w|gv^YT z(d9}ds|X>x%!VW_WlKmZ4P}L*WYq6@mh1Z7_x=0-{Mz$muZ6C65J-hS;WHMJ+d%Qybo zZ%{I{PRFYq4AvR%oOX0!!|iECr;c8^eAQ!`(Pdw=pgL&_C#}^#v$y7UBMa-_=2|IJ zg3POvMN0@;b3IQx9;F?(bNrbWAU?f6uE3s-PEa|l_~4+s$o%--V! zlMhUPn|#OFQPnIM+kbYnLUvGbFlCD(&5%)79|n1$RPF=6%6?QazD4}O(A#JHO>rD8n@CXt4=xFO+KD z2Ed>^@_74+4{uBxGbVpr`SHr*zuR!utiupXxIU!xdFH%)`Q(h{Od=-#^y@mCR7Bas z$J}+m09EEdDI`Wy90M=kii@j7P+6FADstd1qagKR!=e(ibjePCrf`eEdoJQNq=0S% zBA-WpD=CJUUc;4F1Hb3;@yauwCBpZK@(p0Ed8-v50FBWrfaILf8P0SqEsB=E`$o2( zK;Atd{Zs?1>WUdMsF^MM=s4$)n9M~^a{W7eZV16Wr2pd^7^#2 z>QR(XBP9@NNG5Z6Xawfv$bosl@~I(pm+^J7Wf5jhc!Q27rV6yo(3@#_0@3lyU1H71 zF=O1}!aoyPB=*vl1T1W5@y)WFsHq@{`P>>zi$5{g9Efhtk|n=QBDIaASeV&_d9QpS zp1`QVdBl>J=(vTknybeD%vnn+@W|gA5TGXDIlWSz<#5CWJb)?S zXkZa>h#MV;?Zk-_BSuDBoP$t4G}!rcV*hVnzlye-XB`9b4VC=}h{rArgCGSZTg`fd z_xYUsi_|L)p`uo_b0Lz2t@@-!p^kxq{I)#Aly5ki$%HmEt@Rz!R5+Lg;~r%Odxy#>yx*Nv|#sN9Zn;$=e~Z+K{`?M589@_xJn zHYrN#e&Hc=!<3pGFPmOa1!@osa7yRv8bs7MkqhBXaO%oBGm9I0fz(9IdSrH1 zV}3jo*gdWdl9mMgZ4i=I>|br|(*#!wy?m$$@s~iR*$LBPG^AMjZNW49zaQQ@U)saO zkY5?c+u7&Yy?gg8s>jhI8J)~QeFy<#O6^HO28&P(&}8S68$+wxPd_f`ce6$vSdM2N z_qs}D8R(4kbwrR^^S29ABVCp&2(8r~K7HDA*(^ka+^`*Y2k7Wb8oaIf$?Hc(xxD*M z*mONw&5Cwhfuc$w26S|mS}QXJR03-N$r7hNsuw&TR_;$W!e>RM({|o<nO4?hUu+g4+g`I~4Hx*_=d~Zfq~sHVE}s-^j=qtAG711z%2>COQq}Z7J!29RPd#@g!QVkXgZ82`8bTz{K7+rXLdZ*ekyJxzr7`W? z-u?T>!8wUb8pjJ(&+E{kyMVWqEi}%R#=jZ}Ex`A8V6?^V1_%o{L{qgX+zel%5{x(iDKzZfR%UUN14wUFcjv=o$vKz(~(=r^B?VEZFr> ze|fs_!cG+|B0TcV0gPn3Gl%II5t|ci2WUXX0?lPUa{hAf9F%ZM(e>(c-7a)$;r{_-CL1Gp+fVrpoi)B z@yxRxqJ3%FuH8Mh2`BNA1jNRFfz=PeDRx2Po~VF#HS%EFs3 zrdOF2vj97;XL^J>0Q%n4VVSSR*OExMgX`LTr!Tt7qTd{55Q@~`4 zY*#~ma_ps}kr(ppg75$H`OEwc5WIZu^lT5=Ixj1n0bp{oRVq*EpMR{r^|qiuHa2?J z-;vZEp(ZW9iWn|Py+qzhio|ZjT+1TlB}U0HX=$y&7C`xQ9aPJ^E!a=8XmFrgz*-XS zJi-z}yGk+j(4lVuHoY9S21$mL4WThwZl_w`tSFDTY7EE`J##~u9lfB;KgR#!hP&2V zrAzqe)ko9R?x{?*oj#SN6LHW9s5 zubtZwsZ=E*ey|O&1Ne8!2U$9kdHnW`r)7x0_+fNQA`_C~G{GVa7E#-MU%Y(owEDAQjGC}#FY1Ch~ zEVO7yw{F3Szd&VqnxI%4`@Mm#^_k`!9udO)0_C4kf-U)m&0{jl@%(v9xK?TX_>On) zHsF5ooKY#UH&=x(L;le{Cs5_+V+jYYXh4wA-vz6bze`R*gCe6mq+sJ$@!%zh^<$rC zPUW(%cEETn`{T!Z)V5-^%h6C$uuxH^fvA|OeWn6BlBGY4pRTAXfszb&qlN~QK#Obk zZrIZ;pJ@IS4B;<(7I^H~ZBXs;DCoKE(&-|qa~Dw?rh_Y1tk_43P6L^D<=M>JG_-X6 zQ5EHD8O%ydPPPU52TQKan-N_YPenp{aB_;wRjt1n;OCbh^E_Ky$=W*#6vH!iStVGG|K^1s8Dx|QN5y;0IoY~TKxYLQDc-vk1|x^5Zs z3o?<664XRm`BxDG=-`olio6wmn$Mnq;M zKQ$kY1n!j2$N`;2sQ2I^ot%UNN&-3{dzMpD(+R=2#7{uo|3qlb*RQ&k#Y_z{>lT>J zZP2j#m8l3_^Md;rcYD3SdFov9oo5;8 zot|Vio^zzG@bK^?N7RfwFrQ2g;_lxqJya*ZcHI@e_BH4LWlHA`9mL*C=oM<;?FSFe z2sw6XEh)9D%@>n2OdM-wxizu8B0Cz2iew)TM;XHP7gq0C`3O%>0r>^%>Uq_h$;f_aeUifYr~xFhva+I4%J?pz zWs-dhT%`D`mo8mGP-x(_JX|I^Y3=yHa^An6k?k^-+)w9XUZ!O2#`WHh^$~%%=W=Uc zYBZs)o0q%4E3J6217{3bq@y!Y%G%Wsmr2up9X$@5Ng7u(qdDeGUoeA&R%v~m6VJJoO2 zcRcx74}2r&SSWQrfJJ5z7k&8}U?1W&KLetJU*f$34}XLM@24h@ndWQa9N z;3WIfn2vM4?MCoJvj;d(w`o)FdLMrR%;m_s_FlajYi?XU?H-l5?4aS&B@9_TDF~ET zb`{Z8OsqXgRHxYs0l`pD&ZBin(+*0L#)$X)2BWt1M?a30-ZP*hr&53);Abo|?=TGy z|KU|ALc+>3X3$*tZ`3ethpzGAIWnU^3K{e2Jm0CJx*$A#vRyI&n2w^Z`?a56-yU1B za8hzok_{qCdfDnn&YUr3N`Z{%@~ei{Xq>g^9IHpi#%@K+c`W7I1?z}-$~d0`K!a!Q zxol^42CM?vjHAp}19c)1MznDHEQ|^ouy%ZyHS3KYgD&)Dd`>#v?}Fm^+G^h64T#?h zinT1ROPcl=WO;Q#M5&lG9UL0D!g0sPt$%lM`5fP&7Y4f3c+@|8De$7Ijb-gk|Xf<7#=EP`Z9DQhVl4>G7|Etp1TLEfA1;@ zqA&&d`3Xcfq20yf0mr-P&mKN(LKjozg+C9d!dIGQKODcqEf+jA&3xbqIYNTDKKkgWIHu zGuEyx>&3hjZ8jfInl>h$2HjFF?6mlDz`C{b#g_GI)4wMwL^Mv4S7_{Op!xbuOvh=? z^IP>Bb)Gf=Z?zT?y{_EWYCwD^j%4IQ%`FIH&M7zBkd|4$Ki>P0m@@fA&6CtlZOVJr+5w0P zuE!P#tMDsFY{TR#sr7yI;%KZw%Ow7U}*_Ya~Etx zY$R8{VloSDZS@hTQ%|2gk?<6RM&KZtv-5ZR@>kGMh-gW;4N5EZQv3#3f?ES_{LrC8 z0rT$UrJ~HkMpqnft{T%mPHIR~HucP=p{KoL0^eny8q#z4ny(&o@n&Xb{Z61$;%H1f z!F&o8LFzud%ZTQq_TNWLQ<;`_*$h!uows=LF3KRPp4PWUI}aShrGRI-l%B9?&YWAQ z9Qn+Tl28!zMW&ZFKN>z5<0-U)@Cev|ji#L9FeH6sDui?9EE!%%p@&w_M7`HbWix~T zVvzq>;@5fW>Uan+!ObsSDxTcFiK%%M1s}uG#z2)`N0!c=d%Vx;BT-Sq*q0T}j%6B- zM>Lh5e=kKsaOSDKCG0We>CD%kmrB253r-5yBc_8!XD8@w$GpeO-~S#va4$=7;IS|U zIs=a2&+Z%Dg=2HSQErLXCwSbHq>^0a=6qEb)X>!Ea@ZIU6cHnSFEn=xBl`W*#Ab)B z;^3CbU6A2dSesxgF)U2nktHD_Wga}Q-1PCxC zO?xCa^Wk9U-c#E(+OTKAw@>u6LB`9+r37YV58lsQ1^UrZP%Fk(C$GPypPw^(_QZD^ z0D&wQdH|{`iN2l#dmmsw_h`K@=_rk@gBG8>UeVa-=S}_fbaLCJd46se^x}$PVnro z2yS%O*Cbc1Yi)EFel^k6dHm>+%Ta|+G#G*8N-j$dwOmqJzrdl)QqBcxKzh&;plpdB zTDhaG%NQ6H^|1&85&Mu--xF;%QDnd=o%Z>7XMhKHX*}sv@X!ChRzt5TFHm|OvZS0V zirNGHmz?ea3?fgASnfo73x~D^i)|=Ci|Pk3$I%@;crc~=mDt}zgisV5%Rc>Ns8D3? z;9nsKXsK4eeEn)(7~z;XOjO#UW8xvugRKfq*bis~9UzS{3ZmJZS5AXdR&+IPoI-TI zklr#pnDiSoXB0rQKpC7qh0MZ}!fVJ>jOf1^bh(4ZyI)BwRaK^`e%(TaB1dM&x$evu z%3uoDn4u2}^j@X5H%Y;g6P`V@Con1KyN;O!)5On1K$n+hYPbH&M&7F|)8@luzfMic znivcmnU9*kE1FXMKK?Ka&b39w&5%4-zCHmIpyo9#3Rg!ari3DbbSRkpJl zQ4`6IREF8qNpUNi{e`bo_``|;o%QD3ZOOZK$8P|YZ7fY~@|ejt3JMB7$EnU@17Zw< zb?eugjvD5f1)IxYCy&W!pb5ENnOtOtK$>3g5zGQec<{$Pm*3EjWpn1KH^LvEciSWR zZaxmS|96|x7Lni;>;EW1?;@p)#R!%j4;^+}8A%n03)EN)NTe%7;UWXsDAYs^NBIXh zCxS%|w-+;&JZOj{)>B8M{HE~tU7Ja#8VT*dxvXJqoUE?8kojIeD6pC9zgdWTF8!Hq z%mX(=Lqi?04jg}Qo7_AKcpO%8sguM39EL3L%$a1K50bw>`|}<#|HKS_oe^lxD?7g4 z%LxjrDWG2PGDQ>yn`5!}9A!W9RNvJ$Yfv5IgBw88zZ?f4u^+ zp~*AXk7YCX*N-p#2z?p`R%siwlA--%vb^_z0U|3R1t6jtKYDa+LG~CAR{(lE#EwJ= z3hKuM;y>5*IB^SWu3`?{>-X;~?U|4m(_Fk^dPe*aK2S`g)b>tpI$Ar@q@9U&z$LfP-@@;kM-0JuoTq>;G;|3(O{_(fT;1Krgp5p=cZb!O)N3#a zv0^f(+yVo+- z?4-cY`uNe4#YeKHkw2)`0iVwO!*KZ(ivtFQ8#mrp$O8;+XVdGK!CRGph$Vtgl6}NCOT!s*g3rWosmE9t)G`SBYk#mcf}~+5?EYUk>c6M{ z;i&7m((_fTRbwMk1=YGgSE4+EQKLHBfy9dW(tC9pdI0-e|ZTmA)+yBh$ubp68+2 zSp4#;Cy3HSLLI`d9poRlN}=9O!_tn@f%=tK6T2( zLH+w*O%!(-P=BNJ%#E{Ns&(CEwfaSI+KrjQXf2@xUG3tcgIofzTI-ot-Hw+}aNY&= zr-$#07{>16RKm*=zZ1^5ilDx~_IyGNm=!gr;>d*yGIQZGLab``4|F=J(wb2or;yC; z+fQG^KZDnYS-X;OoD^bd+Hc*Cm<5Zfh6)kP>xAW@A;>Ex-FT6fwv44>B*888x{|s$ z%c(1;ThOQP!&23q2ZA5RHlxg_t;`B%cY|qwQ`sde7AQD#fSUP=R)bfZI@I7_Xg%N| zl{aD!!| zm|lXYJ*n!bHF|~w6zQpweVSS9?>Ot*3dA`;3_*q0j&{r_GC6qgps{_tsw<2Y?=fX% ztEXbINi4UBYX9x#mpvKw7!ZT!8}T!E%Ie_UqH02c2jz6msblVw(_t40RvD{v8j1>! z%7|y7Ng+-3F?PVxN!?Nc=YE^r+&3_E^PP8jwB6+5z1MGYN@rMFCY6>GFV$XBweCmS zOfj#6MbzIT{kJxGst+DE2!nG<4_dcj2w--Q!{$ev)$(Pioo;hS!@6gN)H^eM#*9TP zRJ7ZwI6S`?c<9=h6NF9i$fQu53ss4)+hI)MCf5FRN*_glz08+^cTsH_%gC=m*q|Xp zmNTM@_+l)0G&^`7&bRukhht_giOm+D3AqowqP`zBsvYCA>J(?zE%ef*7grXk@&ZLr z#)DkSY*zS+w@3fzq6FW!(@Xp-u*tnSaLon40BW#wZy85 zD3eQF8w>Ej&9|L1XVSt~G7-aYMMq7|I+Rmn;TeHB+7}XD>?V>!Fd-H3yMCXCbBabT z_@y-LRAzJl)#ZRots+yaQZ_MogZUm9QNi}VIzhXQ$g%OsUbrdraD9r%9%~*ib#y8w zc_sZ?XKpri*0;+)Vxo7{+<-o$EamnVeZ2>g0H}!0=$#VZeflh8t0*{X^kHlW_Ez~U z7U%#$hNE?|4-yFmjoUrGw=90Ad5MSQ z0Is8*zhWJrIT}$Ib9!BvN98+Uu~O*>$v8dFIZfNt!~QWRWmjNeM}`fb?Yq#(lR?5W zc^RopON<0@`Cf2rH|wUmKWY!%Yz;L`4e*8#=*y`c!{KJsJ2tKFFQ1I^0v0BV5Qix*qX^+z5YlxVnyyw>`f z>7jw5en#FrVx)Z*Wv_Hba4L+o9;HIDQ?s9*5SS59;mEAh>G<_eRWJCIhCwD+bOeP4 ze$dzlop4IW+G^mK8&McO_4OtsLU<6iC!R=tSFi{>*A=gIq4H6u11Z>rQwa!$2#iVO zMd!!N+#Ecxb(YxJ99_|{AGqO>3r!xlQY43BGbP7=-7IRNoc9-QYCDwy{Q~*-P$?|_ z_AQUcVeT;e-hui*apc#{uy4)!Y(+x0TjyK3*-C~2xULs*KhNBspE~qg=`Mb2Cbr)) zvA`)1D=1;{m-v~VhFP&0Qg;?p57246ff`2+8TUvZPtQaF#9-5vb)Ao%9*-WAIs9xs zB($Fk5YNGcX_G!;Gcsnr%p2;>)f@!iN)O5g+rPmjB~X==?0xS?8q(_zv@1UkM+oU! z7^9K-1DQgT*u(q(sM7rd8BtyyK%rDiq`QCATRE4qfa2*krm}XWlWE9bbds?N!3I+!f9Kac6qy$;#G8X@`vx;wmzK8 zDkM}WGM31PCSKViMk;AjV$NLqQCGp-R-I;6U(Xb#96;6xso1@HeFy@PJRr)&iG4Qj z2Ey+0^M?xzONq@(j&vAtz~6s7NE2fT+2lH4Xw^Z%@{y75`F=As3%#v+&TfiK`MURwdW+NgJiFUw|w^ z$QYorQ|4OKFMyDv!_e-r+MlW}-@kjmXnIn2g?qm)b zF$V(k=`?&Q1r|a$`E=i5*aDuD@BP~{i3wRRQ|k&QmYb@nO@M9+2@RDU^)whc493=s zdurXQJ?anqN5j;5{qO4EJn7VCF4l0)TmSfe6ca~wjj|?MW(5JhG=KNzO!<;Ui)OqT z(N^E%U3UZ%^vc|V3gO^EntTflDdYsUqkm>lLnbO&bZrOfQ}F)1OjJv23?liQbk2v2 zW@Z`iAh?paMf1S2Id$+wnxAe#`WX4BMo*YBkIECov%FT>ElvmIv9OUckfA%MQ%AgNv|1t39NwX~z zHWTRJ%}N|nA#KzY1&n`czW)V;~9Vk!p-Jn+c9g4B!p7!OHJm^ zL{>{i;jCN&MyaIi_BMIo1Mf&TdK;uT8AM$4)wH#AYYi!sAkeWmtIcytz)A6sdY6qi zBN&hqcm>OusZoiMhz$UkP#>OQ?ZhU|ve(~#k6?(G*T%}^?(Nnmrwx$5BCHA|4MPYb zr@IJSh%Rgw+7)>^$WILaS_BLMT}m{r->l{aCU9ejotrin-jJ>Y)Vp=*PmZi?A7J~+ z**=a0Jy3DcbJ@)VefT3HwFOfG!kzH;@{m7vj`V4`KnQQR?%UI;Ox(-9K;}V$Yc`O! zoH^@}%$}EFmB;XL7Mr5y(c{pRT(R1HgA!3{S8zPg6g5A)fjSXlIKKF7#sZ?qfIsV5>V^hKd@H~9#PRiCQEu0~5KOE{(cDgjBr2_G z(=gB2$_GNil_c-rF!T7L3y2MP(nj;Sw)HqPn2j?U70V>i{tb_M?Nd2ASYR3j0r}Cw zb~LAgGlCXRViXIJt6;o%8)-23g&7U+-~ka|L|s7rbcDZt{h9`r$1h2c;s-^4O>rmZ zRABOmRlAuiWdQXa_2eDi@;-d#i8jm-J|ZO@$r>5~);4NegE=rW8Z$K`p?Z)4NiVxS zf$Fi+^&WpMJ}3+r21G(Q=l!T6vUn(84K^CLI;&5^h zhzaF7<&iMX97n%tQyHUxgB!vWe1|Sww$Zp&t97`eTr)CC-72?NMgW9&q7B@NSw-f$ z;)bZeKV;l!1H=qZr}e}B{0cs^IfxqU8d-7w-;LXRN8^?Dc)Qx(^b^4injpV>aETvt zkmg+W)5uA#uhyrE@@rhVaIn5mV=SVBw|u&pdf=5SS3gGnO{Ns zse7QK`4}20lqs*tR^2Z|>J#n`|4?{VD!jWkQC7YsBz*n)$EdsosKNH2)e@0>A`{pz z(7{L2|Dy~vDG0+BGEg~%^;W4Flafgg%XnZo%9i15UqyRxfzc)#w8>$;o}BT zJ+XJ|b%sOsQDck8FN7lddFpajLCY4k&GG(#C|jAiV5l@UZEAiAkqJjB5Fpb9w+0($ zn)zsmOfuojCy;2>WV%er(MQ+-E6He5P^wu#j;!THIwYJ6bf}Das~Ag4VJ<3CMjB{P(h_EoRL9)B(%_7CRBIfV>;r;ua;Kl%N z%pf*#r6gEJ^#uQ2JsMPR8l!^XBSwnV@Z2JG;jV~+S@`i|$2uV(lzlDC6}RNj9m!fh zo7Xy!Gasz#{1>(>)RKN>Z{O~yTC2uB!V4=mBS8RF;AI>CI87`rj`TMU5TA~@)??T5U)I3)DXnZ`k# zT}{>=Bf6HV?c1cE8U{TKz7-vxlN<9S&!yKd`9Iedv3p^Krz%B~4b`cH0Gh^O1N?h` zVuYT+%yUaQJKY-uNldpfR~YBupiSE*NiIW-VlQ=ID&=L|$v z(|HKfJ&&8#Izs-_)t>$A5pqxWMu2M7YjwQBX{Si9-^|K{{oo=R`}zFm*?Ro7*%T_& zVq18qeqrU1&Oz%&n<15yttidY9639`v7spHJv-V}{pC+6=+>eJVOAz{!ceuiJX^lBtNX-UTg#)GSmroI9Ia-fd41oz zitonk-(PGEp8Y_*fp_55#9!F{z~0F=6ojkd`j!+*0C56M>T}IH zAV=!hKEE@YT(WctsE@ruQk#r(V+%_EuzF#iinguluAIGMW>`RQ?MpDZvz0!l+<3^f7rbKgSOW{(xFZys1Is@;F=C7 z*B-!388>Wl5?2oKat9y0TWl6x}7l$mN*Z8;D{v7BOg8nO?Pm_SAJ|HO-x z)h8Gm+Y(r1CQ6(`2olTiqQXx2FWhNFKgvNq0d4tzNg1EEy#C;S3K<`)KxPl>f zUZ8CNBCjg(v!rf+6j2-mf*wmdv#9XdM_~9esFDryP<17Da1`w5Y-K^&%RCRQ{zz3Y z0+#-e?b1sRW(|dfK{BF4CPic_8>R>sQ{I(`Muf$oM%E`ULYj(5Q)&k4Ysnt$C=kUi3ZE9! zE9m_}2NtG0j5HZFbSWmXbGIj?qS`0R6~G+&YCl;>{fKcuP^{(C-A=DRws__{bN!?Z z(=VJey-nF2P*(qgTa%zONi>}iomEv$v=>bbd^~KH-h_!SQd6%cE+YDsHEZ3P;rj({ z8EdE5%ZNUBe-M8x!m<+ z4BC`&1XQ5nwp4anCB-urz(#^6ZR^wFT@{9G6;}HtnAI_wVR0UWc~q5oMuz@Ju+j_8 z)ofVPZ*QL{q3=XkrM+l@>*=bQuh2^Os5UBWS)@Of#3p3zTgB z;s>+>loYb`^2UwzZfZNf0i|2Yt;s8nBzlcM&@YfC{PM!5 z^(1tzB&F{B{h^0}Gd>BVEggV9js*RFGjiAHgavs2uXYb0&buWqV>St z+{|m|S|;poQHu_O&Zp9#5!!|X{gc4gX;i*w{uvF@}8YCTH+;ihr0kD_yKDX0F^~6ET$yiyH~4TOsrdto9~u7 zzrH`W0s~}Z=N)0_juQu^7q%_)UNP>>d1fm*XC~C^sD)rDe>X zVfvO17E^jpU;mO7QbUK{Fj~v}k8jQ&*f6i2;gnTUG1A7!mak4D&d!@VHr5Ti^hnC`I5P0lVFqObH}<7L7;Aq2EY$VOh`>9Aay{e7oCxz25=wbeG$Bmv*zM3 z_zhs)(j2RIpQu=5Zp1!Qn*--hYjj`(1 zE`~b`Y|c%bw58RkMZpti)C}q8d3CaHi21Q|W;RBBg04={@9ecSw7mRta@md#A&0WQ zUDx(oUifW&X6c=k_kaJ0B;U{a@N*y-nR;_CoC1}*#Ypek_3*kF7$%(f;;4Y3e9dT( zC;N4guWRfDb}suaw>55mAJoUh`mc0by81lX|HfB?pR^ovw}rA-gt(6}PT7|utK#?V z^XJ~R=a{s*>*a1bebkL0>;4)VaV;IRjK=w|^Y`~(K=r&oq8w@D-uEZ;m6f~AB+HLw z1ss(}zm=gko=G1Bq7U~MMAsMJCakH7%5%_D=G^SkW&)W7>sRBPid8%>t&;!_vmT6sgM3zbL{EhBr|58lq;RUHIwpp3O;awY+&fJK9}-n|kwJ z(i4rcH!eSZj?XP=H)z>bI=%hjyN1|sRuyiIj(VS@{DZ4~a}aDZ9^tw)qVNZpx@gXt zrV9=N)F7}9#{^r}E{3`gQaMT`<9{hkV=Iz|2|z1J2&%aiDPfwswM-Mckk{vdxs}1UINq>I3G1f+krEhY5QxV z|Iq^6ojQtXE)Wy(-2rN4&S>(H`+Lrt&Y|y>NjJ2NLQ+wkp)}>~%9(*Ql|^h+OJlGJ zrG-(Njq7Ysc%wLZ2|(qY5Z40KwyMxx{~;s+Of$kMj5`MeQR4&RZs;E#fskH z-7>yelX^{(&pDi23qQ*TQvZh2r+Ib_f3$G+{(kNWQ1@3$Rpqb;V%DCZwsse&!NDE| zh!u7bi7S3QyIDZOqkEHyK~T$>+qbJ-2k~8+lW#xMKAVmWryZS^4FK5MINtw-gb&sK z5sqB^gui&-vgWW>3BIXIH*eV@%f?;U60!5#azIJyo0H}VKHlE9A)m0iG>&SM2XJCc zfj1vL&4@r2aHg2YDYOHdd75e{|>iIcC>{l~!B(K}WH&Fnt_Z3p*?AkSO>& zn6BdS5r7N|wr9sruy@J-M1vKdSEN6m-v}D4x+1WjJUksw zJaJIoPSMZtFqyb=cf_eiXPY|vB4lX@;fH1Sn~>MbL^eG-vtzyB3xwqzUh&JD6C}$j zMcK^3HKxf(TA{7l({k=FtLOC6jFNazflv;y%yo$DI8rc8AtoM37J7qAiTFx9(=_2q|i1xfWO4@lQWEoRZHHH>pJ*W-sQSRyU8opSXv^><3CHBFU)oAB&Jf z&Tr5-zhSdAP=_cB=%EbNbq6{bGN65}?u`R4y2w%FI0r!3iAm8Z;IX zkxrvY)26p6xCFo#V4rLQv{RheX?ND!x3Ucl{N`v#->2loKYRHMqkgBQrOCi}^MaOE z`{dJv&@pRgO8lUy8vI~rXq5cwO_u0d7~Lg7QCXo6R(EPYI*{+7vU z7PBb?`zE!sGgbEbY-&^Ez`cDsHKtB|X6ElbWC{8lP{GI%&$&?bR`mkzbsnePZJ*WS zBhxf~f1Hu(Qm>1#A%B;Z?$M%P5~xyq?}r{Ylq|LO30NVOSf`|HtpLxw!JF@L{#PLiUEF{ znH77$sZwy4I}9FtfV*7jE7z!967QoML^N+rHYE~{t6FQpaZ;}Ge5D_YAH3r|`B*s7n|K zWDw#^%CkU@b6mhn2^9juvxbwGNg@@?8=$wxFo#*A-IpGTb={cpi#?Xrivu?NSbzu5 zCR%&kIQrl?a>MD!StU;jDW{8CUP=~#jIdz#fJrh|f>dkPEf=zI*oAHeJ=7at0e9w# z37&={cQgnI`GHEU@%nuoi z=`di$&ds&CCCdp!jexO*)&<#)Y%yvj02xs*tW$}=y5R}n8qmm!K)Ht?q|gMO)sEED;R)1&JtmpfiUmqLG*b9h5?o07-7a1QoR(?o=TKCH` z5TeqqUBeL&R^%_8`+|vZ26?A>GwgxU1zv^@5|hWAPd`wQ)YJ^=zT63OGACw+sI?Wc zdItfUpwc4lg@Ofa?@TEf|7;>rMHJSw<+=_hPaWW>c+PpZZ(mC=SQ+M)+4#8>2tBN> z_ntK#5~BAu4-$+L7drFU45Ik|f_-$BO_SNLhdigEYXp4}9mJJN~4RMN(j z!Uv_uj8I?pbEq%I*KXw>FG5wjR%hjD>khS7zdv^ki}TwtF-OgvVa`N6ZtHhP2nHOQ zf{e->FWrA{wH6ysSM}iu_u8zM<~ngbUD~;5MXhTN7lrrbBw$!L-Bh-@EZKqnyxj3Ti%?An)4df@h@H zZ9@YCqdfruf8ClbyAQV9lFfxEN%?4)%PqS107DYs%!g&tsm|`*Y~L&u`Ump+E&yx* zSM&2$t*Am6JUe9-)(2H#|BMmub8`*EK_BNw$YQV~+eM2GI<}z;makS)a$mc&gg~s; zfIdOIc;wmulJD*2vGyi!@HK%*RELxek{I`eAMaBIs@_Yn+ZQ{${7YkzPnnq9tX z0OgXk%dOlY!Dnyf9;}ggt+sdZo96i|x7=yn@?*w~uw@#YLX;?XQ-bzc3El^91^%tO z&AR^Nw8CVU3z+~4t(X~ESImr!=d4eni=eu>J77585&)+y5BGu$Byp?ZdM=x5ecmmS zRpjvrT*zq8ChsXOc0DPMxdcYiZRHH!BN|-qzyF??D{Q7#=&>bg2u=Y4Jzx1Gc(zGE z`49`>Dyf+iCiV&xx5vJ3$r=5<6v&ylKRIlf9XRpgLw*Mi)JreuZ)MeU;@58>OoW7N z3)TAMe7)=n9=UCiRPK5}@`~riAxN0Z+!+gS8WYW#E58T8P9b%YR@=>i9`AQALZ^r{ z5)Xi~O^;aFlm9O(J()o`j4@aESEZinU5Yurk8m3T1-g$n($03hNAQ?(Hympc;-S|} z%+|oJzoY#45AxD_Og&W&s&eprk@k`J-zxR#F!{OrSJAD~wb>%ZsHdzd=YSt`fKabV ziZ4>2sYq6REc6UBM;tmvv+1}f1#|0H%hbDh@$aPwTA{Fm>Xp55euL2%iixC_C09uA zfP)fbSe^v-PQKUfJ>2p4!5Yzl}%5o2gPlyw?{5JOrTKnKY}73M2WXumx_m?VO$ zyKF0fqdX9P$Sj$Uj!D}tsWpCU0Ezvyt0_vsxz^;YiV zn!fexcbNs3z2(-#59JQmjFGs<&7fAlaHFI(pKB*fb{ri)Rev?NeGAH*lW>YAtdb|n zo__eu2*_3V4xE&_JFR7kBrYaqGh!~m=5k8tS_CyBOvg|ea1NzcwDo%p1cEDo*M%8# z`6H>$5ZWVeDtgp%=nSP8ZJ+lPTQ7p{9_dQuzdvn+pdCeZLsmRO=B=uP ze8k$H9#6q?3bw~2p}mv70}P#thDN(I+IcDx=vHW*2ROF^RrJpu{IHqxCHn<$W?|_^ zlvOF+`2OCZ0huWaY;8wTQMnY|=~~}^)^poeHMP{$i#|((2mmcVMAnHn9`-#tMdMX- z9+h;sC$x`Qk?&+$X;Yh$_O$``!N_`JybrN9x2jAXIJuU0{k6ljdhJ_R97){>yD+Kp zxE0;oy(s0B)2xjfyaz0u1*k?4u@laKHAZ;n`aJh98JUAI7_FyRYm>n2X_uHaYn@*K zK0;meH;sKASBkov3YU?oxNQK+Aj*-Y5s0?Rzg^h)hyhYmhN9FV84{Jya3%u0Fi{W$ zv;rI%vY`mc`sD|5mM=mq?T@Hx{83~C;AVS)u=F`1V$b^)Ga(cHH=LM%0BkIvMQRlVFdF8m^-_wUm8tK*tJ!tOTu*X%*1J z6@}?9t}iC!@vu&!9Yz|JA2E-nwA0X`uU7w6j9St|bdGpl8yvTp5x$}uVt>)jP(`=+ zzD5S*i)F;&fGU%0+aMYb z4OxPR0bM6vCr3fL6WLx!)kQso>O!)Y2#m=-(jLra9|xLg>NN2apmdhSkMM;+o(!lo ztsF!GfsKDKBVkvoQ6S+oj|^Gw1pAjbNV2C)91(ap`96czJPSCxXzVuQh2bC~PeZ>B z1TR1UEnu5#6<9bkepo-}2QlQVRoEWk5$Nj0fq_9A!42rzY>de+rWxkp5i9XGidj30UxC&(|Nw zgue7+#j#CIv%q>8QX-<89OxJP{Jo4#a=_y``QisH{tGD6`B9Vyr0%g>Y2zA z$&X+uns z3;6`-RdhoMY5%7jgjO&EE+n}HxMr|LMcj)cxz1Sp0=YMm&3e+}A+%9N(l@@wz?Z9IpFVqd zePU_B1T_l9qtuf>j!ls3RXBt;;TZ9ZM0+nKDBVd$iplSoHpyk-y->!yD+astA#fHLFb>`|#5K6Lg=v73G2qj@hAdq2{`#N`1SGO~J zG%703f8)(n<2kfm?oWVe5d_2mCDTYwWLv~!zs`*VAu0MUO6`G)SKMp?sS|irDR%UU ziS4HB)xM)~&v@)u&5|3@p*6jYo0{W~}DH0c$j zdFYa!QwQoXH3KQP^h>rOB$^TRQ+?|~QlveBKy+|)0W8)UOL<$|^Aa%vtyV$WmF^VR z>ext;5M*N#MTLmjcbwBeO#$DLGsxJ;2rryLnSDTsWTps|3VPR&2Lid7c+F(ovwR25 z!of%q_X`jXr5GB$h97y1uaZ09`A4U%UMmE zkIN{%Kwv&_UB#=&c-**KET4tq-OGp1)cc}$dn!+bma}>bub%;QSg3*I?9lWguKLcq z!;!{6*jRc!WiNH9QSh`x6*GaQo#cDepUtOfJ9f^`KEb?f_1qaZflz*ZzTJ4$^@Yk_ zN>A>Me6>TR=!WjawTrrDKCZptBf>oG4To}KL;qjH{zp1yt>UVm2Hl!#=<;Y((%Pp#Gm}b9;bSdV9 zzXm<(3Q-1NmVU|M&Iy}Ei`FI-zBohM9TJ#KQIQ$)%wimy$n@zVl5a#|Zz*?+PcO(a zgP{is!{+;7s1xw9yn{aAlzFfUdAslZwG?7Bk=>kLB3w%>{gtZF8NpNZ-0j#Djt%xAWr!3D@x!Xw45iDv>^1^>HzlJl4EDxc! zoc?A>^A`QCUb)iKV*YrVtchlcr7Nr7M4J)c=H-rKe_x=OP23U$P~8@mmaVvlg$3bR zZk1TfvNZ94rw}!o_o3sbXv_0Ic4FRXL$M=pr)yB2y*nxvT74(PC^SlLW zKUcPw(CAa#Xbc+kqUA808oFS)2>|A|Fv8{VWUk>(>X1o{F)@dEY=~uq-n62b?uM8` zG!T3(@$Zfd&nLC~{F`Qd8b20gUFm6aWv`#9BzV#1Q(|h;Oj~TXzEc@So7Gd^nYccm zqD9oT@;>kiK{&!5)5l78d702G9l5%)z#ic9;@|ZCRW<3w0f#Wt9Tb-x5BxcC!AKJ%)r)N~5|Bl1oFmCd7rSV=R zipVZ#O=Rf{>K9qs4Kf%^EgO{=u(8}lRxHZSTQET3FlD@(BH9*K1G(tFf;uFM#uW4# zfdA<2z-xC+0xboc{-$jr|T8{y*dgfO&Cx-gyaKlSfk3k{7)O)XJ7WGErQYsfJ!_2tDc9;etz z$Le7|mbV_CKt>hlh>1ZeAenPTop}^2u+}i8reSTGxi(eqHXWFRA~ct3z>a8$c;G;I zXZfDq6KRgMj#m*U`1id5y#Fu6$&E6pr`4Ao#^Sdz`URO7p{mfqP(-*p8iJjOS5P-f zxRH{ds4ROEnM5QLk1xHV)NOU8t z#jwdfrW$tB_sVj|=_jxg@V#DfFyc##RNrf~9YOEXW;|b~q@?ufVdZO+*Qc(o5iXtN zvE_@205X~=t0Sna`!1QoADWF|R)m|_nW;klB{S1hH~fK3A7aZ`H~3nLpmCMJ(E;Y~ z8?feGx13^5`oW8>y+XqV?OS_tOO)(encdrm7h}CHx%ImnE|z>;OkRd7*+T*S^DFkU z$R9|0Clnz-B(*5V^#B?SpdhA)*tj{lu8|k-zMmIkgO*xuefC@Am0zqn;x zDfmJeM3?h|;%5}0-36~*$zs%Y{heL-(T#&>)gK~c+)(j*6$6OWpl7N=cJM^RH(qNO zGPuIEaHlbrCk6j`iw#!JPcB#9@x=9~^j^6b59x_{yt%GViobkd3^e%+D!`1y0z7@4 z--9ESUt0B#bW=h{HRcX#SNf9=St)xS9#I{NfC7URB;STBhqt9;<+zldokv(1mwEH} zQQ7KE8A7k*#8?TGv}08fs?`<=L+*K`mD0e_kcbx^4a^JN$uL3JEb=s~Ji^LhL+$E* z+^bt~uoC{5vYi*Z{o3`<_#+A2dhz1*|8}wra<{RZICpITej+4A1qG9=9HLB^2u&of zHS^|hyh|9@NN<+612X6#O{EcCNJd5mk-sgX*s4ox)={{QRi0H!e*2t)O2V|em zzZKV>GZYE2Y>{bS@m-0MGn!AI9y=p0iLyscje$`6+(a+2hvhxd-BnePW8F>ws;4_q zsmka!Jo+*Wz!?7!bvCFl@N?DGDILny2j0ZY`4*B)ULKQap0C!l<6(q`YBebb67OGo z{kgMeP1d)p`YTocj6YI%Z_}&L2@#WD$^avLSnL4^awFX%xDs@M4iJBv%nd(15tj4` zXvg~udy2n+9QvEg7r4NW?W&G<;(B$YgjL96mJJSZdiL%p!Hhgcew{uvwAirLUAk=ySAJ-aPS})WgS+a+c-Gfhl`;N*6eS9o4Fy#5K^hRAQ+6X!;uYD z%p?#jYZt+b9)a6CS51g%1nOp(GgY12WwRvrI=rNbmogmZ5(HV$p6Vc zCi6(^!r%fg3dthAQHVAcMur4-=IDaD^aT!yunJChL<>IuoytGKn|~j#vnqT@q5pv? zYA(0Z4%WCDH-0(OI!~7c`L8Ut<%9zzC$I0ZY6nd^50?5q_n^a^In`Y8sf91F;rvB@ z&)(U3@if?+M$^m|{sI3Xg>scTGl-irxyo)iuKW%RmeaJzgKZ+GU|)6yf&osT~cy7(J)yEmhL zyhvj3CuN6%q(YMWx8D5#`ACHgF<-wEO7s{A~{Sb}Bad zF?EJLXq*({=R-k+K4mC+GFvs-BUQA=$XvC4XMdq~E%?cTjRxY0&VuW)PA(!#A0I-WqqD-u1$ZoQ&9 zLXLq&F}M6Y)|ca_w^}5MGCoU%F1E8^Q#ya%nFewDxH)h$&X%02rmCv$etxQm5wiB% z2k86>6V3N**)l5d2`|?(Fk3{33{}f6Dnj3C6qIjiVP?*pSsH3y`4p@E5v(Y*sB`V| z6YiDFlROLY>1wt1T1>dh6T?D&kP^DS2eL^4k!1+Vh02%H_&6P+&@=vnoyD>bWU>l5 zP%rH%uXEb}7SBqp_zbw~f}0;Wu<+L^%9ZL*Xh>$ab&EW{q&ngJGZPD6UaiUu!Cuz( zAxHM#Uxkrtm1BB-l%|_Um+aiOdAG9%uPyOXdO;Z=C6aTi=4rfS!7N3`5Rwc_@E7I_ z69V)^Mp1zIIVDE^?&0Mf*dWAv6x@sAf2sC9*pmQCQG-OcGXt0#*t8x~JB z!XFC}@qb(POi)N+i$!oW9~%^?-)b^W!wYS>aSBCzaZLSQ7zy25_NmxGM4a3gJ34A6 z8c)aO?WVQk3>S@uylsKOl+FH` zKRyroA*s`Dy*-X?2s;?O=0@D-p`Iixt@`!dP%E^G7KnqyX^8<6e_3}!PtK^v*fJp) z+%TYO#tE|rJqEA(Ka{-*SkC*}_J3zyrp&W4OCd5-GKNA)gp?tn%n=PJi?EP{WJ(!A zlFXG-hKiLT8iXQ6MNukAH2gnT*!x}2e)fC3f5)+({j9b2s{8)^zTay&uk$=FEnMWJ zk_24-J%BqzsgJR6()x~R;ZO1(C9G&zEqhX=!>rejw1a8IWVHfMUXpBZu&nu@GH9|S zfPBKdlIdF?ni52pZrI+@;)Erf0bT$7=iRY>b~8${?s0(p+9o(|eZaY(LMJWn+g)>2b8JP(B3v0HJ$%9a9K-u_y(Uf9LO&I+g zGTx9kOGVNjJN`5Wb}3_1_Fg36FjepKuRMlq>(X}fSRYOmxEH6wC#7y<)~7US*X|*c zkh3$dbOW>KUu%1MOq>!8h|oeXg}qNK)0pkj>J4qIs@S^of&6Ag;89DfDE3iYL9wgh zwDwLIo{*3rEL}_v-?4Z?_Qw)Q@fq3<C_10R$^7`RxND}T-)0If zNrJy=iQ26+qBjm57>+FffRcG>ix$)8yM%<_(=?vawOco>1@{{fIiq;;Jw0Qnsy;2==fl4XAmiaB zowk_0q_Y-Q!T#LAvGK(N^H;rg`r0*c>M?65cHC31T*8a`4nY|9?Aa4mQQni<*vrDv z|G5<^uUi7HfGPvu4VsL(WLtusX!?m=;%=Bz{m z{hN=Q8fuS(b}EZMTK8XUbZE{=aHdRPG+d3_%IfCNaE83pM=W6hHTd;R$N@ZS(VK-l zU8Rj-3csaw?S_Jx1M=0pE?awGpbfTtx4mH9Hr~H1D572liEfXH!tF6%=m1Zx|g33cjwGU~w3XzcFSfBA~wMG^nb76Ck;ha{X7{Jv%8r z{;Qmrz$8+33Bz2i+gCeEel}2|0>~t~*4#HSYJYO$YSSgO!>fZ&lke#5Cl*B{@EI^G zh<$SUq%8)bc`MTgWXjJ5|M`{-L~!a;Yxl!Pj~vonTdJql*)2mLz&5G5nJyi=(T;@Y zlG3mo*k=`gfBuTz>go@3p-6#~>enwHI&9cf*z^=p`7a&Bd9_ck{OX3pD7j<;?9GX+ zFc0`Y!qD$L!~ng1t>F~FxAvm(BCU7Id*XOGKMh8n%=cmTKU1q-^+BzNi7trmfZH%A!PsT*F}QP~Gz zrHJ`ZU&!L(wn5Td=J6nsZE-SiT;#Li0I$*XATIgTV?&%PwCPKFk(6rRf@4hEB%bXg zgBPKz&-5Kpf6(iRI0|Kmm>2mm1JcktHMghh*+U3@byUacS)!l7mw8zu@;|Mu>=pZH z(`&Qp5$sn`>-$AsI#Yt3Wh<|r2ra#921$qu>yUM55mdRTloYehmGr2;{_KBav1R3h zeu3H(+dsQmddb`SmChf`%M0=o*44Mt_(J{HfPt^qPFN}qYm0!pieG9+40sX05`zA^5( z7V8Vf(wK=yIr6sqhq+v0>M6=!ArcZ~3=QMf4**(55OJFkt<+|KNi}nNe)!((^r_^p zw8Ropun6ePljD3;@3ZpFEo1X-87!(Or0#(Vf1g>>E}u`jU!PmX{7_Z`F{4llUF8#e zIXIXgFns;j7mI|u!YJ-&Fv@72n9SXAILG)Z79W(d#$A@sWTm3s=b)anOF3o{NNha*^DMf;$z=Bbe`QyYVFe8VcuUanPx2GMi-NIMC(AbF4XBS{;A z15;gDTKB)Jk!UNgV3{no45ew2EsO`eL9>*+YVr$&BOGXP^?rhv?m^`U)}jN=RtRbHX+37EPh-=nxZUa^C&bl@yA`aBAnr{T)b~ z5QPqNC%q;yKter5)2dG|2Owgc7oMz%$0KD!U+5Kbh)xIindUzV5I7|>-dCKs_Nf5YpzH%fL`1w85_{pbbWb2(BW6qFOB} z#YDY9#h)fxY$$0W;6md=FQ8;kp(3LSz-Ae^!3wGu^0a)=ib^a$SV7pz1rE?5!R9z2 zMsRt=#2?c~UF!21BPfoE9RO4<&72_~v07q+N0FiT(3&GC6JxdS>+wFYovpUKtU~;@ zEVqZ8THPN-C62bTHA5jRC+ev#=pBY{`2H>>j!4^TfK4+Zt;W?4W0J=VgE}@Rfnb$nQ@?R!$!jP01O2daQt-C*p}qi2##9LgTMt}1gX7LKk)^@t zq{l(61@2NWwo~(;pwGh)cSo?gENVC$9%=agroneBG0d{d-dIKInErneEL|+z+iYDK zFJvxuXoPKXdw-)h+EW@os(;Jm44Dlf+ySB7adTVYbRt_$GmYS7#BE07J0O~VMwuZ? z1bzaAg(DJPz3HU93Ao9dU@05mfSv7mC9>9*B2UZC+Scp85K_G~SK9o~%sgt`l_QK- z<>?Bil0MKn!htU89Zc)_qzUHir=B=Ur1qM#@o$)dSXZ)B_DbTlt*)+w!_7orK)~vA zQNNIgg;DLuPQ-HvULJ3QHX{yA@M~o1F)R~I)q;(3pi!6L$OT# z4FuEqe-UvvgzG)uy*lO1K5j5t(=l9*dY{^{D(IhORAxQR%%lqRH3H1i#OB>+LhrEW zRlE1SR+OYcPlh(OBlX^+d9Tgx-jsrFT6P*0Pe8!Pno}5}P_A3&XvIF9+b4D3kx!^f zVi>@@K=*pQ2!hI=7`79uW%J$7ahpCR90CO7VU-sWXCGLGWivI>Z3b)(?>xpKs+Cp2 z6tg+GM!%9`R8$q#ce-o$?k&HET3G)ZFeCS`2hkXBV4#t7nPre~V5J-8&pyVIZoJt&T=Ec?aT~kX1VG8{myHZB{u|aR_*aF&H z_H!ELeJAyeoie2{VZZ0PW8+%iy78{!#q;MES#RZ=VYwemUFN2ej65^B=8rf`=zTB5 z3jIK3^8hSUsDEUJf8iai2XVWZKvz@SmCQ%7DYBr{KBB@b1&=qNUd;d9J&Q z*Jp{(05z+EamIZ3czCsN=o&Jo0mGPnY-aVvXuar7XeCW&8afP!j)_s9mqT5K0|^D= zh=SFdNyD7=F(iYb-S?wGy9T}=5C7%@4Bq3+E)HmG{jkcm&G*r*Q@r|5po^_qpQJdivV^({ zc32J8T6FEYjgf?g-RW?>QKkrQ>F;EA3j70x(7|nu*bH?%R_%|*^$MyD1GW=NE*-j1 zdf_bzgWVdyCS3ndIe+`jTHD)jU8bdDaAZm_ge@UX#)Lu zH#86`THyh*>Z1w5XA~J2y!Mg-8T4_2yC&P!<~WcmS5t5$(Nq{}WY@L$e!J$v!4j6nf%Ceyx^zMT28{bxf13a}*Em z+8?-cBgam57-dA$s*jyD>+s|vzEAb&1|9HTGtbhtdX;bvGLNgP>#St+*H0r(|_Dwn=_e*wWp(@ZgP% zjC>1vrkCf&XSVAdclTJzjR@r%XW@Et(kv;VMDGIbE|g|Edh4Wf%@xG_gd4jdpD3c0 z-WP&#+kSlB7b1cHr;xUcEOxrPXJZxm@8l@Kd2kg22J>vTJ>p^O|<3)u~pB~f03uFT(hWsojcC&XfZ-c5e5NxFti{h$SOHRj9n07;Ch7?P{|(uE-W)PH|bP?wrMfK4Y(Q#fgUK6M3aFxfWBL^Y=AQo zN-5mvLjbi#+BTxS5Lz)qPqiz@_`c$1Oj6}v1F1iT%AF%?cP=S0aX%IRDk!B^-k89I zv#bN$W7?Td+P%^72u23nf5O=R8!|H?fFNt^4iHIVZw?d(ksFgd(~WshMj^{(oOB;f z@zh8yqSc8CG29b-3S?&?VPRY=<_Xog>uq=UC9jf#Nh3{{3%14_HeGzk zgcbAWkK#0_hBg#S(<)z6?J%)|BM@;Vnl*1O%d3z=!7D7C`l8}Wvu8KiE9I*E*(=+6 zi=FC%SAu)h!nT@!S>+6=Rh(|Uv!>N918hI5UHjBpT(Yo9Q+3*4RHkiAX-b|KIyeh1 z)YTj#A*73+pxrUpb@9{(D25)BbM{|cGSfuebFxpI`cS=`I44FgX9!Gff-nXEBxs-c z1D<_6I&_(g$xf|=Tek@*rWJu3hj;AVdvfPD{te@>Vzf3lMGnC#!tOdcH&Y$((2f1H zH~+d5lAZ@(y>+X6xEMn_f@* z)#`8a(zfl*zC)|-SCD%AXWM+**Vl!fQRZa8#e(1Rd^@peLtM9$ogf+G{`-OZ7djvp zCu;__;(GzfNKjn#XVra~ks%tIZCSCmp_Lxd4;ZQK14Jus#2~bU!bVG$ zlN4TW-~RnsfrtQl zk2Jqn>NFvJ9mysz`CZF zn7i_3VVErBiN&6M3CWk#-IrR6F=8ndK$UsQ)&_T3r9ylwfSo35U1>1nx3hc>(3^sQ z`N~{A9yKWoWdnEVooC4iM87(>WavliP^?_Fsz*sJg26zVky$HNreHA3^L&$;8BOQo z8F!S&ej09tBJ)}O=##T5S^t(A?$8w4Gv8~!o!2!(<|6Camtl1wcO%p?*&25DM&0^z>QK-cN8yldGX=I%pg`YQa;~2j!^tWb*QL9$1<}O@Vt65q<%sNNS znWM^FTp{eY`gIpUi}0O&q~?cn8WKK~o^YKx@On(+T{|Yp&NA%aa5CaB#g+kS{#?6u zw)V@lQ>J)Kb{o%^JBX$ZN?I3PT?QYjfkkONK^+~P%qu4b3Je0_7YF!lv>ILx^&yj@ zs;P%^;_S)|&D);@3O*4O6i4ZJaLDS@TI1~@yC4^9Q_=`-c`S%4QpmeZgNQB>iC+OV zLa*Vr(~z40f@^By>|)$8>j#Xsr$j9RWbCKJa4K|+`}?2vuU$HY&2XJrRL}eL{kCzr zICNnSNkfAR)B%2{O<>IqsG0%P0q0Nmy?FV1tFu+ZY$+NpeFW@vx#!$4cT{r8@v~>g zTR!==x8oc1P@9>ThnUz`;4*1R;ToG}g7yc++96+GU#&PVQabqFUw|Em?2~2%<9W^! zkr))e9EPrAQdxQEmwx}sTtYtYH2vK>lNWy5v&YPv*AXx;n=CDx1mDsyJ+!_H zuo2+#3rj#e$ue3o9;OCg8=CvXN>`k3tA78!uF0N9kJfMIUd>-cr(mN@I%unJL!u#U zFk3y{ckhWt8EJkCd`qT;RUSCQe^hI!suF5@xt@$%yt$%$mhEnCGJxQQ95?oV51@!P^)}NsOsT=Q**Z$;sewfF zGE91LgW|6a4c{=77%+Xf^N3ZSp#~HPsRw|@(=3;>1QP51to+cxWo>xbSrC#l`DIpM zfBN<_boA|J4yZ1hVHp57rGTVLfi5h@`qaAz!VESpYyv_lhz|;(vnB%lrvvPOs-hCQL_|Q+z;J!3#4T(Ln7ZEfp)1vJyx=!(Z zf?`p7#}TWul>rsA&#U-$*VT=@W4QoaL`1mt>fL44I+IdF;kSt?;u-Ct^~8|CPn#k- z5#Vi};ASqRQM($9_9U-x9%;Sst;EI#zOm?CN!E7)^4r#D5DjnF;>2=Q1CJmj49USRrK& z6bpH_jByg{JbTSXU??~7{6nhg0k5y%5ort5mDwr)T~#4Oi5?}AF{5;Gr)uO8JCtCqiSKa+oabQGPP z>|UTGE+8EPtc+k%W6gfL!h44sWqf#=kicAitmT6KQ!YuD#f_111BH~B<^fn{kgOA} z6Qi@I0mT@#>jn+~s+M{Cp}ibNoT2S^~&rQBib!a!a;~3fEQxX zSsm4adFcy&r}1(d6>a-HRHRs&Z)3**=_~`<13{`P{Nd^Jz@4pM9OJf&^3A50mx0l z@fHmdASb2RGz$?DrlqOUS`#QYWz=5BUL|{$@OoZ|=Ezm;f4pI5aAwMjss>bwih`x` zI_XtSRBk;>NH|H%L@Z~}z5UN{a~z3)@JFz#+s?XP*OG+IfBq@^Y?#%4Op8>7$HtNl zHyB^I0sz@4MJpvx;5AiMHDtg}Xw%izMRGI|6yhvXBfciU`fBD&na7eCZc5ljb=M{( zkjP5mp_cT~w_B}EE+9rB@V1p@N5Rq;CsLD-GP8L7 z`gIxA3?HG#51QYT#jTV?x&?9r987wYksHXUtmt75A$X9eT=5$mX|2{5bs@kwY>I5& zYG>A^Q6}_3Q4J*E`_MF=MkbniqZN20pYaXph62BlipuU|$5aqL&T_xWh*SzqynuxD zA?BPO9=AE5vUiLV<#*kB{`@}~8=$HdfgCN9Ri#2k;>6ngnPQUYlT(yV{g)nEYSL;? zvjyRcVaj2jlIb?+wb))VSZLg|sn97gBYgTTC12hUu|ZrLlP#Rw1UF#Sh|ohNOMxeN zjyxt1I;wP{sTKW%47mpn8@41|6QMok4o10iD~#G*rnj^6FOLILp)t@Fml!*<){ z=JrCiEv&IScwKEB;JaI>6sVjZfY`n z_l_MIuws*jhCJnMsHv+P56xaof!dZnKpZd-6Y`@k@nUG)4H_IYHYVWPZtgdHxQeH~ zU*8k94nMvOK0EHjMg~s%twk4X)Xs7GROjnq_5~UETkhPkW5H*&pA8#3!kYA&anVJT z(S&|l7I&~m;!~3jEvlM2t5~pU+C9e6+;V&F8w9uB{C=?-)h<~rhZYR`ZAXRY{&vZ-A=2E1LjUOi8%By*cnDAGmTj<(u2`z#H#Tj|dtD zG{IOEjc`E!TSmDEh@i?pJ;?lZYF35kx?R5CUR(iohTPSaj!WQ1xu|&_4`?UfV+~0!hEfzzIGS@M&j5C^g_SH!Su{3<*VT(_b@FK5-ksH(U57OhyG;w>D|h zW>%JCB9(3&82q)zQv;^=qU(8(TKU5#myPUfkqt~-FK*$_mZip}q_jm@@S&T=RdqGB zxmY!lW%?nGW|GpyvQt%_t8eeS;1;CSZ?GT4l!#_$Ai-O|M3oim$0+zFg}y(Cs1}Wd zNS3I@7Qh--Uhi?`@~eXxDZlAV%Ng^~~c{IePjl0QRD*K6p066it+ z2e1li6D387&7f7(CeYEbS8cw0oDS9V^_U7#scAErB4%S44 z$2)vuo+|cnvC)aXz8x4Mh&uw9Bo7^q!FH4bO=73IPxkTf*u>7_$@ci?S{?Y@IT9ff)2BB@k)i1d~H}sb|6y zCTw@Uxp&j+ZJ#QfK;4ZucieBH_0Y*VBpJcM!&~{~MN~0nD#lGSY9eHQ%Ky@`=ml6>mf@p!l+(bapgF6Ad$wEdoN>JV`575)S1foL}~cf-Bp$agTMCYQY5JeAOl10a=Ie`_yd}VvyyISj`u9J@VPv&ue=RNb z;$3ZJyDqsuliJ(F-?%2M!OUy$Ghx2YNxzZreBu20)pufI^h!(0J1)F7PG_6y*6Ss* zZws+dz{BLkxUo4OK8Rf3J9rVAa>2gCpJ-`+vYh52L=RFeX}+u5A7etrB?K@{;vgec zaog}TC#8a;ebXK4yu;AI;3V{CQEu@sxji0BvDr!3Y($H6ij;-Wht}jia|7b=8nz?# zN;lAD%46TbHHJ}-F=YB{*RC;s#_2do!|^W72?z?Izh>GYzX^tmE8KsoT>2DC=vC{> zi<1RKU>4I4{s`3~=dK}TlIhU&7R{R%P#YFu)KOh(1{&(e9B8pOVeBixhIPa6+hk9m z(7qVnpFv+dH0e9orqtxjccpp-5@E}VD-};)Jne9?KW8@#aLuIJ9oZSiwVU>*2AJe| zOoPE{gj^;ISJ7#KOvtUY+FfVy|EvRv*86?Oli2^LU;iDTRlpuvTm4a?{mz_NHdz=9r)UV0(8`or3%Y%a%Rj zd8#Zv1V!aQ&J!m}MXQSk?k)T2_T31hUpj1}3ie@ha<%9MU}0arW9N*B0~AF)jXmm@ z;iS~9TUTPQuy;yc%6>fI5W#+9RRzN>)us0;#GP9Z*_i%tqqfaTert=4&+W+P-|*}f z`|p%w4?bov`r+?c%q}f0#h!Q*@Z9R}U#!LKpQa7&To$?-l9!U&W>--4en>yC8gxTX z2jbkO#q_`YpmE$;VoU*eIrG(Mu1@I)rS(4%!*?H= zbsE00Y_N>p@P*)Cc|tJgKKlSW^M=Dl7Rv+Hec5xQUtF@>_PFzpX{Q;no-tV@Vsv=P zF2+T@DODh)uPl65`-HjiZ!W+DItL#96pLJp{Np{oZ3O&`jcicAKCGQxP1Gaiz)7OF z!K+|Y>qoX=FZzooC(o)Z%~pjmu@yBUg^(CV${?F?kaN`=pvUcoK^w$1V!jIeJDa`B zvY)uUT|BOTpr%clHU$XQ7~lvIrZ-*C2{uKE&=qe0@)b5TmVhLJ;Cz@>=6g>(Q^YCf z)ald4CMHtd4*{jeV$D0|>?zRCg-l(*Z&c8^W)4n*=1Ac`mH#223@=mOYc&3A9hf8pi& zUMVmK zy&Wer?v zGt1A5^M$HGHkCQCbeteSf&h4$a}w&)2nmGwGCMM~bJEmEZ` z&H}~AA`K*xv@bt!Jb!L_pgwiwvG)TRyPPa2zq{sDA}XO)ht5+BxZ4e**hRfFE891O z{h|cLUITOx*HVz$qAL>>te>A>;VjM(9*03IU)}gP?(e+SCEE@rK}(o~%4fi7CLKLm zE(TXis=K7|sFv<+z9Vhxg5L#?!#4PMU_j8&Z)Q_U(!fX5(z$Z>D&XQ;EM#T0W|sG+ zcUQ(QJ7o6=o$uSs%&|aw-0UiU2yg*KbYyUnvSzb?ReDrhvDteVf>P*+J0u{gg7ZDs z%4(WT81#ZJkOP^e$Z#fbfZn)q2{<=;f33tUMX0;P0P7lClh4oGjNu7%^crf@t~kDC z+Kz7I*TmpBVUTZzYlIb5>4m}_`=c0Bt_}0MQ+|FuswZGZCIdz>u3o$LG;bre z#9gCZ5)i5A|EViLk!wM?V)&(Vz)rV5lNp?MW?84SywR_J|HPNyPO>knCArWgW0RLg zQM;ybUR3k1HZ;vULE28Pj0|vYYYmK43X~oH;B%59>LhK%hx)3sk2O7IR zIrm@Y4%oB&b?H*URBbJ;*+B||i#$_YXjAYEhhb9^YtV+uj}={CS}7cUK_PD8SH!sOnY%R-8w zt@8dFNnwQ;zYwhYiR{=qe;(-MIKva*3$<4V@`Fl+$NOKa5UJzx63TN~M`?EG8hyxS zS}U4d71{&YRSNY-;3}jViasn_r9ql7Y0@;k1u|kLj)(%80Im?0H)?JGuAt8crM3{X zhQIlKP5plXfjg$N#)$h&nG1V$R0nY)=0_<1bJ!yZu<6d z9hK%E4`6RiJgPQYWAasPd{B(7yt*9gOAjs<^}>0ePOv?aUUd?-FWP)eUuWs8){HV^ z05@b`l=X|tnF;3&n2HR6L4S%2B&<}9=(M#%GJ0tI40@98Ae@O=(eTQ$)o^J|Y+l1V z@g(vTVjJu(w~$^H%2+{B(LSXiQKDOTIwUoq*OwzSy#%Y+)U88r1sL2iP3g}ZAhdu5 zLUht(!ShsljGK>{%(~u#pK^MWQ{QBK^a?!bRBaPx1 zrcrz#B)ulT_RozPm$`L2`Sbyk8F35UG66P8C22uWK@LC+#k73VsHP@bPk!_{o`y(Y zs%l#P&=j@SSxEyvJ5_!Gi?dv?;7c?2U3`r*y`bgM>xm-(KwWi@@Eq{J{0nA=tSy8ADx3b`snYWq)H>RHl=fxn0Eyz9Um>Cs`Y6!i!7T(C3JhFk z<)wD4BPUr@K-|M8+;%kJtixJsMs07BsPuYUgYt7{UWGgC zvyGWK{y=qpC@4tX;_+Z9s{8}=^M*m{w!RtM{;1qe;Z#JN;kJ4XGZL?4vcc?1z87qet z8FHYhqp&53MO=Q&|4jnCZJ{wh(K#i&A<5Oq?nr#HaT%+@olQDy%}^YKTQ3}Ad@VX@ zX%!x-*J$*rDmf-?KrPj6(A5P@6J}@M%O&rkwd^aIn=#O?w47Crlo&pwr9X9&RmYKg0A zb+xV(0t`rdwBM4nXmKl*dKxlM9uPLyd7;lQq>83L*RDtLAQGB z1rQE9N1rfG1;qHFYr^3)uEJ3g#L;}-{&o5zRb^s z<8%D+117GrpAj93f8SSdAK`-vCj^OHK+$&@WCC-1|NedGqssmpcdH)(D-<3!bGOR| zjZegD|BGTrukhei%*i$1rx^x-L(%jY7TlMsW2nc+27^pzJiU~v$D`JPCUFtM;78vy z*TyE61Aq`U=+^-2cU4fQK$C68{*CMRF12ICX7ywp!K_XH#V&Ad{DXK@H!sHRmF2&h zdl`9x;@{;?;7HX;O>KPkLX)ajl<~skT+>y+QRGR43)4moaZi)~72**NwO_R;qxDa7 zm~y8Rk!stkcRZ;+Eq1BGe8*pZO*oW*XHzRCLYSVKmwbTMBu4f`QF>VWYRrf4(AX?ai_oF9Tc1_Ay!k(FO;k~@pVQv`t(#1q zFk#A#KialwBj!LoZqJ28>E^qVvAKqWp}d9yHCY?RN~wt)G6E~%W!kS=A)~LFXSg=p z1_he+!l{~B6AnV>P#|y3e4M~?;=C32h5iUAC?r&Fj}^4@Hn;B!#0NVFV$3FhDTX8o zb)MsiBr0RlmE>p`GO=oo{ArUyiyp}qPbIRPIQ07_YTE#S%N9%Sx793W5;RZqfI`8U zU1(Jp{opl*Ziq`JQ)w^~`UB5${-t!GKf$Y!O~GwaxSwq>T;bAO1m4tIqK_R1ck~xp zXj67cs!@Ipq+AHQo)nLNacsm)g`KXlzLEM)$eRTt*iG9e*15BT%K%j;8L7dRR* zM&hWuqQqpgw*|%TRxG*Ukv)BXhc?z+mPy${2>`otV08{G0IzA7;Q7Yxg596EN*cUG zhU6lRrdldxj0((iniJ`1e%QXgxJqKdE(@7NTfxI~m{+m&XCVB2zQ$z6UpsQqagV*- z_-}9qr$($u4Y~Aql1bH3Dg(#4m6qniL`H%zGH&kEg^!o3u6vZF@xt!&U4SpNi{~E` zvH;M)D3mx?A0wkaTTsp+8-*@a)b;P;{S4Nl!yd8fjV<6v!~bx3s32ftDHr$dknaM z=w8Ha9ss5@zR)Ng-RW;Zj%3b9At_u7Hak4UO0w&>Z<5NXl9|c~tS;-uFFzvmRtbET zv_aA-M(;!q&{up!V!^Sg%f+VQziEnqJzYuLWLOGAlRp}-`w(9f-6dUzXQNBkktPT; z5lG=K>IFHQluwL$F+iUOqO~+Nq_GPyVgigRAyF`q65a{3y8g>v?gu&VYy0lLt$Qg9 z=4*N5er6Tn(z8U|RX{ED2W~?(hE~Q(dX(;yefI*p**PjtQ*TaRWl4~rY}Zf@%5~hm zBtI;^_^0*I$0msk7R6!zV#Y7ERNcr)jI0VC#W6e-~{stUn^ zF23qVt6VYu>v(v$w(qx5Pj_Nec-I=$6T}0-m4W(kvpbs!zyLP&U2g z;9x){bSXeGk(l7z(TrU)D>(6Y2?8?widid_6&wly6Pa%N<&2*+$&a8gqM-JdAJuoF z7yd(tOU&;SobAgX9}<8u_2#g3rCh?7ULmGM3@0vLxsosg^D|}e^moL{g}}}uRt3L8 zKp~AKbonV+t5`q&zY?o}cMo#W7@klK!K1DwjBc} zPH~vcg?fQ=Ij3c;pK$@61}kCai&l);LS)5v4ITzy6)USAqv;s~h4g1m-j3n)_#uxo z+ph-h5B$Leo zi%dwo0p%%qd3lAe_~(gK>k22abG!2u!9)8WrF?mq;ybds80V8p; z#HzTM?^mUEi>C6T5|m-n^yeXyezqJwr#{UcqQAC`)H@Hi`XL);YinSFHTE z2tr^0{z(~EG`3I>Y6x4P)25bffrG?Z8uBP;%3G~md@3bjjwl!%Lpqvb5$06Vn>n`d zk}MA~Nt9j6)K;#LOX*T;%Iai=)y9yL|5XE`ta%T$XL@#(+S5~Oe#a36G=uhWe36+$ z){Z#TvP-_t^rvf8y#6gCb;%e}MDQc8&F2FHKI}PqbQjd0CTExZLq{rkQ06X330Nb4 zV#+?iUkguh;~ppP&G- za~Cg$DpHeD{|zjv`3NELA*7ku9NYWni>i8C@_#&09SD<7&3m(^`Hd*(aGv>bM~9r$ zPClV>_BAh_@FylanLjAMjbN1Ozi!LFHcI<}1O?ff&QR(gm=l0!%#$Y!GRsBV{~thE=C zr^_Y85b3bGQdXf%eSrx8GPf{K zk?se?FE-bz<`yPa64zY^=-gi$i&;>$W^-ouGUS74`MfN@7r<*!(C{jW#WKSajC4et z1qWN$Kd(3b_+*=W{K3Xw6cGGx=PcK0?(J;V3lY~gifrUe?1laVYKz{kR+}IhV(+p3 zjisy(91*-a{M26Eaic~Q#HF3wx^cCwt;mGNjvZ^Y{BG4a%=A@p@QDs9>7cD@Os{&b z34FnoYuCm=$E98Q=T&WFC8H&l=a~$iQErD}27O@dX1Yc3+}N(I$Q$R&$Szsn<pf_yGr-)atcU-!Hz$(tx~JC-4l0 z2>mS>M_q4X@){3Wa%h4*1s{}=9JMqK%YMkjPa>V2H{xF$*xoq_)apW+S^L9iMl3mQ zv1{x?5TA5aQSDB>nssw((nfwGjpPC%?4WR2ZiV(Ks+gC}B|N-w^S; zz#M_oJN)yd4SQD44nL?e;L8!Ni=N)rXD*S~FGS?LSuysNb8e!4=aVO-L;D}k9ayj% zEmGBEr?bmEq`R!+JSuw7)Zdx;gkZMdD&i3)hN2WC=KwffSXKSF?PuP&DmUh0?UMqW zKd2Ke78(uWadKSx%#Ye$H#wPIJiL4~eD`o4oH+-+FpmX1p(O+}X_6?@JbT~$>2;Z& zm38`j0KGlGuLbzcuk9ZO%MAUDJ=raN4J{Ar`x*B>IQ*k9!Niq@+BtLWiPzYO(x$XYVEQm1!u{XM zppYvo{zDlwJ;bN^r)`ey8M<0B!fx~6+_-~u1Z{y#Tm}zbp^d=D$m7uSXrKQNJA1?xqlAU=;Xdc>K>BQT3uu%m?RTAbkv8;A-BF zl>Sm##TnnZJQW5GGoRiWQU-l zzly43Ct404lgc5-%5gIpMn;KUx{R@mI?At-B@sBDDGU9;v+~?%PcAl%&>kNjL+YW{ zDC=b_8UOGePn>bJkig+JCyW|U`!^SW;#~%iwAi#mvM!6soao~S_*L0b9Ay#I&ZEbn z^%GAmikVFrPZrbOVp!92T^duToq&|}TJ)UDpk2be3xFFxZU2SnoO=5hOuz{4@gNNZ zhY_{S`2;=-FC8AA0llo}hn&2SoaLc{(@wpfO<_Su8Djgc4xtyYdMfV*yj-bS?N&+V zwoGGs5P#5vAs;*3-+C17EN}x$-VRB>2|pIm#{Jk><7%!iub(^y$I{HwS;m%P2?x{T zkdD*!m?ca*W|)~JFlcl8@c%~#vKz+Oey4{TLoG=2|99bs=d&W^4&*h)72pm7zUG3< zN%wT>R2S+Pp-@0+Is5tfxncFX!}aBTp>tK&zNhhhNW5R}b+wAn9vb(5_#RKIy!YCB zo7I&3N>%5}`Ki7geATjAEV%bke@Ljw@=de6wfZH*&)%K6{P1zldV5dJD^8j`v$yB0 z_NzPgFFrf-u=jN1gccKTPaJ8~;zf-GmF4OA$L{tD(69-*XSeEAiB8d>Lvf4pLc?qq zS`@(`9Qfz^?0&WFbDMk)v_1@r-}_uBOEdoYXZB7SV-}7cjP@DQVLfVh+5qcjix6oF zSr=@#DI6dARBox>%IC?+!N2vjl#1okXnvOXNsjyYttMq*Rv7K=jw zlsm9Gl8RcWCo~b{Vk>Goy|5ivXE&FnolH4};t%1B+a^%1Ol)`WUPy?G`MwBeKce6B zn@)qo8dQeS#2?vY$hQJpf&UXSes9X)xte{nlDr5Htwn z!=1m&{7_s*fkyqPvH2E?>}p}GlBBsiz&*2cW>n%S3uB1)5LrJL6bNw<0GKNAp`7{Q z8ywwI4ry8KXxxb9!M#Yp+3a1bfhp5LQh^WRw2Lf={>4F~1`@|a2&*P9&%ja>(mwU1 zaE)fqK3Y(&B>xl!xyr_G^LA#+fQ$JDMU}s;PxAFh5be1&R@1f%>8uX+gV>AFfTOKA zD4*h9nrI}rizBVAdx!|~ddhNHePOpKZ*#V;Y@68=_StR_pY)K*14oUNViRXr5Vpwddl#>2koK>7PB*w3jI!U*FX)j zEJLrR&6;i5w{NOnsr-6Bzf})<1%n^1n~-vD5;;Ps)VzMZ%;W%cjSa*laTh%l4XwqK zp^(!{>E@V#cjc-g$Yedlic51et%gNMN4G<)&d_doMCn3cli5~blK$Zp0%z+}#CF^F z*OBEn{yrh%Yu;}!eI7(OK5Mt&+<2Qda3@YS#PTTE2zQ1OU$u2AKPRvCGD%b%XU z{x*M9;fU`D)L-?KmnkW<^;`d5aA$!Q zRl-&lrukWCeaJS1*m3vOtM>c;-okeJX)6Bzj4gzMYmeGBxuD)|JqvwE@ov|%Y8M}= ze3<@*%Kr&~jed!ZJo4F^{6e79-BTvoFt!7k~hNmLR87{Qq2nFj=lLiN^F~fNcZeC^O$nb<7Rad!O8{{)JWz*V@@d-@bi3B20sK z{*OPl=CSab!7gZNQ!|Pw=mZ)&I(XF``)`qq3PtggPKOh}6vyJN%7_M&TZU?8@ z*5j^6j_sOGjK;RzJnkg$apO=&82bh=8-(}*TBpg_ZwQl-;#We{ZXBwIJ@%c@rcY^< z#5S4es)fu*RwC%4t`oBaCRYi0UdzwN+X zOcq2VOoPGeJohW342U|HT4o;oKH*Oa3;q(!DunYGZ%YjdAMOWCm@BA7(rSS5vDP?%;CT{p7U%*&A>i=;Ps{W#4 z6r&~hsj^pynu7o-no8VVvwuBz5080Q*V)>3B5U)pGxGD}+OJC;3E)EUFHf6VPpv~w zeu1rPW_GrS%VhY)84&G1hyC{niIEK_|MACm^LW-^4;k{J&L2KF=2#LY|9YiEjhAhoVd(cCFO+Li2#0{R!WZk*@KJ3dlwIuN zW3)&rEjGOwmKU;_N7pJrW~*n;EYjJnFK?)KG|ixx;9a0V!?q z%d0&wv`q(V_v1T}<#{S?^olBxr!Tv(Q zzCRP?0pvU&pffA%NkhF915Qjs`I3?@SiJ{S zl($!~OouIbipo$S!)ZWVw9HB(Z(%sdnBbX#bHHlKR0fW*K&w&2x0y?RFw=i|!kd2= zj6CD?k>%V11&>QXW_ym!v5bF}wLdE;vTls(EUVY~ZJPhwX26y?HsaI=DE!ZgB< z4MzN$Ce513-EfP`s(Yu~K7W;Q-Q?pZpCcy9DsS22Ct^SW@$BpZe?_SVBRc*&Ja{}1 zFuEJ8FH(VHt_<8+`@!R=hT#}6_`(|7(`R2hBJ_(@8Dln<6^ug?7Zi%UG)%MGfQJ*9 zWO%=1;^E4Jp|_HjjO)~y4x7*Df#W~;CT+B^2MC%npg@DQ!F8OhQnR4Z7DA+j}cS1g0Pf@?t_s~QkEI_Vm)U26_^3C(+JV>&7 zJzX@W-Y?DACIE{FGav?%^WX|Aq#}hMmM@_LZ24)#1}DIN5o84|DESo%->Kg?xbh-t z8M#}ZQMZI}K>b{|w#1D4)2`qyL=LmwSYB)@>_V7@?(7=*{qg(!^W(!OFMd^1fikO( z3w`;uWkQ}mG|@D`pk?O#uV& zGzH#wJEt_h7h7e>oX$FEju~XOMhaNCs$_C>%onj*;zx}q8Oj<k#cl184(HU?q)hlGVudAM=yDE;Tb&&9_23Y`Z_v80{j zIVbH-IUf>&?7tNiMt9|E-aC0m>U;jj`j@!NCJp#nIj=YitSpKry*DGRZ$tSsd1?qMM?yqh(w zGJVgD8Rxyq#uP;#ZtK_lG(!wbU%xTS@Z#Ob~g^If&tDj zrnRz(l!t{nK8}&FxO>a`Ls|Nb>H$G*9& z7+q8^A#XPPz6B@MdiE3sJ{(AH07?3~{hU=V5nT7s)Esm`R+nK=$NcFnVy<4nsiU|P zy-qtgI!bxKpekq8{;KQb)s3o#a)Pb6KA)qS8h(DcBnSs`^R!n|(|SZIBtKoC9@Jt) zeHT<#c9%0P9B}SEn(?q>PCxHbr%tA$X3lI4kG4h!e6eY{TX)O=*(pd}9ryI&ywQ0# zODvf52^T|V##~5zOjaZ75SKs7v}#8Kp5!t{w{+L3b&EtN$qXTl0$GL&ES=9ldUH&C z?Qoe9)tPX;fhsmulrG}CiwGD>P?eecmMyqkOn|D$94qG8vjdE0gvr2k9%)+*S(O*# zw7O;{V+DL1(roTE;N)pCz*%@yvo(HrD3C1qkDhMs&^$uF&>rz{L3G=V^%JtWoU3vE zh174XWP{uXxiXw zQPGt;wQ!lV)H;;2YD?8iFty;>p&403O#b%m+h^SCpK>mKRSadWvRKEqCLKuud|KH^ z&)9>a@E$l0*<)w-9M~$)EH!@zZ2b?+1qvc~2~u{&WP?eQ4{ET}wI`nyl%-XXQ*q8% zcvbn~MGbjUwcZFNUM9XY@)!B};%$cIpW+r5owlwzyw{em79m9-cR0WgRl=Xu5{{yX z38@(xHEA*m_C7Oi&`Bi(@`kY2zVXi7ieWt|^)|x>V{Nw~$s?;+w#cajRr1!t#(fw-xM7sb3>5ru{ zh95%~u$;w92OeVS_oIUr9&86K>!b(I!wjMbWH0Soe;+6abLz!cGJ>(78E|Wzr z60i7$+PH%d%G;xc`~3!@XHV^zft^%9p>8x-YW+6-x5H*je2#RlD!egs5z}V~&eSu? zLvKoA5kTbZYKgF)T&lnDN*6nw&PMpMD>*K|zrN})3Qo4_TLH9?X& zRq*#3b20cVJQk=)f$?sx^h%<<5_Ub<*W1%~mi*1n5j7^|LUqdSYuC{lFXEHR)Z(XK ztqa3`U=JCK(1-Vf^zpV^(v2H?(uzQuHA~m+2YrRfKq84|A_TwX3jSkoMi72Rz9}|8 z8y{~>1dkIwqD&jfqIHgj%}XlNY1Xo(SxeiXtJa=YiI`8?|M0C`@2@(cpQXLE{vSR8^HnfMLRTo3=cG_05CVO%58$d(#`% zZ}s5k57$>jz{@J`<}U%$7OlXf5vtHw}`( z9Lpp1KBx)>(9epATF#n=lYiC+?`$=C^%_v^HhDr%@w{E#3*TO72WE_pgS-I$;Ld{w z^~jTqbz~pd>({UCot$LlJe<=7Tedt>PL^QX*yAN&QQr;U*T`~Y(JX_EkXM%Fgqg`9 zEo^Bw$#wf2uw5{~bK$LXZC}zqQdZ1yTPDf@ve^qp$ULjQr?+k0YRk%@Cy1koLJseb zZwp~PG7D0&o9C`zlpiX8MYVTr8J7u`MQsf24o>W*;`qMf&Qe*;&AACVwUHt-fpN*i zZcRBXqP&3Z6%l(KP;wz%z2eKq7e$BbHmcr##p|P>rv1pzmWL_gWI+bsXAPkvHnt0p z88r}7&CN@+M$~_D(`g+6-#ELj8nqsUXIp%#pjF-?1X(|93v5do&4PvxH|BKws%iV` zmFlFej??if8}^dy!LOC3Qz0&dj8^h@)ZQM+I&I-Sq3{;ZD0-k4gR-BkY3hIF%Ia%_ zl7200x$mX{i;7@(8HF!M4mjO~#D#o9x$d$_NA?m!yltC3qI4{RGm{3Uw=cw1L~XX zTI~!JeAHkn!L!9Fsa=f zVA}K}-B7z6;j_2Swj+zkw1H8Z9Cqed6p;oX$Xp1|?}f00W}I*N*gNR#dniAC7ccH% z?(inH`UkM#hmf&ldmj;~bDut+_ugA(agX24T=snQWnPP&O0a`>OWWP2dzUwr>kYVJ zL=ZYUe8XW_($w-O2L`BDJEBqZNI(q~K(>gfcV;eIx^!3F+k`HXmx#QnB3bs$&fa_T zBZ7m2RTfkgfX42WV)rPNo|A8otXaXl@+xaITYihjgY?U;+hHe&?Hgi(Q~1_Uw>+%l z-tE6s`}K*kqGb}I7gInoLSH(F`mOr7NLRv<;?cc8%tUg;9`#Rf>o=(lmbmMDZKa4T z|JoL8yPtw;Eou4_Ope|XhXzc4E@meRddk4-len`QuSP)r6r(@Uci=yejmRXpPHECr zcYc&3HP>4Tc9ATx2BX!W5^v|dl?)WR;6Fk{&K0a}>uZ3I8?1A9e6sj3(n(<>hx5_8Ee?u3Z9DjtwipS!SRhCv` zuR*mChi0Q83uc@9ocp0O`q90Rnad(i7Wpw#q1-D3LEGcybq@-YQY+kSh4&%4a52ZE zO+Z>$jlm8bGheQ@G=nfZILYG3z;JoVE7fcwkR>Z#wM$Z0e@qSCC%2Wkl_d#nibsfg z*4v@D=fGeWi#~n*3|>R$VZ&0sJz$5Ft;3>4+3K5P1by?73%zW$pGM@bI;<=S)5Eqw zS$%96K=GI;G15PNtbdCdMF@|w%84zczVx6xY!tAez_3-zMWmb-RITe=1)(2l|0Bw~ zXq}SOyO5yN)YUW6*c8Kw==ZW0KrlLhDqwPZH20VvInd7b`aUzIz|@;NXknjTliI}L`-d~RU)oPEcB+-Lepm9BVdr+RBLQM7 zVyC&lK6Ez<3DQ(}?I;{%oAz5wD*fn-sPGEmFw2qz$ZjN)rIH0G8fzlL3EdNv_9wCj zQC5RT@`{USdoD8$V8YQuLnGj_%_KTEn|4QLPMGiqn*j8lgv`GcaxPL_e);V16?R#3 z`lnq;!3!$3!(|BKaJrFY$Y}B>TO6g<|JU7nxK(v+-`~e1_7)R~y#`|f5ycKF#zsW3 z0S6Q*#)1MC5D^g-j7c;qYLKERHi~c%uz&&rqQ*wCAjN`$A}F931uPW#eI{`4H}T&4 z{sAw~lf)qGz0cZf%`)bgV|He$tGz0BfCR0Ct!DJX{%@jz>n5~yU zKtUgmS1Fa65{Syzz>zS88!JqM8q( z0VS}7^ux-}lB1bd43px?t|uhB0%X!gGS9QN^j=K3<8?cBs`QAW)dK+S`EoUh8clr9 ze%{$d{J4}9S#g@1WtM#QyuYN`kc5}_l(7~q0}-tw3ms~8KiYOd!iX6o+?NAHdA=PY zWv(q(E5(#?I}eE3D5IsaidT@n(3*Q#Q?C6->Lpc8!ovGJSC^3v^L#d*DZ%y1=)SKL z;xMRk=%A=cu8LADAM^yw+z+pIx+?_e5MfzDJ_R?L2TJ1YmGBtHl z?jzU>nK^{O-KXtGiy9cz%LR1V{8B%_Nci}j zsq}%h>Ee*TS}!X`jVRagN)LtTd(k0)YN~v$HQr z;RM~h1L<52o#>46pilA5&8{nFOYSIb8c?V55m{Nx-61?@-SNd8%lTyDLSF?1fJ-Pi zgO9d5H2q2{er|S*ofhzMgK_487(3*i578cu`0ZmjU_u> znD^54iZ)7JbO(Fx>)fR0n*1gLHFNub9b-j_fiU11!gUZMKtRc)cM_I!4o$8{S^ra5)>>NicB z&Sw$D5b>co2=PPS3!8-QhU|CK6&S`9pQT4dL)+VS$dUSxPm?EG&o3x{D#fWN z-I%63G?TJ3PlwcPXbeDdClkBFeHwKA?jjo-Q`hT=Aa!teud`b*99&3>vvjnHp7^4D z#9i%r=@}B3N&HW_F7o69xIj288Ew;#X_vdNEi{=ld@d0mem$2Z6g?e3zaNP)q^SUW z5j~w!uPf^b04a;v!lRT(jPrMF)%6gqf~3yQwN?$8NYI7=sBtOzD?sHu&*yK=dHyJO zQ>tdOjW^sQ-kCW*EUY;bDkpS{g}Y#?62#+_7;n~l^k|Dou5ytl{>rc~PDH$jGawyw zZskNQ_&;Ddh5jNGm#e#`J`n0fb;+I?vF*DKZIK+>W;7KPDfZE$Y4^*P1@q^x0R^yo z?nHYE7ajJnVo}e!OQ9o^Na!TZQN+9q#I4b@ao+tyd9MZUK?rt|ikqq|&@sCK-Ca70 zVM2zV>;<4Gv3GJ@{vn-jTRt71B=I{FBTmi6YHEWK91t=Tr_c24OUKZr6Co{hLz#>(OTEN(d(=ROWcUoBaaxnBypI(Rs zRg=-iA~k@(24ZC=PM;pW{7nTlc7SGqg)gsF-G4gFQ-mjl^MyIyrbm<44myfHajy0R z2F%`0z-Me_b)AEJp7DzbAJ7qC=YLWWR#?Bqk2D z9YY}L^E0Ew?@}w8V>rZY`t&JNR411?w^s=_VX%Y6iK-Ji4$n(U>}o!MvXW|nPj(@O zh2ZMBZ+#AzP`axpXl+C8~{$@+2R3p$Gkl+;pTpGLb-Tk}| zrj5aC*sS1+eL9)GXjEZA?JUO;$SUCRXFZHoD)Vn&x)hvod&~_pOXs#Qv89p-4MkMx z@$Hho(-x2&n?ykDEyeB2!+EF8u6u?bI+TCpV0XL4i+^-B^KU(` zap;#!A(O_cP^XybM442L?bkJzwl%U9pPzRYIrh?9*Ew`Up4mdClF?i!cqxOZegYFo zVE5~L#OeN5;Bou=PHqVn&sM8w{A;nlwYAw%K#C=cSAY0W!r)DZNTW9UTOV6a;EiL4 zUN55zAgKvYA}cZwRv7b6cmhh@ruPrV*XZA{zD~7}V~FnTRQY+S`{A}wF{T`8^;gm` zv&zU+ZL3+A@@Db=g14Rn8W9?HmevYAUyZW%0M%1oq#brCKV5RcqUrsM7X!0T3%2(B z?rsT-sj!GPF#iPT4ser6?FrwHxN(316Gr*5^GE9R?|)?vlBH9j^N~)SS>i^rL&@_$gSlHZ~kAXFX#nD8v6#2oNK z7E^)fq^1~NSBPPbvnt;de51}@cwR~IDd(LN{q*Z?=#%kGHabOIiUISa|AU~QCxK$v z?J;lbZ(A8*wB^paf*eQ;apl)X*;{VZSw}rbFHqz7oaM{Q)LL7%g@;Whh1>gm>sH{4 zK%-r+*8$8uK_fTf#5Cv#g2W1Xfq4(_&@5sKmCAq*SI(VYzp$MK?2dr5W$~%=H!!7Z zQsp+1;(Y+zf`tN)g?H8b;QxRC{&F>VcD@Ct{ar?Pt!2EHh|=&SXeCND795`}l2q6% zVuo#75DAJ<&}7@eP9oYHxMZKRv-W+)9{dYgypPA3Z89fpYXh#RAM@;gqlzP8^fjW2 z7Bwp>pj{jR1QLZ%AQHl-HRr3}jNkE!F?jC=0R(Kbg}fE+9H&{xE0GB|kAL=U&9E4% z7gEH_o>}dgmX#%AHF;t!+cAJwqFT$I01aYYFj*pQMv_2?-iA8z<Ei4fH4M z%UhAu6qmB<*k>67C~QldO3Suw7r|dpJ%qf$*UBDf0uJl>c%62idF7kV+<012V!o(F zEP0o5Ivrw+LK3zo#RuQA1}DEXIU)kGE+U3X8gLj*rHKKLIvm(VQ94 zx2`;$>`Ek^sFmA%_7x*L8gao+nTH89rYKj2&tI|PhP1NMnM)W+kePKyPgAos0lDsj ztHTkf?CJWX10e7nkaeGN50EYvtO$u~J4@?j)z5m>y=zXaL!c1fO(B?7qX2M7#u|?)O>W$yaZU7AB~O-l!?Y(G zEvTmP?j-X9WHjom^fMuPW)gFvJGjl3%ji$8ntukFrFM4(k?WActA?ur?)^)ZFlMJsB94YT6_22tvHT z-`5}m@EgumG?`fxCQy_xue-mXof<$mH8j_8FYWzbGB0Y|^ZNJN*GLn@5_>+kxW-6H z&2W2jMqKyV*6^1rT`6&9zOM>?@8rBpb#{ks(!(_$DzA`MI^JJ)rc1f1it+HO+ZX?8 z@Trl_rJRBEzd$80qYknKyEVOEZ-*lL`((spN)aqQ2{{8b3X9TK($PI;?a&p8m0tm= zwU@cHp`lv@bCKii0WkOJQ!Sf42jh{U9q;I<4GM$4OlP0TgiKR`l!Rmz8{2tgr+LRz z=7r+swr_s|z5%x@@$!b{B*F|r&eHAaLHTTmOxw7=U#5*nXiY?Pxd!M*`03KC!Z?wn zS9a}`zP`RjP$(hoM^s;3I9Qv$bLrFoh3HI)3=H@QdGsTYpHK41=@fLsb@<|o3&5Q% z+P8nY!o9j1*KB_MSAF~Npt}@IWlIq@rV#XOuRRTS9z|Z;g_49Zm zEoFQp*}-sPBGxA}zK7F3Xw=(YiO={u>No!(&uoj{HW}oZJqH+k1*qeha*>T32&3j6eklsS_cZ1OGJgFd^YTa+_fa(C(l>#mg!Hk&%!`sR(av>;XjzX?8xhSw85 zA9UcAKwW`@Ukf9ykRt(bd1`i<912~VF{4K##573NUkIN-EdW4(8WildZL6c2L6i)f zyBSAGL5haFZnRrkAU$^_J-sd3WlCe=o*SC&xSMm1ybtaG& z@FLh5U2lF=;sFY& zES*AS^Xh-Wc)rAQGuEux$WF=o=}x6f7RH6d@vPil=F4LFsE~-B%W(`Kr4>mpg>yKc zRx3oCug^cr(V-~}24J3fwLqP;wy4bS?@h!b5M_PAXo)jtMi6odg&incbdlICn~}{R z9P6vA+towhJ%nw96k=$&T|4Rc@#9_Jc1Q*~MJ?&QFulOBdyHbD$0`5$Pflk$O6AgT$R67!0UGAnxfPl{S;4h{_^Z1__8{#eY0XzKu z|Jd;oQ0JEP_coWGg4erKV3S#&L~?TK_AWkf82$t=`vinMMZGTtKB3=ZHl%xnusEm~ zLo^Y6tYjFal=?pdN6M=4d$gaL1_mb`KWV?T|0iaV<|pS&dsLtrpIY?S3D-CPp^Nk5 zKr#xPE*)RL439ywdU@gCpX}|oK{2C5tv~xNhM^%`lJYWmOQ`pAN}Is5K&sB|v>Q>5 z`m?mfqFqs`9f21Q+vV^Be0o8Z`M7Z#9S5YZvy8-+fE^c@TX2RUkrZ+$;2_lODpj!s zzD{%7prCuK6OmEDqht=RvYAJE>7cO_E$dy}L@gm7_1f^R56Kt>jq()Ds6nWKlWCNk zwXgT6et)W9F4WZg4sIZ4rF-|KCM_dGK2SK)>@SKbopSd!w6ka}xhu#4$@TGUhipUv zg_^1rJHsJTw^&qEK!+ei#D_ZX$vV9g6bhWZz<2KQ^VhGJd7W17BOH(pWVmA6Q=-hK zLh4~PE_Y_O$H0;E%sBCYMpH1y=&63g=n8jt_ieRnzq|jPmR1pf9SV9YLF>)t{;`UR zmq_q{6++Y6d~Km{lTR)=wV`$`QRkuJ*LAW#=R6wA#k^}<cp0 zF4_&3sD&8kS^jZukkgiTWZ=r{P=bUrOoz`WbLXZ$&ZFrWjI{D+)xxQ@LWmL#Dap+? zHt!kwvTRGPL)wT{dk)^Qn!DxhI;Sn~n|&U-Zb8faZa16M4D7h9o!+OH*+cr!4 zxEvT}1UO!7Q8Vzkg|>H_*}wR2oZ{n~{%CP>-zWUv-bd~~Eg9nU>~30aRD8vRh8JBTvVzKxtBP`q*cp?w*uh=5*WR}G<%PS z8NB92Opa6&8W`(aEKIY0+tNgW63n?wl8am$e6NJ`Z7`)VnNviw87K=zBg^H?lYvW- zuR>=!EB2~u5>zLYGTQfad#;UpTVV2?9{ghHgEABt7{MxCD|M1Cd8@LS!3i3{5GH%lnKRHI(KRq3nwaF_83&%HVw+xm{kRrrj* zuC^jGE|)R`z>f%2! zNqVF3nQ-23>LsU&r(F-{tHLJrn;f^=$Ink15~uAHtk$+!}NjZPM z362>fs>gSWnF;7m-e|NfXeY;-FrRc5gD~6gq8y5a<=1kiCmoB^UVEX)a?;EK~ zxh=@F-{+kBa0%wlcKn5;8Za>uLfy~S?=1^DzbCf&3)EIrK?UCDct#nWnYi47OfSc< zw{?jYgLb5Pf)Qh$*^R2Q9sl=ufBRGBxJKhI2rUIap%IBjZU6p9tIGWAKZ}~#f`0Qo z0FqwQ_wtH-U2p*t6Ym4jU$1Q0v3F9Ex38~^Evc)`tCPtMO(6{bm#@e#r5_H02Wa~a zl3pk=8+Y!U(by>PL=`q{VjG#A1>7m!S)>g@Yr^na7bpF`B{LkGd0@@smV!@9^9gLS zlCx7A626cAZM2QRz8vE5E6Ow?4KAJf|3{f8Ui^5{aNwTb58hU7w+l=Qh;JPqSo| z4~8|w?H~O+>@tPzH57}5gF+jJyO3*yyh&cl%1)El5M(vS$9kd8BPpEm*C$-dZ8+32 zZ-?}26a{Ko;<61}w`8giMTeNgJZfPzaU?I><9?~Ag|F({n|@T92Beih25UmI1K}Ub z;}Bd&9?<{j4K)65j1Jc!CzXg4m>PMlHX^Emwr%6HJ@I$A)o1=GF05BuVa$bv@tB4^ zL`E#m0@hUA5de0my7kS?RBA>uUF&Zxs{g!gT~y+zovQeIbz?uw|EhU2wa@?TGH&Lq zCdLy=d{6n_%^!BD`>EIVJI)PFw||)OeWhmd%|xvAvVAI^fPZayKC>QTRaZ+ z)#zQm@#4$&ot}34-J`&9jb3)uF{gLWZ_ro%;)Z!-fonipMY0m^E)$?8tpz+<#H?ke z%1$DMHjA;#O0(>$P$(RJjC=ICwj#G3IFbKY_at)oFrot@xN8J&d{e1=4p~spb}t{F ztO1c7`JKOiB}X8C9STcs35E=<*lcLI6;Si9Nrn>KgdcTLpX$^}p-}$B?EwlkH8iH+ z7kq9lF#)pmxgLXNJ=?9t=b!%lvhsU<0&j)lmEKoX*-=X=fhtZdr4k49Mek<(y~Yn) zIwxBOv!jSNa_Dj{eE$4#$;Y+R*4zB1|6^QHS`<*r=qN7vrXeQQ3!9^ygB_$lVLTSeeghD?BBo2y8W`5?|IDK^trZ&8dX$rZ7@h0?~ytBiRy?3$^&cO}9?9wzRNFv()m=B&#rl2?sMQkb(%#oX%Y$tnG42MiiClccantIJ3CiIstMF`#_|5J&?^ z?5aKg7ZJ`Fw&Z4V!v=iYlnv;x%@QSLAaq7cR2xP;y>ZYFsu@vaII|!_StPr>q(thF zi|CRo^ZIC)d6fEkdk2vb%|Q1hjGE6d;{C*$*QqMYv!@$RmL?}FxeeWADW7d?-$OTu zG)-sZ$ZvUU65^0~FtR<<|6`G6h7q(ZfthN4%ElA^;!!9K3{IJ)nicq4<8`NU-TZbl z*$pIW8iNOCM8C*8XUzv)xoVXOsbWXYCW3=-Jwo~A|6HG4>a{Sd*1`vh!E+8=;D1ZLdr5VG&1s%r{`qw7K}iQO{jcvng9fYdGw~Vu(M8d znXe?!ak=)0_lLi~oCQpw#y8*S zlYU~B|5#PH^%CGNb3QB6$VC77m3Dr~P&=?*e?%(8kn%L1y~bL>hVl0F^o&cD^_)4W z{^F}~mV?99dW~K)6GDqu5S+z5rr&ElfP(m+*In)dfB}=ikTy&sj5MxJc!C-uN8V{T zS+VW7sA3Vbd96EquSXF*C{^>T=AV5QN?$-%~*1UNeO~>AwRqVd#mK9ac$G_R>*Su(#4a7G?el2SbqH^sHDr? zkMFS+gUl1?Lt{9>375^~*>j9JYhptCV#Bud*ylk6k{?e4p#~SHZz1FbK5v(Qt@Yy14x3jo!v^EbSP|D2_n3590|EX_+gboY=589d@((6}cv)^oxGJ z#2~CGlP9zIzY}Aoezpyf!Yp(c@bP=kF8$|+@NhFl{u?d8V<`7L+uUozOiX750ca29MOIQf4p zm`e-@^8m|u6R~j2@jAqcTkz3LVb?DL_bPiW50^V%UbxLqgE}+(PaCJn_N>A~c^^2S zWx)EFfjdpp>;&D+h<#%s!JdWAKR;D+fach-*03wIBXpl5>c-RB(OQ5E-XD#hr(6Ac z%>tvIcKGqRX4}il%NH$J5D3=1@4}_vC=O;#*Lth+e;1C8lE{Gw!8~zo43&XO`o5q2 z+R17r83G=DLBrGUX@Vb1?h&;r>v6^pj>VXrQ8gX?=vSR7Rty<3NkLm|99p3 zKb=XV09i>g$<&@jy6fH@rwBZhp=)h>=~?_ceDl~_|J*TT2NkqiX%KE=O@}8^%kWlb zq}^nB6Bz6#FJDICX$g>ign(uf+5Mk4%des~w}f>xPg)n3R9!YoM_1QQl)3fR#iA+L zxN}E|OHLlsUt9ZxVeCk51>irpbDNW=Pn)tyGNDNjz4}|tjb3uN-psH1=S@6TVoYYx z-6FxF4_1?~&z{kT66?b4|JO}Emz%6VKX1^tNc+N^pS5aLGz{U6Uw#=#^{7jmW)hm> zbx6iFPjN#PcCARO*FW!-yBXxLl0?%UynpM?oqhx%RE$C-9zYOlgo~^Ee3^r95g~-f z{qqEEtW@^cy7w~@NsAD>Fy^)#Ewx^~qIg5aT*?o*2NL>95k)67WOa9K-nx3D{&n-PBBVU=WO57YZT9n2hFLe-ReAx&RM0B z^$dLqX@Ak6&dc9bHGHhtQNbZ0BqfFrE^Z_CUikR&5lYXc0WJ?`IXL_^?fueL{9tnJ zX55LQ5IqoI`j7}Fqn6NCBauHRK#fDgd$wb9ii1(tp#=N$HH?|v-IKw^iHV7|FUsU9 zd_@}D9=~z^{3F#B>cT0vZoOLVv&s^nEVB5WBsLU6I`-D8LSdc*DUf5Oo` z`r)m6of(@ZF)*@e{S$bc&11+!4BSJGO)&ts5~rDoYnygZ&ytKLq3DK-#K`$CTXgtT z;owR5@Pt9pRKxw>unC``J%PU69trCy01!JGV$h6Nt~?o(YTbQlM$qqUqdqj{xVpKe zVh2&o*cBcg9v9yb`8mvBVDe3Xj$=SFbEs~V+&y&^ZlaCd*bOzYva4lxfqzv!-Zt<_ zNr{U3!-JcCC&|*~k*Dn$G>-7^Uefxryn{+$E}uF5`i>Z3hI;s3cq&gyOT$^AtLM*; zgB3uCGIQO!1i7jZ6y&!saYNxS-o0~2Vh(sOo?_E!>R{fg)grq=o!d}2-z{a0+Jatn zyKXH>IC7n=E5iDml^98qoMFr%AeJ@ul6%3lv;{|43q{LtTso#tMG49)2;0|~quv-@ zzFpmxlDk;&GiS_jT83{C05h9Cdk;HDa(-tHO4`mB%51Ay5nhG`xd`CSF*h&4#f2K( zj|p9pR@d`%(mlPf$F8EVrpZh`uS^a$eyte7#)yDYJT0K02}Kl4<1Y0v6*niSK-qqc zA)lK|rgn>R6y$?JZF`Q6&CoMY+bMlwG01^QEG*&+j1{F89zwZ_dc6UQk73?T3Pk}` zOM0MoZd$eA#$wxN=kNz+(UvzX?j^dNA-oI%mXyr+;<&7( zorZL0xGa%y+(xC=A_La`s{Gfde3baI<;#D_ww zx*bFP4XjC&>FT2q47crtH-yBez8{ z!u(^QGC&R>F9y}Ut^07W_@P>i<+hZ|2`Y_!eh`OpFyKRB#}FG%aW2XHgLBVp?jvkJ z$(_7{NHXkE;*b>q*;K81N!DiI1+02hqcZG>Bdy3c)V}!Qh*$u^+S?SJBf%BzIbdCN zRt8CfQf;Yq9q5TS!#I`3OX5@o4qx9mdTU804!55H%b;<4b)r)k5Dg8PUg|`Dw^It# zJRfBpcqCYfTO+8JTa6j0peZ<%jYJyRO-GKok!cIgsEW$!y$Bx!&Uub&0a0sgzG0O8Pvdb15AP$#tEKZ&w7 z>%sLUVpzohdZMg`D^bwp|AQ*|BE8Fq^c_o}IPFqiShJnSo~b(V&z2E?foxr~;iRBqRov2*bRVJUDv> z5XCSjlsu2%)T}6;sv)j?!rt|~J1^SSpw_w~(_#l(kyPp?MDFlR%p&N8 z&Yt>$Vi<=Q{=t(-6iQroH7`#RCz^wvlCsv@HkC3px3Ix3DqrlwdFT5=E%ipKT@(at z+(Grk3A4Dxs7V5>Wx)>P_I=VA@K= z{nL-2dCHv}4RwJ{z5}fz8K@Boxyehb`THqyj4=p_hpTh2vyiJmw|!RW%&z9-2%U2? zVR{xOh0OgLRA45*cxk!o3uXe`d)pZ-t)GDa73%Nby_@U>0qPqR$P6+7=}66vP!KRM z0%k!_O~nFuhh59d%j=jj2GCQkOHONSed^RHL63OY+d7hBEnFC+SM}`cTIBq)*&8v) zYcQ49EF?cLxX|aEvN~ry9Bwm$dFkiAokW2AjNQ5trL^e8ljigTf~pO0esNC;c8O?0 z;!qkE4OGY3NJH63S*|)Hei8;r_QZzjXFxm#?rg#p*H%0`Oe<;J*|YnKCQAimvKKTn zjTgqsqRCi>eg=Xu7x#pK|HmqwRdZE%39lf8^Z*KTy`J7C^N|AqP9(u1R@z4=N@IQe zf%ist6JqZTX;c4NJlxD+Nl@fFj4>_6{F$;okg6h~L>ALFA~lVaS_?J#;pGCoD{?oI z(vfuzr1MAfo|aWYCRS`LvltoB*Q7{<1KTR41se^?qM@rABT_ zJ4>W=4!D)+pzu4Fs^a)_akYCM~d1Dg*p*vBtdUXiA#X4ve0Ib@!=H*fHD6 zoju3M_sR5Y{L455vkbfr9<&Z#uS$va(;Mp^fI`}8v+j%S?KO*Er`*gL+)nALHx;k0 zIdbG|fk4=elI+4n^K)&te3h2Z(UY!@F^d~f$hW?*q}GGYR^h8-2w!lUj69eOWA^5k zGe-YZWk|QQLXr0C!NH#>^0gR*a*QoqiuzF&K32lBS|(evp32p$PYcXNx0P6KJNg9x zN-MCf_H<>oYSCgB(>d^`9lLiw2I5je&vXXXHOA$oRlr#Hhq(_LBJbEfoKr8Y&1!Y3 zp(CFma#d(AVJu-X;YhA>r;huF1nivgBWKBw7B4=^XPe^1fcntI2VC`eNw+9&RKSmV zMr@J^qjrw%H2E%+=4kAZ1eb;m<-})R3#$&tg=h6O07p$Y5>Zh@-NE~=HB;^)EaHp= zr6WKA%e1r&$f%3>vx=Ss%UcA7aZ~Px8`I3Jowk$ns#TuQuB0GTBas+ zrvyOU?3|z6gv>&OIB(O+R!Lx7%1Ngby$c{kweBT##$^xUl^FcBhkVX8Q}eupmkv{^q(5JF29^me{-6-n5>-=Dq}e~1avK@H zEfhNrnK#K)D6xcK1=tZ|N@o>#AUp5}fky!BMRT|))urL#T>Qy^uR#~vddS}P^O5T6 zK`A*WVa-J>_xJNNE7O)d**5r*5t>ztK0G|Ncqc0;>CNQ$gz;hEDDb8SQUawK{Vn}= zSa{WO5#i?O&scDI`*)|R9E+b0lblU`Szc1@RwqyWKy`t$pkV^B*z8sFy%8|#D8;ME zUQ%f!5VCOeJbGa56kcWb=lS`E@X>>>NU8JZBP75lqoXsAuKDtJg`&N-!NhSUzzc@e zAjwWlk2#d_CdDqD${njM0Y&=3vDQ@XOxW)DU9+FaeU_$$mzWnVVoyjaNKIfex|0Hb zkeNhMb(O$_?>-y$z>RK>5Qk%p=eK*D)rj)<-_HQ<5p~J>Q;7WQf_v^?I~7Mwd0L#l zr;~d?dBc*%o|^RIkAn9IHi1o-(BVaHoH6ZI;)x`xV0r9ul4Mm$ajB9k?25RG9DVZty>Hxcd?k73&h6c?kp_r z-VK83L&>0xEKr@pq)#R?6VFT55e;$i#obds(ng4GkoUfQ`y{Wx@P@nBeUK=#^3mpA zb{tnh696nUmb%bZc-)koPMeMY`1|+w2w#Q||5|k&C;lUaw;s$aiDLSc0N1sv@RYMG+4LU0bS>`_v!YwD4NE){{IIR0Zg8 z&ZEwV(|A|6@pFkdSNE)8TPo59_UkuE&_viivi)&~w)9uaCgd1}&|ky8Eya2&1;+<{ zY^ZJKtG=Hydph3Tv2FD0VfhB~!F z)R3;8k`;^8teh|A=FYORIskPzjFTr;OTac6SSoldnuN&Z9mLOvP-GAn3zK^*G^TzA z0;n+r!Q|YA^<${g>*8PJF^Cs%VXlx42S0G5+Cd7FNI)yeK24|-gs33dJxR_~FPeE& zfN|CxdZLhUSTr%JNQwSrv^9l%zSI~1~>v@Cv%TlKzs{HXPoX54TQTTAYEWzGl63T_C_UX%!hXcQl}#tSSTefqk2fwU&=U&5yuW4 z`T+rSUDww^#g$R_DF7*}W=Lxysi%s(SkSY*Z&Cc+MY4q=NqR1bI$sV0QFt zR0~@x%}wVIk$%y7FilTS&#d-vG-am}fmC6205LpadM416w~h^)0L0x$s6kRqAQBuh z#29wOJ?f%+)yz|6(hpyIFC6bQ(8?R>w!I5LJe3v}$A~{l34m3`M|aR$n9_YW?g+{l z6&WIihtXrP5ulA`SX;VB(4L&yEs~lawDF7L2YHB`RHkSr;Dzu#17w$*>dX0ioUy3uh$-#pwr^T!Kmc=5a zHsi$#)I~T>Xdd!WatGMH!>4t`cq(_M-f{?kqT}|cp_A5T&(573_ zo{3^ttH_NOJ>+EH-#$i?WrgBfYuhcc3B6V7J;rXZfH8)=!yiZ%?q!1S&_q=HLVS!I z-IN{sHe23YQYiTvVU2}DnlTCz<0QeC37mkDg9xL;0RvmLY&naf15~NmW$nAmU6LPM zkAFRZKP=xK5D>6#-MS|Q1x{HDaGQLZ@A~!cui39(a9TB|ggIQs_^hElAjGa{)zL4~ z2HN&dt$NWYHUpQA8I12?*^rg*Qm4$M(ZByw)eEp(*7`PGwnqht4;qA zjM?P74YwIZ{ejy!93^VXGbqaWSTg1 zIxafYQoSr}_S>hQd}6rqk%xyuDj-}gcahvbxyMF<5n~5-@ze*A0wi)`W6mPMC-Mx0 zrvfc6J^Cf7U&__1-#SGCeF$lYj`&bexR9KTw}s2recOOwH{MuLNWru>wh9E~=9k%4*v{n~4Zrq5!)yYejee2PxeicF|ZrJca?fhf7(Io_&A%we5Q)bMN zYNrolWs26dZoMu1w{0>*u2Uz!!5g1sq~E)D&v48?2tpqF24cj-jy9Hq=m6spN@(T` zd5LU7JdSe)cJXy(r4P^^ha?j%WC0=Km>I@0y7Un{;jo*r5PmH?V`}$(a@jr4y6X=p z0>nSorN>W7fSWd5RJ)(`hF(~T6W1!vaNfG;|7!*6|C?MJLZrhxA$T(6zId4#B!F`G97cE@q-EH*st5;{zo4I#AhmoyB2}|;A z=3n7;gEjJ)TWLVfuh#4j`Z$z<`fYFP1dn@S1ke@o)_v^h(X2X2Tf%R*8VpI}gzPw2)}?ljHv88Wb-O|1ZE5vHL7}RDTR@_^c+eB0%RhS2v&3a z1u+-D{(9*}iH2M?U!#8Q^IL&K@@8j&e1TOG+lNaES@%2KQ|DKYCvjZRw@HDgAU8PJ z9uz7B)=2rZ9IY{CUd3a3PpPfFb51j{eoHkzt-@ogk5;R{%!E5Y84C~217ei>3nq$< z5S|g=jJg9)2fD7Mu>-|ews*b%u+aRMSNPY}8vmO-=5j`EL*Vhz-*Bws>OahXd>@0; q`F~%|+-OhSw*T@we9NJ{#@*xWpO?mH)SZ-{FqkxbV&wRFKmR`|C0ZH) literal 165656 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�yG9?^ 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 WR%}9=_FYv5An Date: Fri, 30 May 2025 13:55:20 -0400 Subject: [PATCH 022/115] WIP on runner --- tools/cosmovisor/internal/runner.go | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tools/cosmovisor/internal/runner.go diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go new file mode 100644 index 000000000000..fb43af792d6f --- /dev/null +++ b/tools/cosmovisor/internal/runner.go @@ -0,0 +1,69 @@ +package internal + +import ( + "cosmossdk.io/log" + + "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/internal/checkers" + "cosmossdk.io/tools/cosmovisor/internal/watchers" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +type BasicRunner struct { + logger log.Logger + cfg *cosmovisor.Config + // upgradePlanWatcher watches for data in an upgrade-info.json created by the running node + upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] + // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator + manualUpgradesWatcher watchers.Watcher[cosmovisor.ManualUpgradeBatch] + actualHeightWatcher watchers.Watcher[uint64] + heightChecker checkers.HeightChecker + runner ProcessRunner +} + +func (r *BasicRunner) Run() error { + for { + select { + case <-r.upgradePlanWatcher.Updated(): + // TODO shutdown + case <-r.manualUpgradesWatcher.Updated(): + // TODO shutdown, if we're past a manual upgrade height then it's an error condition + case <-r.runner.Done(): + // TODO handle process exit + } + } + + // start file watchers + // start the daemon + // wait for: + // - upgrade info JSON -> shutdown + // - upgrade info JSON batch -> check current height -> shutdown + // before shutdown: check for current height +} + +func (r *BasicRunner) RunWithHaltHeight(haltHeight uint64) error { + correctHeightConfirmed := false + for { + select { + case <-r.upgradePlanWatcher.Updated(): + // TODO shutdown + case <-r.manualUpgradesWatcher.Updated(): + if haltHeight == 0 { + // TODO shutdown, no halt height set + } else { + // TODO check if this would change the halt height + } + case <-r.runner.Done(): + // TODO handle process exit + case actualHeight := <-r.actualHeightWatcher.Updated(): + if !correctHeightConfirmed { + // TODO read manual upgrade batch and check if we'd still be at the correct halt height + correctHeightConfirmed = true + } + if actualHeight >= haltHeight { + // TODO shutdown + } + } + } + +} From 090cdfaad6ec2f577e267509cf38656f96a855b3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 30 May 2025 14:03:44 -0400 Subject: [PATCH 023/115] WIP on runner --- tools/cosmovisor/internal/runner.go | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index fb43af792d6f..a0f52408648d 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -21,27 +21,20 @@ type BasicRunner struct { runner ProcessRunner } -func (r *BasicRunner) Run() error { - for { - select { - case <-r.upgradePlanWatcher.Updated(): - // TODO shutdown - case <-r.manualUpgradesWatcher.Updated(): - // TODO shutdown, if we're past a manual upgrade height then it's an error condition - case <-r.runner.Done(): - // TODO handle process exit - } +func (r *BasicRunner) ComputePlan() error { + // TODO check for upgrade-info.json + if _, err := r.cfg.UpgradeInfo(); err == nil { + } + // TODO check for upgrade-info.json.batch + return nil +} - // start file watchers - // start the daemon - // wait for: - // - upgrade info JSON -> shutdown - // - upgrade info JSON batch -> check current height -> shutdown - // before shutdown: check for current height +func (r *BasicRunner) DoUpgrade(plan upgradetypes.Plan) error { + return nil } -func (r *BasicRunner) RunWithHaltHeight(haltHeight uint64) error { +func (r *BasicRunner) Run(haltHeight uint64) error { correctHeightConfirmed := false for { select { @@ -65,5 +58,4 @@ func (r *BasicRunner) RunWithHaltHeight(haltHeight uint64) error { } } } - } From 869406b693f253ceb6b146a5013d36f888d329f8 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 30 May 2025 14:15:47 -0400 Subject: [PATCH 024/115] WIP on runner --- tools/cosmovisor/internal/runner.go | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index a0f52408648d..6ec3197b3214 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -1,6 +1,8 @@ package internal import ( + "context" + "cosmossdk.io/log" "cosmossdk.io/tools/cosmovisor" @@ -59,3 +61,35 @@ func (r *BasicRunner) Run(haltHeight uint64) error { } } } + +func Run(ctx context.Context, cfg *cosmovisor.Config, logger log.Logger, haltHeight uint64) error { + // TODO start file watchers + if haltHeight > 0 { + // TODO start height watcher + } + // TODO start process runner + //correctHeightConfirmed := false + for { + select { + //case <-r.upgradePlanWatcher.Updated(): + // // TODO shutdown + //case <-r.manualUpgradesWatcher.Updated(): + // if haltHeight == 0 { + // // TODO shutdown, no halt height set + // } else { + // // TODO check if this would change the halt height + // } + //case <-r.runner.Done(): + // // TODO handle process exit + //case actualHeight := <-r.actualHeightWatcher.Updated(): + // if !correctHeightConfirmed { + // // TODO read manual upgrade batch and check if we'd still be at the correct halt height + // correctHeightConfirmed = true + // } + // if actualHeight >= haltHeight { + // // TODO shutdown + // } + } + } + return nil +} From 20fb95051135087f467471e0f5a1bd67c76766fb Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 30 May 2025 14:29:12 -0400 Subject: [PATCH 025/115] WIP on runner --- tools/cosmovisor/args.go | 4 + tools/cosmovisor/internal/runner.go | 144 +++++++++++++++++----------- tools/cosmovisor/manual.go | 10 +- 3 files changed, 95 insertions(+), 63 deletions(-) diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index 1a76cfb375d7..6da35c8be6e6 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -458,6 +458,10 @@ func (cfg *Config) ParseUpgradeInfo(bz []byte) (upgradetypes.Plan, error) { return upgradePlan, nil } +func (cfg Config) ReadLastKnownHeight() uint64 { + return 0 +} + // BooleanOption checks and validate env option func BooleanOption(name string, defaultVal bool) (bool, error) { p := strings.ToLower(os.Getenv(name)) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 6ec3197b3214..18c1f75f1593 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -2,6 +2,7 @@ package internal import ( "context" + "fmt" "cosmossdk.io/log" @@ -11,46 +12,106 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -type BasicRunner struct { - logger log.Logger - cfg *cosmovisor.Config - // upgradePlanWatcher watches for data in an upgrade-info.json created by the running node - upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] - // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator - manualUpgradesWatcher watchers.Watcher[cosmovisor.ManualUpgradeBatch] - actualHeightWatcher watchers.Watcher[uint64] - heightChecker checkers.HeightChecker - runner ProcessRunner -} - -func (r *BasicRunner) ComputePlan() error { - // TODO check for upgrade-info.json - if _, err := r.cfg.UpgradeInfo(); err == nil { +//type BasicRunner struct { +// logger log.Logger +// cfg *cosmovisor.Config +// // upgradePlanWatcher watches for data in an upgrade-info.json created by the running node +// upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] +// // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator +// manualUpgradesWatcher watchers.Watcher[cosmovisor.ManualUpgradeBatch] +// actualHeightWatcher watchers.Watcher[uint64] +// heightChecker checkers.HeightChecker +// runner ProcessRunner +//} +// +//func (r *BasicRunner) ComputePlan() error { +// // TODO check for upgrade-info.json +// if _, err := r.cfg.UpgradeInfo(); err == nil { +// +// } +// // TODO check for upgrade-info.json.batch +// return nil +//} +// +//func (r *BasicRunner) DoUpgrade(plan upgradetypes.Plan) error { +// return nil +//} +// +//func (r *BasicRunner) Run(haltHeight uint64) error { +// correctHeightConfirmed := false +// for { +// select { +// case <-r.upgradePlanWatcher.Updated(): +// // TODO shutdown +// case <-r.manualUpgradesWatcher.Updated(): +// if haltHeight == 0 { +// // TODO shutdown, no halt height set +// } else { +// // TODO check if this would change the halt height +// } +// case <-r.runner.Done(): +// // TODO handle process exit +// case actualHeight := <-r.actualHeightWatcher.Updated(): +// if !correctHeightConfirmed { +// // TODO read manual upgrade batch and check if we'd still be at the correct halt height +// correctHeightConfirmed = true +// } +// if actualHeight >= haltHeight { +// // TODO shutdown +// } +// } +// } +//} +func Run(ctx context.Context, cfg *cosmovisor.Config, logger log.Logger) error { + // TODO start file watchers + if _, err := cfg.UpgradeInfo(); err == nil { + // TODO return err need upgrade + } + manualUpgradeBatch, err := cfg.ReadManualUpgrades() + if err != nil { + return err + } + lastKnownHeight := cfg.ReadLastKnownHeight() + haltHeight := uint64(0) + if manualUpgrade := manualUpgradeBatch.FirstUpgrade(); manualUpgrade != nil { + if lastKnownHeight > uint64(manualUpgrade.Height) { + return fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d") + } + haltHeight = uint64(manualUpgrade.Height) } - // TODO check for upgrade-info.json.batch - return nil -} -func (r *BasicRunner) DoUpgrade(plan upgradetypes.Plan) error { - return nil -} + // TODO initialize watchers and checkers + var upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] + var manualUpgradesWatcher watchers.Watcher[cosmovisor.ManualUpgradeBatch] + var actualHeightWatcher watchers.Watcher[uint64] + var heightChecker checkers.HeightChecker + + if haltHeight > 0 { + // TODO start height watcher + } + // TODO start process runner + var processRunner ProcessRunner + defer func() { + // always check height before shutting down + heightChecker.GetLatestBlockHeight() + processRunner.Shutdown(cfg.ShutdownGrace) + }() -func (r *BasicRunner) Run(haltHeight uint64) error { correctHeightConfirmed := false for { select { - case <-r.upgradePlanWatcher.Updated(): + case <-upgradePlanWatcher.Updated(): // TODO shutdown - case <-r.manualUpgradesWatcher.Updated(): + case <-manualUpgradesWatcher.Updated(): if haltHeight == 0 { // TODO shutdown, no halt height set } else { // TODO check if this would change the halt height } - case <-r.runner.Done(): + case <-processRunner.Done(): // TODO handle process exit - case actualHeight := <-r.actualHeightWatcher.Updated(): + case actualHeight := <-actualHeightWatcher.Updated(): if !correctHeightConfirmed { // TODO read manual upgrade batch and check if we'd still be at the correct halt height correctHeightConfirmed = true @@ -60,36 +121,5 @@ func (r *BasicRunner) Run(haltHeight uint64) error { } } } -} - -func Run(ctx context.Context, cfg *cosmovisor.Config, logger log.Logger, haltHeight uint64) error { - // TODO start file watchers - if haltHeight > 0 { - // TODO start height watcher - } - // TODO start process runner - //correctHeightConfirmed := false - for { - select { - //case <-r.upgradePlanWatcher.Updated(): - // // TODO shutdown - //case <-r.manualUpgradesWatcher.Updated(): - // if haltHeight == 0 { - // // TODO shutdown, no halt height set - // } else { - // // TODO check if this would change the halt height - // } - //case <-r.runner.Done(): - // // TODO handle process exit - //case actualHeight := <-r.actualHeightWatcher.Updated(): - // if !correctHeightConfirmed { - // // TODO read manual upgrade batch and check if we'd still be at the correct halt height - // correctHeightConfirmed = true - // } - // if actualHeight >= haltHeight { - // // TODO shutdown - // } - } - } return nil } diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 5b3de7edb027..2fa40699345c 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -82,15 +82,13 @@ func (m ManualUpgradeBatch) ValidateBasic() error { return nil } -func (m ManualUpgradeBatch) FirstUpgradeAfter(height int64) *ManualUpgradePlan { +func (m ManualUpgradeBatch) FirstUpgrade() *ManualUpgradePlan { // ensure the upgrades are sorted before searching sortUpgrades(m) - for _, upgrade := range m { - if upgrade.Height > height { - return upgrade - } + if len(m) == 0 { + return nil } - return nil + return m[0] } type ManualUpgradePlan struct { From 06817225d08d608ff8c908bf747de39b2083dfb9 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 30 May 2025 15:02:58 -0400 Subject: [PATCH 026/115] WIP on runner --- tools/cosmovisor/internal/checkers/checker.go | 12 ++++++++++++ tools/cosmovisor/manual.go | 2 ++ 2 files changed, 14 insertions(+) diff --git a/tools/cosmovisor/internal/checkers/checker.go b/tools/cosmovisor/internal/checkers/checker.go index 03888f68dd49..4db683f3e408 100644 --- a/tools/cosmovisor/internal/checkers/checker.go +++ b/tools/cosmovisor/internal/checkers/checker.go @@ -3,3 +3,15 @@ package checkers type HeightChecker interface { GetLatestBlockHeight() (uint64, error) } + +type HeightWatcher struct{} + +func (h HeightWatcher) Updated() <-chan uint64 { + // TODO persist to file + panic("not implemented") +} + +func (h HeightWatcher) ReadNow() (uint64, error) { + // TODO attempt to read actual height, read from file as a fallback + panic("not implemented") +} diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 2fa40699345c..abe34fdfb732 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -38,6 +38,7 @@ func (cfg *Config) ParseManualUpgrades(bz []byte) (ManualUpgradeBatch, error) { // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. func AddManualUpgrade(cfg *Config, plan *ManualUpgradePlan, forceOverwrite bool) error { + // TODO only allow plans that are AFTER the last known height manualUpgrades, err := cfg.ReadManualUpgrades() if err != nil { return err @@ -57,6 +58,7 @@ func AddManualUpgrade(cfg *Config, plan *ManualUpgradePlan, forceOverwrite bool) sortUpgrades(manualUpgrades) + // TODO we should not write the file every time we add an upgrade, but only once per command otherwise we can trigger spurious manualUpgradesData, err := json.MarshalIndent(manualUpgrades, "", " ") if err != nil { return err From 3100d729586b6eea9a2791bbd74e9f0c6724ebef Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 30 May 2025 15:17:32 -0400 Subject: [PATCH 027/115] revert --- tests/systemtests/upgrade_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index 88dd93a5d3ff..59d47e271e41 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -82,7 +82,7 @@ func TestChainUpgrade(t *testing.T) { votingPeriod := 5 * time.Second // enough time to vote systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) - systest.Sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight-1)) + systest.Sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) govAddr := sdk.AccAddress(address.Module("gov")).String() From d9ec0798d779d78f956d18b869e711e1ab2c3581 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 2 Jun 2025 10:57:18 -0400 Subject: [PATCH 028/115] WIP on test setup and run implementation --- tools/cosmovisor/TEST_SCENARIOS.md | 17 +++ tools/cosmovisor/args.go | 20 +++- tools/cosmovisor/cmd/cosmovisor/main.go | 5 +- tools/cosmovisor/cmd/cosmovisor/run.go | 41 +++---- tools/cosmovisor/cmd/cosmovisor/run_config.go | 9 +- tools/cosmovisor/cmd/cosmovisor/version.go | 3 +- tools/cosmovisor/cmd/mock_node/main.go | 7 +- tools/cosmovisor/internal/errors.go | 12 +++ tools/cosmovisor/internal/runner.go | 102 +++++++++++++----- tools/cosmovisor/internal/watchers/watcher.go | 15 +++ .../testdata/mockchain/cosmovisor/.gitignore | 1 + .../mockchain/cosmovisor/data/.gitignore | 4 + .../mockchain/cosmovisor/genesis/bin/mockd | 4 + 13 files changed, 186 insertions(+), 54 deletions(-) create mode 100644 tools/cosmovisor/TEST_SCENARIOS.md create mode 100644 tools/cosmovisor/internal/errors.go create mode 100644 tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore create mode 100644 tools/cosmovisor/testdata/mockchain/cosmovisor/data/.gitignore create mode 100755 tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd diff --git a/tools/cosmovisor/TEST_SCENARIOS.md b/tools/cosmovisor/TEST_SCENARIOS.md new file mode 100644 index 000000000000..f8ea397e2e11 --- /dev/null +++ b/tools/cosmovisor/TEST_SCENARIOS.md @@ -0,0 +1,17 @@ +- basic: start node, get upgrade-info.json, upgrade +- manual upgrade added while running: + - start node + - get upgrade-info.json.batch + - restart with halt height + - reach halt height + - upgrade +- manual upgrade added while running but get upgrade-info.json: + - start node + - get upgrade-info.json.batch + - restart with halt height + - get upgrade-info.json + - upgrade + - restart with halt height + - reach halt height + - upgrade +- start with halt height \ No newline at end of file diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index 6da35c8be6e6..df57fd6a0ff6 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -458,8 +458,26 @@ func (cfg *Config) ParseUpgradeInfo(bz []byte) (upgradetypes.Plan, error) { return upgradePlan, nil } +const LastKnownHeightFile = ".last_known_height" + func (cfg Config) ReadLastKnownHeight() uint64 { - return 0 + filename := filepath.Join(cfg.UpgradeInfoDir(), LastKnownHeightFile) + bz, err := os.ReadFile(filename) + if err != nil { + return 0 + } + + h, err := strconv.ParseUint(string(bz), 10, 64) + if err != nil { + return 0 + } + + return h +} + +func (cfg Config) WriteLastKnownHeight(height uint64) error { + filename := filepath.Join(cfg.UpgradeInfoDir(), LastKnownHeightFile) + return os.WriteFile(filename, []byte(strconv.FormatUint(height, 10)), 0644) } // BooleanOption checks and validate env option diff --git a/tools/cosmovisor/cmd/cosmovisor/main.go b/tools/cosmovisor/cmd/cosmovisor/main.go index 294dd66f7165..28de07372de1 100644 --- a/tools/cosmovisor/cmd/cosmovisor/main.go +++ b/tools/cosmovisor/cmd/cosmovisor/main.go @@ -3,10 +3,13 @@ package main import ( "context" "os" + "os/signal" + "syscall" ) func main() { - if err := NewRootCmd().ExecuteContext(context.Background()); err != nil { + ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + if err := NewRootCmd().ExecuteContext(ctx); err != nil { os.Exit(1) } } diff --git a/tools/cosmovisor/cmd/cosmovisor/run.go b/tools/cosmovisor/cmd/cosmovisor/run.go index 4da2ac24d846..e0f2feeb0c5e 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run.go +++ b/tools/cosmovisor/cmd/cosmovisor/run.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" "strings" @@ -8,6 +9,7 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/internal" ) var runCmd = &cobra.Command{ @@ -18,18 +20,18 @@ Provide '--cosmovisor-config' file path in command args or set env variables to `, SilenceUsage: true, DisableFlagParsing: true, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { cfgPath, args, err := parseCosmovisorConfig(args) if err != nil { return fmt.Errorf("failed to parse cosmovisor config: %w", err) } - return run(cfgPath, args) + return run(cmd.Context(), cfgPath, args) }, } // run runs the configured program with the given args and monitors it for upgrades. -func run(cfgPath string, args []string, options ...RunOption) error { +func run(ctx context.Context, cfgPath string, args []string, options ...RunOption) error { cfg, err := cosmovisor.GetConfigFromFile(cfgPath) if err != nil { return err @@ -47,23 +49,22 @@ func run(cfgPath string, args []string, options ...RunOption) error { } logger := cfg.Logger(runCfg.StdOut) - launcher, err := cosmovisor.NewLauncher(logger, cfg) - if err != nil { - return err - } - - doUpgrade, err := launcher.Run(args, runCfg.StdIn, runCfg.StdOut, runCfg.StdErr) - // if RestartAfterUpgrade, we launch after a successful upgrade (given that condition launcher.Run returns nil) - for cfg.RestartAfterUpgrade && err == nil && doUpgrade { - logger.Info("upgrade detected, relaunching", "app", cfg.Name) - doUpgrade, err = launcher.Run(args, runCfg.StdIn, runCfg.StdOut, runCfg.StdErr) - } - - if doUpgrade && err == nil { - logger.Info("upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.") - } - - return err + return internal.Run(ctx, cfg, runCfg, args, logger) + //launcher, err := cosmovisor.NewLauncher(logger, cfg) + //if err != nil { + // return err + //} + // + //doUpgrade, err := launcher.Run(args, runCfg.StdIn, runCfg.StdOut, runCfg.StdErr) + //// if RestartAfterUpgrade, we launch after a successful upgrade (given that condition launcher.Run returns nil) + //for cfg.RestartAfterUpgrade && err == nil && doUpgrade { + // logger.Info("upgrade detected, relaunching", "app", cfg.Name) + // doUpgrade, err = launcher.Run(args, runCfg.StdIn, runCfg.StdOut, runCfg.StdErr) + //} + // + //if doUpgrade && err == nil { + // logger.Info("upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.") + //} } func parseCosmovisorConfig(args []string) (string, []string, error) { diff --git a/tools/cosmovisor/cmd/cosmovisor/run_config.go b/tools/cosmovisor/cmd/cosmovisor/run_config.go index f025b06eb619..5a0982de87cd 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run_config.go +++ b/tools/cosmovisor/cmd/cosmovisor/run_config.go @@ -3,6 +3,8 @@ package main import ( "io" "os" + + "cosmossdk.io/tools/cosmovisor/internal" ) // DefaultRunConfig defintes a default RunConfig that writes to os.Stdout and os.Stderr @@ -12,12 +14,7 @@ var DefaultRunConfig = RunConfig{ StdErr: os.Stderr, } -// RunConfig defines the configuration for running a command -type RunConfig struct { - StdIn io.Reader - StdOut io.Writer - StdErr io.Writer -} +type RunConfig = internal.RunConfig type RunOption func(*RunConfig) diff --git a/tools/cosmovisor/cmd/cosmovisor/version.go b/tools/cosmovisor/cmd/cosmovisor/version.go index a51b376355af..021f8ab4832b 100644 --- a/tools/cosmovisor/cmd/cosmovisor/version.go +++ b/tools/cosmovisor/cmd/cosmovisor/version.go @@ -47,7 +47,7 @@ func printVersion(cmd *cobra.Command, args []string, noAppVersion bool) error { return nil } - if err := run("", append([]string{"version"}, args...)); err != nil { + if err := run(cmd.Context(), "", append([]string{"version"}, args...)); err != nil { return fmt.Errorf("failed to run version command: %w", err) } @@ -62,6 +62,7 @@ func printVersionJSON(cmd *cobra.Command, args []string, noAppVersion bool) erro buf := new(strings.Builder) if err := run( + cmd.Context(), "", []string{"version", "--long", "--output", "json"}, StdOutRunOption(buf), diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 3396219756e6..c3fe60e40dc6 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -145,7 +145,9 @@ func (n *MockNode) Run(ctx context.Context) error { } else { n.logger.Info("Mock node reached upgrade height, writing upgrade-info.json", "upgrade_plan", n.upgradePlan) upgradeInfoPath := path.Join(n.homePath, "data", upgradetypes.UpgradeInfoFilename) - out, err := (&jsonpb.Marshaler{}).MarshalToString(n.upgradePlan) + out, err := (&jsonpb.Marshaler{ + EmitDefaults: false, + }).MarshalToString(n.upgradePlan) if err != nil { return fmt.Errorf("failed to marshal upgrade plan: %w", err) } @@ -158,6 +160,9 @@ func (n *MockNode) Run(ctx context.Context) error { return fmt.Errorf("failed to write upgrade-info.json: %w", err) } } + // Don't exit until we receive a shutdown signal + n.logger.Info("Mock node reached upgrade height, waiting for shutdown signal") + <-ctx.Done() return nil } diff --git a/tools/cosmovisor/internal/errors.go b/tools/cosmovisor/internal/errors.go new file mode 100644 index 000000000000..217b85fed303 --- /dev/null +++ b/tools/cosmovisor/internal/errors.go @@ -0,0 +1,12 @@ +package internal + +import "fmt" + +type ErrUpgradeNeeded struct { +} + +func (e ErrUpgradeNeeded) Error() string { + return fmt.Sprintf("upgrade needed") +} + +var _ error = ErrUpgradeNeeded{} diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 18c1f75f1593..1593db0c18b1 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -3,12 +3,14 @@ package internal import ( "context" "fmt" + "io" + "os/exec" "cosmossdk.io/log" "cosmossdk.io/tools/cosmovisor" - "cosmossdk.io/tools/cosmovisor/internal/checkers" "cosmossdk.io/tools/cosmovisor/internal/watchers" + "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -63,15 +65,21 @@ import ( // } //} -func Run(ctx context.Context, cfg *cosmovisor.Config, logger log.Logger) error { - // TODO start file watchers +func Run(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) error { + return RunOnce(ctx, cfg, runCfg, args, logger) +} + +func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) error { + logger.Info("Checking for upgrade-info.json") if _, err := cfg.UpgradeInfo(); err == nil { - // TODO return err need upgrade + return ErrUpgradeNeeded{} } + logger.Info("Checking for upgrade-info.json.batch") manualUpgradeBatch, err := cfg.ReadManualUpgrades() if err != nil { return err } + logger.Info("Checking last known height") lastKnownHeight := cfg.ReadLastKnownHeight() haltHeight := uint64(0) if manualUpgrade := manualUpgradeBatch.FirstUpgrade(); manualUpgrade != nil { @@ -79,47 +87,93 @@ func Run(ctx context.Context, cfg *cosmovisor.Config, logger log.Logger) error { return fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d") } haltHeight = uint64(manualUpgrade.Height) + logger.Info("Found manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) } // TODO initialize watchers and checkers - var upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] - var manualUpgradesWatcher watchers.Watcher[cosmovisor.ManualUpgradeBatch] - var actualHeightWatcher watchers.Watcher[uint64] - var heightChecker checkers.HeightChecker + + // create directory + dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, cfg.UpgradeInfoDir(), []string{ + cfg.UpgradeInfoFilePath(), + cfg.UpgradeInfoBatchFilePath(), + }) + if err != nil { + logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, cfg.PollInterval, dirWatcher, cfg.UpgradeInfoFilePath(), cfg.ParseUpgradeInfo) + manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, cfg.PollInterval, dirWatcher, cfg.UpgradeInfoBatchFilePath(), cfg.ParseManualUpgrades) + //var actualHeightWatcher watchers.Watcher[uint64] + //var heightChecker checkers.HeightWatcher if haltHeight > 0 { // TODO start height watcher } - // TODO start process runner - var processRunner ProcessRunner + //// TODO start process runner + cmd, err := createCmd(cfg, runCfg, args, logger) + if err != nil { + return err + } + processRunner := RunProcess(cmd) defer func() { - // always check height before shutting down - heightChecker.GetLatestBlockHeight() - processRunner.Shutdown(cfg.ShutdownGrace) + // TODO always check height before shutting down + //_, _ = heightChecker.ReadNow() + _ = processRunner.Shutdown(cfg.ShutdownGrace) }() - correctHeightConfirmed := false + //correctHeightConfirmed := false for { select { case <-upgradePlanWatcher.Updated(): - // TODO shutdown + return ErrUpgradeNeeded{} case <-manualUpgradesWatcher.Updated(): if haltHeight == 0 { // TODO shutdown, no halt height set + return ErrUpgradeNeeded{} } else { // TODO check if this would change the halt height } - case <-processRunner.Done(): + case err := <-processRunner.Done(): // TODO handle process exit - case actualHeight := <-actualHeightWatcher.Updated(): - if !correctHeightConfirmed { - // TODO read manual upgrade batch and check if we'd still be at the correct halt height - correctHeightConfirmed = true - } - if actualHeight >= haltHeight { - // TODO shutdown - } + return err + // TODO: + //case actualHeight := <-actualHeightWatcher.Updated(): + // if !correctHeightConfirmed { + // // TODO read manual upgrade batch and check if we'd still be at the correct halt height + // correctHeightConfirmed = true + // } + // if actualHeight >= haltHeight { + // // TODO shutdown + // } + // TODO error channels } } return nil } + +func createCmd(cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) (*exec.Cmd, error) { + bin, err := cfg.CurrentBin() + if err != nil { + return nil, fmt.Errorf("error creating symlink to genesis: %w", err) + } + + if err := plan.EnsureBinary(bin); err != nil { + return nil, fmt.Errorf("current binary is invalid: %w", err) + } + + logger.Info("running app", "path", bin, "args", args) + cmd := exec.Command(bin, args...) + cmd.Stdin = runCfg.StdIn + cmd.Stdout = runCfg.StdOut + cmd.Stderr = runCfg.StdErr + return cmd, nil +} + +// RunConfig defines the configuration for running a command +type RunConfig struct { + StdIn io.Reader + StdOut io.Writer + StdErr io.Writer +} diff --git a/tools/cosmovisor/internal/watchers/watcher.go b/tools/cosmovisor/internal/watchers/watcher.go index 994048961a22..7b678ee99bde 100644 --- a/tools/cosmovisor/internal/watchers/watcher.go +++ b/tools/cosmovisor/internal/watchers/watcher.go @@ -1,6 +1,21 @@ package watchers +import ( + "context" + "time" +) + type Watcher[T any] interface { Updated() <-chan T Errors() <-chan error } + +func InitWatcher[T any](ctx context.Context, pollInterval time.Duration, dirWatcher *FSNotifyWatcher, filename string, unmarshal func([]byte) (T, error)) Watcher[T] { + if dirWatcher != nil { + hybridWatcher := NewHybridWatcher(ctx, dirWatcher, filename, pollInterval) + return NewDataWatcher[T](ctx, hybridWatcher, unmarshal) + } else { + pollWatcher := NewFilePollWatcher(ctx, filename, pollInterval) + return NewDataWatcher[T](ctx, pollWatcher, unmarshal) + } +} diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore b/tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore new file mode 100644 index 000000000000..1f4ddec6a80e --- /dev/null +++ b/tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore @@ -0,0 +1 @@ +current \ No newline at end of file diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/data/.gitignore b/tools/cosmovisor/testdata/mockchain/cosmovisor/data/.gitignore new file mode 100644 index 000000000000..86d0cb2726c6 --- /dev/null +++ b/tools/cosmovisor/testdata/mockchain/cosmovisor/data/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd b/tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd new file mode 100755 index 000000000000..9517c720c186 --- /dev/null +++ b/tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e + +exec mock_node "$@" --block-time 1s --upgrade-plan '{"name":"gov1","height":10}' \ No newline at end of file From 489b234a86dd897991f08bc43ba0b4cf036ac210 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 2 Jun 2025 11:26:33 -0400 Subject: [PATCH 029/115] WIP on runner and test setup --- tools/cosmovisor/args.go | 1 + tools/cosmovisor/cmd/mock_node/main.go | 26 ++++++++++++------- tools/cosmovisor/internal/runner.go | 11 +++++--- .../internal/watchers/poll_watcher.go | 12 +++++---- .../{cosmovisor => }/data/.gitignore | 0 5 files changed, 32 insertions(+), 18 deletions(-) rename tools/cosmovisor/testdata/mockchain/{cosmovisor => }/data/.gitignore (100%) diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index df57fd6a0ff6..a792b35036cc 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -433,6 +433,7 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) { // UpgradeInfo returns the current upgrade info func (cfg *Config) UpgradeInfo() (upgradetypes.Plan, error) { filename := cfg.UpgradeInfoFilePath() + fmt.Printf("Reading upgrade info from %q\n", filename) _, err := os.Lstat(filename) var bz []byte if err != nil { // no current directory diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index c3fe60e40dc6..c50df53ce03e 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -10,6 +10,7 @@ import ( "os" "os/signal" "path" + "strconv" "syscall" "time" @@ -33,7 +34,6 @@ The --upgrade-plan and --halt-height flags are mutually exclusive. It is an erro Based on which flag is specified the node will either exhibit --halt-height before or x/upgrade upgrade-info.json behavior.`, } - var startHeight uint64 var blockTime time.Duration var upgradePlan string var haltHeight uint64 @@ -41,7 +41,6 @@ x/upgrade upgrade-info.json behavior.`, var httpAddr string var blockUrl string var shutdownDelay time.Duration - cmd.Flags().Uint64Var(&startHeight, "start-height", 1, "Block height at which to start the mock node.") cmd.Flags().DurationVar(&blockTime, "block-time", 0, "Duration of time between blocks. This is required to simulate a progression of blocks over time.") cmd.Flags().StringVar(&upgradePlan, "upgrade-plan", "", "upgrade-info.json to create after the halt duration is reached. Either this flag or --halt-height must be specified but not both.") cmd.Flags().Uint64Var(&haltHeight, server.FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node. E") @@ -67,7 +66,7 @@ x/upgrade upgrade-info.json behavior.`, } } node := &MockNode{ - height: startHeight, + height: 0, blockTime: blockTime, haltHeight: haltHeight, homePath: homePath, @@ -85,13 +84,6 @@ x/upgrade upgrade-info.json behavior.`, if err := node.upgradePlan.ValidateBasic(); err != nil { return fmt.Errorf("invalid upgrade plan: %w", err) } - if node.upgradePlan.Height < int64(startHeight) { - return fmt.Errorf("upgrade plan height %d must be greater than or equal to start height %d", node.upgradePlan.Height, startHeight) - } - } else { - if haltHeight < startHeight { - return fmt.Errorf("halt height %d must be greater than or equal to start height %d", haltHeight, startHeight) - } } return node.Run(cmd.Context()) } @@ -119,6 +111,15 @@ func (n *MockNode) Run(ctx context.Context) error { upgradeHeight = uint64(n.upgradePlan.Height) } + actualHeightFile := path.Join(n.homePath, "data", "actual-height") + // try to read the actual-height file if it exists + if bz, err := os.ReadFile(actualHeightFile); err == nil { + n.height, err = strconv.ParseUint(string(bz), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse actual height from file: %w", err) + } + } + n.logger.Info("Starting mock node", "start_height", n.height, "block_time", n.blockTime, "upgrade_plan", n.upgradePlan, "halt_height", upgradeHeight) srv := n.startHTTPServer() ticker := time.NewTicker(n.blockTime) @@ -138,6 +139,11 @@ func (n *MockNode) Run(ctx context.Context) error { return nil case <-ticker.C: n.height++ + // Write the current height to the actual-height file + err := os.WriteFile(actualHeightFile, []byte(fmt.Sprintf("%d", n.height)), 0o644) + if err != nil { + return fmt.Errorf("failed to write actual height to file: %w", err) + } } } if n.haltHeight > 0 { diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 1593db0c18b1..58e6f9d08599 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -126,9 +126,15 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args //correctHeightConfirmed := false for { select { - case <-upgradePlanWatcher.Updated(): + case _, ok := <-upgradePlanWatcher.Updated(): + if !ok { + return nil + } return ErrUpgradeNeeded{} - case <-manualUpgradesWatcher.Updated(): + case _, ok := <-manualUpgradesWatcher.Updated(): + if !ok { + return nil + } if haltHeight == 0 { // TODO shutdown, no halt height set return ErrUpgradeNeeded{} @@ -150,7 +156,6 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args // TODO error channels } } - return nil } func createCmd(cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) (*exec.Cmd, error) { diff --git a/tools/cosmovisor/internal/watchers/poll_watcher.go b/tools/cosmovisor/internal/watchers/poll_watcher.go index ab38ec47aa29..3c5bf8d50696 100644 --- a/tools/cosmovisor/internal/watchers/poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/poll_watcher.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "reflect" "time" ) @@ -26,16 +27,17 @@ func NewPollWatcher[T any](ctx context.Context, checker func() (T, error), pollI case <-ctx.Done(): return case <-ticker.C: - _, err := checker() + x, err := checker() if err != nil { if !os.IsNotExist(err) { errChan <- fmt.Errorf("failed to check for updates: %w", err) } } else { - panic("TODO: generically check for empty value") - //if x != nil { - // outChan <- x - //} + // to make PollWatcher generic on any type T (including []byte), we use reflect.DeepEqual and the default zero value of T + var zero T + if !reflect.DeepEqual(x, zero) { + outChan <- x + } } } } diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/data/.gitignore b/tools/cosmovisor/testdata/mockchain/data/.gitignore similarity index 100% rename from tools/cosmovisor/testdata/mockchain/cosmovisor/data/.gitignore rename to tools/cosmovisor/testdata/mockchain/data/.gitignore From be564e632f83e8f8e66febbddb33c0b53704805c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 2 Jun 2025 12:42:25 -0400 Subject: [PATCH 030/115] testing manual upgrades --- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 3 +- tools/cosmovisor/cmd/mock_node/main.go | 20 +++++----- tools/cosmovisor/internal/checkers/checker.go | 17 --------- tools/cosmovisor/internal/checkers/sniff.go | 1 - tools/cosmovisor/internal/runner.go | 27 +++++++------ .../internal/watchers/height_watcher.go | 33 ++++++++++++++-- .../{checkers => watchers}/http_block.go | 2 +- .../internal/watchers/log_watcher.go | 4 +- tools/cosmovisor/internal/watchers/sniff.go | 1 + tools/cosmovisor/manual.go | 38 ++++++++++--------- tools/cosmovisor/process.go | 5 +-- 11 files changed, 81 insertions(+), 70 deletions(-) delete mode 100644 tools/cosmovisor/internal/checkers/checker.go delete mode 100644 tools/cosmovisor/internal/checkers/sniff.go rename tools/cosmovisor/internal/{checkers => watchers}/http_block.go (98%) create mode 100644 tools/cosmovisor/internal/watchers/sniff.go diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 6d3c599ee222..62fe8d6aa667 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) func NewAddUpgradeCmd() *cobra.Command { @@ -63,7 +64,7 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) if upgradeHeight > 0 { - plan := &cosmovisor.ManualUpgradePlan{ + plan := &upgradetypes.Plan{ Name: upgradeName, Height: upgradeHeight, } diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index c50df53ce03e..10e45ef9eddb 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -18,7 +18,7 @@ import ( "github.com/cosmos/gogoproto/jsonpb" "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor/internal/checkers" + "cosmossdk.io/tools/cosmovisor/internal/watchers" "github.com/cosmos/cosmos-sdk/server" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -49,9 +49,6 @@ x/upgrade upgrade-info.json behavior.`, cmd.Flags().StringVar(&blockUrl, "block-url", "/block", "URL at which the latest block information is served. Defaults to /block.") cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", 0, "Duration to wait before shutting down the node upon receiving a shutdown signal. Defaults to 0 (no delay).") cmd.RunE = func(cmd *cobra.Command, args []string) error { - if upgradePlan != "" && haltHeight > 0 { - return fmt.Errorf("cannot specify both --upgrade-plan and --halt-height") - } if upgradePlan == "" && haltHeight == 0 { return fmt.Errorf("must specify either --upgrade-plan or --halt-height") } @@ -107,8 +104,11 @@ type MockNode struct { func (n *MockNode) Run(ctx context.Context) error { ctx, _ = signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) upgradeHeight := n.haltHeight - if upgradeHeight == 0 { - upgradeHeight = uint64(n.upgradePlan.Height) + if n.upgradePlan != nil { + upgradePlanHeight := uint64(n.upgradePlan.Height) + if upgradePlanHeight < upgradeHeight { + upgradeHeight = upgradePlanHeight + } } actualHeightFile := path.Join(n.homePath, "data", "actual-height") @@ -175,10 +175,10 @@ func (n *MockNode) Run(ctx context.Context) error { func (n *MockNode) startHTTPServer() *http.Server { http.HandleFunc(n.blockUrl, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(checkers.Response{ - Result: checkers.Result{ - Block: checkers.Block{ - Header: checkers.Header{ + err := json.NewEncoder(w).Encode(watchers.Response{ + Result: watchers.Result{ + Block: watchers.Block{ + Header: watchers.Header{ Height: fmt.Sprintf("%d", n.height), }, }, diff --git a/tools/cosmovisor/internal/checkers/checker.go b/tools/cosmovisor/internal/checkers/checker.go deleted file mode 100644 index 4db683f3e408..000000000000 --- a/tools/cosmovisor/internal/checkers/checker.go +++ /dev/null @@ -1,17 +0,0 @@ -package checkers - -type HeightChecker interface { - GetLatestBlockHeight() (uint64, error) -} - -type HeightWatcher struct{} - -func (h HeightWatcher) Updated() <-chan uint64 { - // TODO persist to file - panic("not implemented") -} - -func (h HeightWatcher) ReadNow() (uint64, error) { - // TODO attempt to read actual height, read from file as a fallback - panic("not implemented") -} diff --git a/tools/cosmovisor/internal/checkers/sniff.go b/tools/cosmovisor/internal/checkers/sniff.go deleted file mode 100644 index d0ae62bb397a..000000000000 --- a/tools/cosmovisor/internal/checkers/sniff.go +++ /dev/null @@ -1 +0,0 @@ -package checkers diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 58e6f9d08599..60c1a83e5ca2 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -105,11 +105,14 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args defer cancel() upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, cfg.PollInterval, dirWatcher, cfg.UpgradeInfoFilePath(), cfg.ParseUpgradeInfo) manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, cfg.PollInterval, dirWatcher, cfg.UpgradeInfoBatchFilePath(), cfg.ParseManualUpgrades) - //var actualHeightWatcher watchers.Watcher[uint64] - //var heightChecker checkers.HeightWatcher + heightChecker := watchers.NewHTTPRPCBLockChecker("http://localhost:8080/block") + heightWatcher := watchers.NewHeightWatcher(ctx, heightChecker, cfg.PollInterval, func(height uint64) error { + return cfg.WriteLastKnownHeight(height) + }) if haltHeight > 0 { // TODO start height watcher + args = append(args, fmt.Sprintf("--halt-height=%d", haltHeight)) } //// TODO start process runner cmd, err := createCmd(cfg, runCfg, args, logger) @@ -123,7 +126,7 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args _ = processRunner.Shutdown(cfg.ShutdownGrace) }() - //correctHeightConfirmed := false + correctHeightConfirmed := false for { select { case _, ok := <-upgradePlanWatcher.Updated(): @@ -144,15 +147,15 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args case err := <-processRunner.Done(): // TODO handle process exit return err - // TODO: - //case actualHeight := <-actualHeightWatcher.Updated(): - // if !correctHeightConfirmed { - // // TODO read manual upgrade batch and check if we'd still be at the correct halt height - // correctHeightConfirmed = true - // } - // if actualHeight >= haltHeight { - // // TODO shutdown - // } + // TODO: + case actualHeight := <-heightWatcher.Updated(): + if !correctHeightConfirmed { + // TODO read manual upgrade batch and check if we'd still be at the correct halt height + correctHeightConfirmed = true + } + if actualHeight >= haltHeight { + return ErrUpgradeNeeded{} + } // TODO error channels } } diff --git a/tools/cosmovisor/internal/watchers/height_watcher.go b/tools/cosmovisor/internal/watchers/height_watcher.go index a96abfffbd1b..92c7b3494d29 100644 --- a/tools/cosmovisor/internal/watchers/height_watcher.go +++ b/tools/cosmovisor/internal/watchers/height_watcher.go @@ -3,10 +3,35 @@ package watchers import ( "context" "time" - - "cosmossdk.io/tools/cosmovisor/internal/checkers" ) -func NewHeightWatcher(ctx context.Context, checker checkers.HeightChecker, pollInterval time.Duration) Watcher[uint64] { - return NewPollWatcher[uint64](ctx, checker.GetLatestBlockHeight, pollInterval) +type HeightChecker interface { + GetLatestBlockHeight() (uint64, error) +} + +type HeightWatcher struct { + *PollWatcher[uint64] + checker HeightChecker + onGetHeight func(uint64) error +} + +func NewHeightWatcher(ctx context.Context, checker HeightChecker, pollInterval time.Duration, onGetHeight func(uint64) error) *HeightWatcher { + watcher := &HeightWatcher{ + checker: checker, + onGetHeight: onGetHeight, + } + watcher.PollWatcher = NewPollWatcher[uint64](ctx, func() (uint64, error) { + return watcher.ReadNow() + + }, pollInterval) + return watcher +} + +func (h HeightWatcher) ReadNow() (uint64, error) { + height, err := h.checker.GetLatestBlockHeight() + if err != nil { + return 0, err + } + err = h.onGetHeight(height) + return height, err } diff --git a/tools/cosmovisor/internal/checkers/http_block.go b/tools/cosmovisor/internal/watchers/http_block.go similarity index 98% rename from tools/cosmovisor/internal/checkers/http_block.go rename to tools/cosmovisor/internal/watchers/http_block.go index 97ffc73bf6c0..8bf187e6f953 100644 --- a/tools/cosmovisor/internal/checkers/http_block.go +++ b/tools/cosmovisor/internal/watchers/http_block.go @@ -1,4 +1,4 @@ -package checkers +package watchers import ( "encoding/json" diff --git a/tools/cosmovisor/internal/watchers/log_watcher.go b/tools/cosmovisor/internal/watchers/log_watcher.go index 8e1c84b9e021..782b78a776cd 100644 --- a/tools/cosmovisor/internal/watchers/log_watcher.go +++ b/tools/cosmovisor/internal/watchers/log_watcher.go @@ -7,11 +7,9 @@ import ( "io" "os" "regexp" - - "cosmossdk.io/tools/cosmovisor/internal/checkers" ) -func NewHaltHeightLogWatcher(ctx context.Context, log io.Reader, checker checkers.HeightChecker) Watcher[uint64] { +func NewHaltHeightLogWatcher(ctx context.Context, log io.Reader, checker HeightChecker) Watcher[uint64] { check := func(line string) (uint64, error) { height, err := parseHaltHeightLogMessage(line) if err != nil { diff --git a/tools/cosmovisor/internal/watchers/sniff.go b/tools/cosmovisor/internal/watchers/sniff.go new file mode 100644 index 000000000000..38643bc0295e --- /dev/null +++ b/tools/cosmovisor/internal/watchers/sniff.go @@ -0,0 +1 @@ +package watchers diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index abe34fdfb732..6663e165a684 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "sort" + + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) // ReadManualUpgrades reads the manual upgrade data. @@ -37,7 +39,7 @@ func (cfg *Config) ParseManualUpgrades(bz []byte) (ManualUpgradeBatch, error) { // AddManualUpgrade adds a manual upgrade plan. // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. -func AddManualUpgrade(cfg *Config, plan *ManualUpgradePlan, forceOverwrite bool) error { +func AddManualUpgrade(cfg *Config, plan *upgradetypes.Plan, forceOverwrite bool) error { // TODO only allow plans that are AFTER the last known height manualUpgrades, err := cfg.ReadManualUpgrades() if err != nil { @@ -73,7 +75,7 @@ func sortUpgrades(upgrades ManualUpgradeBatch) { }) } -type ManualUpgradeBatch []*ManualUpgradePlan +type ManualUpgradeBatch []*upgradetypes.Plan func (m ManualUpgradeBatch) ValidateBasic() error { for _, upgrade := range m { @@ -84,7 +86,7 @@ func (m ManualUpgradeBatch) ValidateBasic() error { return nil } -func (m ManualUpgradeBatch) FirstUpgrade() *ManualUpgradePlan { +func (m ManualUpgradeBatch) FirstUpgrade() *upgradetypes.Plan { // ensure the upgrades are sorted before searching sortUpgrades(m) if len(m) == 0 { @@ -93,18 +95,18 @@ func (m ManualUpgradeBatch) FirstUpgrade() *ManualUpgradePlan { return m[0] } -type ManualUpgradePlan struct { - Name string `json:"name"` - Height int64 `json:"height"` - Info string `json:"info"` -} - -func (m ManualUpgradePlan) ValidateBasic() error { - if m.Name == "" { - return fmt.Errorf("name cannot be empty") - } - if m.Height <= 0 { - return fmt.Errorf("height must be greater than 0") - } - return nil -} +//type ManualUpgradePlan struct { +// Name string `json:"name"` +// Height int64 `json:"height"` +// Info string `json:"info"` +//} +// +//func (m ManualUpgradePlan) ValidateBasic() error { +// if m.Name == "" { +// return fmt.Errorf("name cannot be empty") +// } +// if m.Height <= 0 { +// return fmt.Errorf("height must be greater than 0") +// } +// return nil +//} diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index a7feb758b544..63f052a5315e 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -17,7 +17,6 @@ import ( "cosmossdk.io/log" "github.com/otiai10/copy" - "cosmossdk.io/tools/cosmovisor/internal/checkers" "cosmossdk.io/tools/cosmovisor/internal/watchers" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" @@ -58,9 +57,9 @@ type Launcher struct { manualUpgradesWatcher watchers.Watcher[ManualUpgradeBatch] haltHeightWatcher watchers.Watcher[uint64] actualHeightWatcher watchers.Watcher[uint64] - heightChecker checkers.HeightChecker + heightChecker watchers.HeightChecker upgradePlan *upgradetypes.Plan - manualUpgrade *ManualUpgradePlan + manualUpgrade *upgradetypes.Plan } func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { From cc3082fe0fe3baf9430b359afd4f591cc0b8235d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 2 Jun 2025 13:50:55 -0400 Subject: [PATCH 031/115] WIP on upgrade flow --- tools/cosmovisor/args.go | 5 + tools/cosmovisor/internal/errors.go | 5 +- tools/cosmovisor/internal/runner.go | 47 +++--- tools/cosmovisor/internal/upgrade.go | 216 +++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 25 deletions(-) create mode 100644 tools/cosmovisor/internal/upgrade.go diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index a792b35036cc..e0e27658ff2a 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -653,6 +653,11 @@ func (cfg Config) Export() (string, error) { return path, nil } +func (cfg *Config) DeleteManualUpgradeAtHeight(height uint64) error { + // TODO + return nil +} + func askForConfirmation(str string) bool { var response string fmt.Printf("%s [y/n]: ", str) diff --git a/tools/cosmovisor/internal/errors.go b/tools/cosmovisor/internal/errors.go index 217b85fed303..04eec3cb29fb 100644 --- a/tools/cosmovisor/internal/errors.go +++ b/tools/cosmovisor/internal/errors.go @@ -1,8 +1,11 @@ package internal -import "fmt" +import ( + "fmt" +) type ErrUpgradeNeeded struct { + KnownHeight uint64 } func (e ErrUpgradeNeeded) Error() string { diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 60c1a83e5ca2..4dcfeb373704 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -2,6 +2,7 @@ package internal import ( "context" + "errors" "fmt" "io" "os/exec" @@ -66,33 +67,30 @@ import ( //} func Run(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) error { - return RunOnce(ctx, cfg, runCfg, args, logger) -} - -func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) error { - logger.Info("Checking for upgrade-info.json") - if _, err := cfg.UpgradeInfo(); err == nil { - return ErrUpgradeNeeded{} - } - logger.Info("Checking for upgrade-info.json.batch") - manualUpgradeBatch, err := cfg.ReadManualUpgrades() - if err != nil { - return err - } - logger.Info("Checking last known height") - lastKnownHeight := cfg.ReadLastKnownHeight() - haltHeight := uint64(0) - if manualUpgrade := manualUpgradeBatch.FirstUpgrade(); manualUpgrade != nil { - if lastKnownHeight > uint64(manualUpgrade.Height) { - return fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d") + knownHeight := uint64(0) + // TODO handle cases where daemon shuts down without an upgrade, either have a retry count of fail in that case + for { + upgraded, haltHeight, err := UpgradeIfNeeded(cfg, logger, knownHeight) + if upgraded { + logger.Info("Upgrade completed, restarting process") + if !cfg.RestartAfterUpgrade { + logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") + } + } + err = RunOnce(ctx, cfg, runCfg, args, haltHeight, logger) + if err != nil { + var upgradeNeeded ErrUpgradeNeeded + if ok := errors.As(err, &upgradeNeeded); ok { + logger.Info("Upgrade needed") + knownHeight = upgradeNeeded.KnownHeight + } else { + return err + } } - haltHeight = uint64(manualUpgrade.Height) - logger.Info("Found manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) } +} - // TODO initialize watchers and checkers - - // create directory +func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, haltHeight uint64, logger log.Logger) error { dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, cfg.UpgradeInfoDir(), []string{ cfg.UpgradeInfoFilePath(), cfg.UpgradeInfoBatchFilePath(), @@ -130,6 +128,7 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args for { select { case _, ok := <-upgradePlanWatcher.Updated(): + // TODO check skip upgrade heights?? (although not sure why we need this as the node should not emit an upgrade plan if skip heights is enabled) if !ok { return nil } diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrade.go new file mode 100644 index 000000000000..7aaaa99b09dd --- /dev/null +++ b/tools/cosmovisor/internal/upgrade.go @@ -0,0 +1,216 @@ +package internal + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "cosmossdk.io/log" + "github.com/otiai10/copy" + + "cosmossdk.io/tools/cosmovisor" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +type UpgradeCheckResult struct { + Upgraded bool + HaltHeight uint64 +} + +func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint64) (upgraded bool, haltHeight uint64, err error) { + logger.Info("Checking for upgrade-info.json") + if upgradePlan, err := cfg.UpgradeInfo(); err == nil { + upgrader := NewUpgrader(cfg, logger, upgradePlan) + err := upgrader.DoUpgrade() + if err != nil { + return false, 0, err + } + return true, 0, nil + } + logger.Info("Checking for upgrade-info.json.batch") + manualUpgradeBatch, err := cfg.ReadManualUpgrades() + if err != nil { + return false, 0, err + } + logger.Info("Checking last known height") + lastKnownHeight := knownHeight + if lastKnownHeight == 0 { + lastKnownHeight = cfg.ReadLastKnownHeight() + } + if manualUpgrade := manualUpgradeBatch.FirstUpgrade(); manualUpgrade != nil { + haltHeight = uint64(manualUpgrade.Height) + if lastKnownHeight == haltHeight { + logger.Info("At manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) + upgrader := NewUpgrader(cfg, logger, *manualUpgrade) + err := upgrader.DoUpgrade() + if err != nil { + return false, 0, err + } + err = cfg.DeleteManualUpgradeAtHeight(haltHeight) + return true, haltHeight, err + } else if lastKnownHeight > haltHeight { + return false, haltHeight, fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d") + } + logger.Info("Found pending manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) + return false, haltHeight, nil + } + return false, 0, nil +} + +type Upgrader struct { + cfg *cosmovisor.Config + logger log.Logger + upgradePlan upgradetypes.Plan +} + +func NewUpgrader(cfg *cosmovisor.Config, logger log.Logger, upgradePlan upgradetypes.Plan) *Upgrader { + return &Upgrader{ + cfg: cfg, + logger: logger, + upgradePlan: upgradePlan, + } +} + +func (u *Upgrader) DoUpgrade() error { + u.cfg.WaitRestartDelay() + + if err := u.doBackup(); err != nil { + return err + } + + if err := u.doCustomPreUpgrade(); err != nil { + return err + } + + if err := cosmovisor.UpgradeBinary(u.logger, u.cfg, u.upgradePlan); err != nil { + return err + } + + if err := u.doPreUpgrade(); err != nil { + return err + } + + return nil +} + +// doCustomPreUpgrade executes the custom preupgrade script if provided. +func (u *Upgrader) doCustomPreUpgrade() error { + if u.cfg.CustomPreUpgrade == "" { + return nil + } + + // check if preupgradeFile is executable file + preupgradeFile := filepath.Join(u.cfg.Home, "cosmovisor", u.cfg.CustomPreUpgrade) + u.logger.Info("looking for COSMOVISOR_CUSTOM_PREUPGRADE file", "file", preupgradeFile) + info, err := os.Stat(preupgradeFile) + if err != nil { + u.logger.Error("COSMOVISOR_CUSTOM_PREUPGRADE file missing", "file", preupgradeFile) + return err + } + if !info.Mode().IsRegular() { + _, f := filepath.Split(preupgradeFile) + return fmt.Errorf("COSMOVISOR_CUSTOM_PREUPGRADE: %s is not a regular file", f) + } + + // Set the execute bit for only the current user + // Given: Current user - Group - Everyone + // 0o RWX - RWX - RWX + oldMode := info.Mode().Perm() + newMode := oldMode | 0o100 + if oldMode != newMode { + if err := os.Chmod(preupgradeFile, newMode); err != nil { + u.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") + return errors.New("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") + } + } + + // Run preupgradeFile + cmd := exec.Command(preupgradeFile, u.upgradePlan.Name, fmt.Sprintf("%d", u.upgradePlan.Height)) + cmd.Dir = u.cfg.Home + result, err := cmd.Output() + if err != nil { + return err + } + + u.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE result", "command", preupgradeFile, "argv1", u.upgradePlan.Name, "argv2", fmt.Sprintf("%d", u.upgradePlan.Height), "result", result) + + return nil +} + +// doPreUpgrade runs the pre-upgrade command defined by the application and handles respective error codes. +// cfg contains the cosmovisor config from env var. +// doPreUpgrade runs the new APP binary in order to process the upgrade (post-upgrade for cosmovisor). +func (u *Upgrader) doPreUpgrade() error { + counter := 0 + for { + if counter > u.cfg.PreUpgradeMaxRetries { + return fmt.Errorf("pre-upgrade command failed. reached max attempt of retries - %d", u.cfg.PreUpgradeMaxRetries) + } + + if err := u.executePreUpgradeCmd(); err != nil { + counter++ + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + switch exitErr.ExitCode() { + case 1: + u.logger.Info("pre-upgrade command does not exist. continuing the upgrade.") + return nil + case 30: + return fmt.Errorf("pre-upgrade command failed : %w", err) + case 31: + u.logger.Error("pre-upgrade command failed. retrying", "error", err, "attempt", counter) + continue + } + } + } + + u.logger.Info("pre-upgrade successful. continuing the upgrade.") + return nil + } +} + +// executePreUpgradeCmd runs the pre-upgrade command defined by the application +// cfg contains the cosmovisor config from the env vars +func (u *Upgrader) executePreUpgradeCmd() error { + bin, err := u.cfg.CurrentBin() + if err != nil { + return fmt.Errorf("error while getting current binary path: %w", err) + } + + result, err := exec.Command(bin, "pre-upgrade").Output() + if err != nil { + return err + } + + u.logger.Info("pre-upgrade result", "result", result) + return nil +} + +func (u *Upgrader) doBackup() error { + // take backup if `UNSAFE_SKIP_BACKUP` is not set. + if u.cfg.UnsafeSkipBackup { + return nil + } + + // a destination directory, Format YYYY-MM-DD + st := time.Now() + ymd := fmt.Sprintf("%d-%d-%d", st.Year(), st.Month(), st.Day()) + dst := filepath.Join(u.cfg.DataBackupPath, fmt.Sprintf("data"+"-backup-%s", ymd)) + + u.logger.Info("starting to take backup of data directory", "backup start time", st) + + // copy the $DAEMON_HOME/data to a backup dir + if err := copy.Copy(filepath.Join(u.cfg.Home, "data"), dst); err != nil { + return fmt.Errorf("error while taking data backup: %w", err) + } + + // backup is done, lets check endtime to calculate total time taken for backup process + et := time.Now() + u.logger.Info("backup completed", "backup saved at", dst, "backup completion time", et, "time taken to complete backup", et.Sub(st)) + + return nil +} From b7c813fb0c1da7e778e439fad6c0b443d85010bd Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 2 Jun 2025 13:58:02 -0400 Subject: [PATCH 032/115] WIP on upgrade restart flow --- tools/cosmovisor/cmd/cosmovisor/main.go | 13 +++++++++++++ .../testdata/mockchain/cosmovisor/manual1/bin/mockd | 4 ++++ 2 files changed, 17 insertions(+) create mode 100644 tools/cosmovisor/testdata/mockchain/cosmovisor/manual1/bin/mockd diff --git a/tools/cosmovisor/cmd/cosmovisor/main.go b/tools/cosmovisor/cmd/cosmovisor/main.go index 28de07372de1..7fcb8ecb552c 100644 --- a/tools/cosmovisor/cmd/cosmovisor/main.go +++ b/tools/cosmovisor/cmd/cosmovisor/main.go @@ -2,13 +2,26 @@ package main import ( "context" + "fmt" "os" "os/signal" "syscall" + "time" ) func main() { ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + shutdownChan := make(chan os.Signal, 1) + signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM) + // ensure we shutdown if the process is killed and context cancellation doesn't cause an exit + go func() { + <-shutdownChan + fmt.Println("Received shutdown signal, exiting gracefully...") + // TODO configure this timeout + time.Sleep(10 * time.Second) + fmt.Println("Forcing process shutdown") + os.Exit(0) + }() if err := NewRootCmd().ExecuteContext(ctx); err != nil { os.Exit(1) } diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/manual1/bin/mockd b/tools/cosmovisor/testdata/mockchain/cosmovisor/manual1/bin/mockd new file mode 100644 index 000000000000..644fc6a2cba8 --- /dev/null +++ b/tools/cosmovisor/testdata/mockchain/cosmovisor/manual1/bin/mockd @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e + +exec mock_node "$@" --block-time 1s --upgrade-plan '{"name":"gov1","height":10}' From 8e83e458c4a19079d2fef6551b859831ec9d1ad2 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 2 Jun 2025 14:09:29 -0400 Subject: [PATCH 033/115] WIP on upgrade restart flow --- tools/cosmovisor/internal/runner.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 4dcfeb373704..f4f860b1a362 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -69,6 +69,7 @@ import ( func Run(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) error { knownHeight := uint64(0) // TODO handle cases where daemon shuts down without an upgrade, either have a retry count of fail in that case + // ideally backoff retry for { upgraded, haltHeight, err := UpgradeIfNeeded(cfg, logger, knownHeight) if upgraded { From ac2e934cf73ae6496783ed50c652a1cf0be0ea92 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 2 Jun 2025 17:24:23 -0400 Subject: [PATCH 034/115] WIP on upgrade restart flow --- tools/cosmovisor/cmd/mock_node/main.go | 4 +- tools/cosmovisor/go.mod | 1 + tools/cosmovisor/go.sum | 2 + tools/cosmovisor/internal/runner.go | 64 ++++--------------- tools/cosmovisor/internal/upgrade.go | 6 +- .../{ => upgrades}/manual1/bin/mockd | 0 tools/cosmovisor/upgrade.go | 2 + 7 files changed, 23 insertions(+), 56 deletions(-) rename tools/cosmovisor/testdata/mockchain/cosmovisor/{ => upgrades}/manual1/bin/mockd (100%) mode change 100644 => 100755 diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 10e45ef9eddb..662b30da5965 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -48,6 +48,8 @@ x/upgrade upgrade-info.json behavior.`, cmd.Flags().StringVar(&httpAddr, "http-addr", ":8080", "HTTP server address to serve block information. Defaults to :8080.") cmd.Flags().StringVar(&blockUrl, "block-url", "/block", "URL at which the latest block information is served. Defaults to /block.") cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", 0, "Duration to wait before shutting down the node upon receiving a shutdown signal. Defaults to 0 (no delay).") + // TODO add flag to use either jsonpb or encoding/json + // TODO shutdown at upgrade height cmd.RunE = func(cmd *cobra.Command, args []string) error { if upgradePlan == "" && haltHeight == 0 { return fmt.Errorf("must specify either --upgrade-plan or --halt-height") @@ -148,7 +150,7 @@ func (n *MockNode) Run(ctx context.Context) error { } if n.haltHeight > 0 { n.logger.Error(fmt.Sprintf("halt per configuration height %d", n.height)) - } else { + } else if n.upgradePlan != nil { n.logger.Info("Mock node reached upgrade height, writing upgrade-info.json", "upgrade_plan", n.upgradePlan) upgradeInfoPath := path.Join(n.homePath, "data", upgradetypes.UpgradeInfoFilename) out, err := (&jsonpb.Marshaler{ diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index e0c66450025f..eaaa1c509d98 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -4,6 +4,7 @@ go 1.23.5 require ( cosmossdk.io/log v1.6.0 + github.com/cenkalti/backoff/v5 v5.0.2 github.com/cosmos/cosmos-sdk v0.53.0 github.com/cosmos/gogoproto v1.7.0 github.com/fsnotify/fsnotify v1.9.0 diff --git a/tools/cosmovisor/go.sum b/tools/cosmovisor/go.sum index 4121847ac67a..f9baef07528c 100644 --- a/tools/cosmovisor/go.sum +++ b/tools/cosmovisor/go.sum @@ -723,6 +723,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index f4f860b1a362..d7845b65c938 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -15,68 +15,26 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -//type BasicRunner struct { -// logger log.Logger -// cfg *cosmovisor.Config -// // upgradePlanWatcher watches for data in an upgrade-info.json created by the running node -// upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] -// // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator -// manualUpgradesWatcher watchers.Watcher[cosmovisor.ManualUpgradeBatch] -// actualHeightWatcher watchers.Watcher[uint64] -// heightChecker checkers.HeightChecker -// runner ProcessRunner -//} -// -//func (r *BasicRunner) ComputePlan() error { -// // TODO check for upgrade-info.json -// if _, err := r.cfg.UpgradeInfo(); err == nil { -// -// } -// // TODO check for upgrade-info.json.batch -// return nil -//} -// -//func (r *BasicRunner) DoUpgrade(plan upgradetypes.Plan) error { -// return nil -//} -// -//func (r *BasicRunner) Run(haltHeight uint64) error { -// correctHeightConfirmed := false -// for { -// select { -// case <-r.upgradePlanWatcher.Updated(): -// // TODO shutdown -// case <-r.manualUpgradesWatcher.Updated(): -// if haltHeight == 0 { -// // TODO shutdown, no halt height set -// } else { -// // TODO check if this would change the halt height -// } -// case <-r.runner.Done(): -// // TODO handle process exit -// case actualHeight := <-r.actualHeightWatcher.Updated(): -// if !correctHeightConfirmed { -// // TODO read manual upgrade batch and check if we'd still be at the correct halt height -// correctHeightConfirmed = true -// } -// if actualHeight >= haltHeight { -// // TODO shutdown -// } -// } -// } -//} - func Run(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) error { knownHeight := uint64(0) - // TODO handle cases where daemon shuts down without an upgrade, either have a retry count of fail in that case - // ideally backoff retry + // TODO handle cases where daemon shuts down without an upgrade, either have a retry count of fail in that case ideally backoff retry + startsWithoutUpgrade := 0 for { upgraded, haltHeight, err := UpgradeIfNeeded(cfg, logger, knownHeight) + if err != nil { + return err + } if upgraded { logger.Info("Upgrade completed, restarting process") if !cfg.RestartAfterUpgrade { logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") } + startsWithoutUpgrade = 0 + } else { + if startsWithoutUpgrade >= 5 { + return fmt.Errorf("process restarted %d times without an upgrade, exiting", startsWithoutUpgrade) + } + startsWithoutUpgrade++ } err = RunOnce(ctx, cfg, runCfg, args, haltHeight, logger) if err != nil { diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrade.go index 7aaaa99b09dd..18a6ba127544 100644 --- a/tools/cosmovisor/internal/upgrade.go +++ b/tools/cosmovisor/internal/upgrade.go @@ -102,6 +102,8 @@ func (u *Upgrader) doCustomPreUpgrade() error { return nil } + u.logger.Info("Running custom pre-upgrade script", "script", u.cfg.CustomPreUpgrade) + // check if preupgradeFile is executable file preupgradeFile := filepath.Join(u.cfg.Home, "cosmovisor", u.cfg.CustomPreUpgrade) u.logger.Info("looking for COSMOVISOR_CUSTOM_PREUPGRADE file", "file", preupgradeFile) @@ -201,7 +203,7 @@ func (u *Upgrader) doBackup() error { ymd := fmt.Sprintf("%d-%d-%d", st.Year(), st.Month(), st.Day()) dst := filepath.Join(u.cfg.DataBackupPath, fmt.Sprintf("data"+"-backup-%s", ymd)) - u.logger.Info("starting to take backup of data directory", "backup start time", st) + u.logger.Info("Taking backup of data directory", "backup_path", dst) // copy the $DAEMON_HOME/data to a backup dir if err := copy.Copy(filepath.Join(u.cfg.Home, "data"), dst); err != nil { @@ -210,7 +212,7 @@ func (u *Upgrader) doBackup() error { // backup is done, lets check endtime to calculate total time taken for backup process et := time.Now() - u.logger.Info("backup completed", "backup saved at", dst, "backup completion time", et, "time taken to complete backup", et.Sub(st)) + u.logger.Info("Backup completed", "backup_path", dst, "completion_time", et, "duration", et.Sub(st)) return nil } diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/manual1/bin/mockd b/tools/cosmovisor/testdata/mockchain/cosmovisor/upgrades/manual1/bin/mockd old mode 100644 new mode 100755 similarity index 100% rename from tools/cosmovisor/testdata/mockchain/cosmovisor/manual1/bin/mockd rename to tools/cosmovisor/testdata/mockchain/cosmovisor/upgrades/manual1/bin/mockd diff --git a/tools/cosmovisor/upgrade.go b/tools/cosmovisor/upgrade.go index 8fb25b3446a1..e9953d8d5108 100644 --- a/tools/cosmovisor/upgrade.go +++ b/tools/cosmovisor/upgrade.go @@ -16,9 +16,11 @@ import ( // We can now make any changes to the underlying directory without interference and leave it // in the upgraded state so that the app can restart with the new binary. func UpgradeBinary(logger log.Logger, cfg *Config, p upgradetypes.Plan) error { + logger.Info("Upgrading binary", "name", p.Name) // simplest case is to switch the link err := plan.EnsureBinary(cfg.UpgradeBin(p.Name)) if err == nil { + logger.Info("Upgrade binary already present, setting as current", "name", p.Name) // we have the binary - do it return cfg.SetCurrentUpgrade(p) } From a53bc4d9eeb31dbef48dddb8d9278948e6c45976 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 3 Jun 2025 13:01:40 -0400 Subject: [PATCH 035/115] WIP on testing setup --- .../cmd/cosmovisor/mockchain_test.go | 70 ++++++ tools/cosmovisor/cmd/cosmovisor/run.go | 3 +- tools/cosmovisor/internal/errors.go | 4 +- tools/cosmovisor/internal/runner.go | 69 +++--- tools/cosmovisor/internal/runner.mock.go | 174 --------------- tools/cosmovisor/internal/state_machine.dot | 28 --- tools/cosmovisor/internal/state_machine.go | 207 ------------------ tools/cosmovisor/internal/state_machine.png | Bin 192656 -> 0 bytes .../cosmovisor/internal/state_machine_test.go | 54 ----- 9 files changed, 116 insertions(+), 493 deletions(-) create mode 100644 tools/cosmovisor/cmd/cosmovisor/mockchain_test.go delete mode 100644 tools/cosmovisor/internal/runner.mock.go delete mode 100644 tools/cosmovisor/internal/state_machine.dot delete mode 100644 tools/cosmovisor/internal/state_machine.go delete mode 100644 tools/cosmovisor/internal/state_machine.png delete mode 100644 tools/cosmovisor/internal/state_machine_test.go diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go new file mode 100644 index 000000000000..8debbca69e5a --- /dev/null +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +type MockChainSetup struct { + Genesis string + Upgrades map[string]string +} + +func mockNodeWrapper(args string) string { + return fmt.Sprintf(` +#!/usr/bin/env bash +set -e + +exec mock_node %s "$@" +`, args) +} + +func (m MockChainSetup) Setup(t *testing.T) { + dir, err := os.MkdirTemp("", "mockchain") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, os.RemoveAll(dir)) + }) + // create data directory + require.NoError(t, os.MkdirAll(filepath.Join(dir, "data"), 0o755)) + cosmovisorDir := filepath.Join(dir, "cosmovisor") + // create genesis wrapper + genDir := filepath.Join(cosmovisorDir, "genesis", "bin") + require.NoError(t, os.MkdirAll(genDir, 0o755)) + require.NoError(t, + os.WriteFile(filepath.Join(genDir, "mockd"), + []byte(mockNodeWrapper(m.Genesis)), 0o755), + ) + // create upgrade wrappers + for name, args := range m.Upgrades { + upgradeDir := filepath.Join(cosmovisorDir, "upgrades", name, "bin") + require.NoError(t, os.MkdirAll(upgradeDir, 0o755)) + require.NoError(t, + os.WriteFile(filepath.Join(upgradeDir, "mockd"), + []byte(mockNodeWrapper(args)), 0o755), + ) + } +} + +func TestMockChain(t *testing.T) { + MockChainSetup{ + Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":14}'", + Upgrades: map[string]string{ + "gov1": "--halt-height 20 --block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":30}'", + "manual1": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":15}'", + }, + }.Setup(t) + //dir, err := os.Getwd() + //require.NoError(t, err) + //if !strings.HasSuffix(dir, "tools/cosmovisor/cmd/cosmovisor") { + // t.Fatalf("expected to be in tools/cosmovisor/cmd/cosmovisor, got %s", dir) + //} + //// switch to the root of the cosmovisor project + //t.Chdir(filepath.Join(dir, "..", "..")) + //// clean up previous test runs + //require.NoError(t, exec.Command("make", "clean").Run()) +} diff --git a/tools/cosmovisor/cmd/cosmovisor/run.go b/tools/cosmovisor/cmd/cosmovisor/run.go index e0f2feeb0c5e..28c3c4741db9 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run.go +++ b/tools/cosmovisor/cmd/cosmovisor/run.go @@ -49,7 +49,8 @@ func run(ctx context.Context, cfgPath string, args []string, options ...RunOptio } logger := cfg.Logger(runCfg.StdOut) - return internal.Run(ctx, cfg, runCfg, args, logger) + runner := internal.NewRunner(cfg, runCfg, logger) + return runner.Start(ctx, args) //launcher, err := cosmovisor.NewLauncher(logger, cfg) //if err != nil { // return err diff --git a/tools/cosmovisor/internal/errors.go b/tools/cosmovisor/internal/errors.go index 04eec3cb29fb..4960de4e334b 100644 --- a/tools/cosmovisor/internal/errors.go +++ b/tools/cosmovisor/internal/errors.go @@ -4,9 +4,7 @@ import ( "fmt" ) -type ErrUpgradeNeeded struct { - KnownHeight uint64 -} +type ErrUpgradeNeeded struct{} func (e ErrUpgradeNeeded) Error() string { return fmt.Sprintf("upgrade needed") diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index d7845b65c938..9f42f70d2464 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -15,19 +15,36 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -func Run(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) error { - knownHeight := uint64(0) +type TestCallback func() + +type Runner struct { + runCfg RunConfig + cfg *cosmovisor.Config + logger log.Logger + knownHeight uint64 +} + +// NewRunner creates a new Runner instance with the provided configuration and logger. +func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) Runner { + return Runner{ + runCfg: runCfg, + cfg: cfg, + logger: logger, + } +} + +func (r Runner) Start(ctx context.Context, args []string) error { // TODO handle cases where daemon shuts down without an upgrade, either have a retry count of fail in that case ideally backoff retry startsWithoutUpgrade := 0 for { - upgraded, haltHeight, err := UpgradeIfNeeded(cfg, logger, knownHeight) + upgraded, haltHeight, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) if err != nil { return err } if upgraded { - logger.Info("Upgrade completed, restarting process") - if !cfg.RestartAfterUpgrade { - logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") + r.logger.Info("Upgrade completed, restarting process") + if !r.cfg.RestartAfterUpgrade { + r.logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") } startsWithoutUpgrade = 0 } else { @@ -36,12 +53,11 @@ func Run(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []s } startsWithoutUpgrade++ } - err = RunOnce(ctx, cfg, runCfg, args, haltHeight, logger) + err = r.RunOnce(ctx, args, haltHeight) if err != nil { var upgradeNeeded ErrUpgradeNeeded if ok := errors.As(err, &upgradeNeeded); ok { - logger.Info("Upgrade needed") - knownHeight = upgradeNeeded.KnownHeight + r.logger.Info("Upgrade needed") } else { return err } @@ -49,22 +65,23 @@ func Run(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []s } } -func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args []string, haltHeight uint64, logger log.Logger) error { - dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, cfg.UpgradeInfoDir(), []string{ - cfg.UpgradeInfoFilePath(), - cfg.UpgradeInfoBatchFilePath(), +func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) error { + dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.cfg.UpgradeInfoDir(), []string{ + r.cfg.UpgradeInfoFilePath(), + r.cfg.UpgradeInfoBatchFilePath(), }) if err != nil { - logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) + r.logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) } ctx, cancel := context.WithCancel(ctx) defer cancel() - upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, cfg.PollInterval, dirWatcher, cfg.UpgradeInfoFilePath(), cfg.ParseUpgradeInfo) - manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, cfg.PollInterval, dirWatcher, cfg.UpgradeInfoBatchFilePath(), cfg.ParseManualUpgrades) + upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) + manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker("http://localhost:8080/block") - heightWatcher := watchers.NewHeightWatcher(ctx, heightChecker, cfg.PollInterval, func(height uint64) error { - return cfg.WriteLastKnownHeight(height) + heightWatcher := watchers.NewHeightWatcher(ctx, heightChecker, r.cfg.PollInterval, func(height uint64) error { + r.knownHeight = height + return r.cfg.WriteLastKnownHeight(height) }) if haltHeight > 0 { @@ -72,7 +89,7 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args args = append(args, fmt.Sprintf("--halt-height=%d", haltHeight)) } //// TODO start process runner - cmd, err := createCmd(cfg, runCfg, args, logger) + cmd, err := r.createCmd(args) if err != nil { return err } @@ -80,7 +97,7 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args defer func() { // TODO always check height before shutting down //_, _ = heightChecker.ReadNow() - _ = processRunner.Shutdown(cfg.ShutdownGrace) + _ = processRunner.Shutdown(r.cfg.ShutdownGrace) }() correctHeightConfirmed := false @@ -119,8 +136,8 @@ func RunOnce(ctx context.Context, cfg *cosmovisor.Config, runCfg RunConfig, args } } -func createCmd(cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger log.Logger) (*exec.Cmd, error) { - bin, err := cfg.CurrentBin() +func (r Runner) createCmd(args []string) (*exec.Cmd, error) { + bin, err := r.cfg.CurrentBin() if err != nil { return nil, fmt.Errorf("error creating symlink to genesis: %w", err) } @@ -129,11 +146,11 @@ func createCmd(cfg *cosmovisor.Config, runCfg RunConfig, args []string, logger l return nil, fmt.Errorf("current binary is invalid: %w", err) } - logger.Info("running app", "path", bin, "args", args) + r.logger.Info("running app", "path", bin, "args", args) cmd := exec.Command(bin, args...) - cmd.Stdin = runCfg.StdIn - cmd.Stdout = runCfg.StdOut - cmd.Stderr = runCfg.StdErr + cmd.Stdin = r.runCfg.StdIn + cmd.Stdout = r.runCfg.StdOut + cmd.Stderr = r.runCfg.StdErr return cmd, nil } diff --git a/tools/cosmovisor/internal/runner.mock.go b/tools/cosmovisor/internal/runner.mock.go deleted file mode 100644 index 5f2535df0eae..000000000000 --- a/tools/cosmovisor/internal/runner.mock.go +++ /dev/null @@ -1,174 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: state_machine.go -// -// Generated by this command: -// -// mockgen -source=state_machine.go -package=internal -destination=runner.mock.go -// - -// Package internal is a generated GoMock package. -package internal - -import ( - context "context" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockRunner is a mock of Runner interface. -type MockRunner struct { - ctrl *gomock.Controller - recorder *MockRunnerMockRecorder - isgomock struct{} -} - -// MockRunnerMockRecorder is the mock recorder for MockRunner. -type MockRunnerMockRecorder struct { - mock *MockRunner -} - -// NewMockRunner creates a new mock instance. -func NewMockRunner(ctrl *gomock.Controller) *MockRunner { - mock := &MockRunner{ctrl: ctrl} - mock.recorder = &MockRunnerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockRunner) EXPECT() *MockRunnerMockRecorder { - return m.recorder -} - -// CheckActualHeight mocks base method. -func (m *MockRunner) CheckActualHeight(ctx context.Context, args ...any) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range args { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "CheckActualHeight", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// CheckActualHeight indicates an expected call of CheckActualHeight. -func (mr *MockRunnerMockRecorder) CheckActualHeight(ctx any, args ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckActualHeight", reflect.TypeOf((*MockRunner)(nil).CheckActualHeight), varargs...) -} - -// CheckForManualUpgradeBatch mocks base method. -func (m *MockRunner) CheckForManualUpgradeBatch(ctx context.Context, args ...any) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range args { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "CheckForManualUpgradeBatch", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// CheckForManualUpgradeBatch indicates an expected call of CheckForManualUpgradeBatch. -func (mr *MockRunnerMockRecorder) CheckForManualUpgradeBatch(ctx any, args ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckForManualUpgradeBatch", reflect.TypeOf((*MockRunner)(nil).CheckForManualUpgradeBatch), varargs...) -} - -// CheckForUpgradeInfoJSON mocks base method. -func (m *MockRunner) CheckForUpgradeInfoJSON(ctx context.Context, args ...any) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range args { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "CheckForUpgradeInfoJSON", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// CheckForUpgradeInfoJSON indicates an expected call of CheckForUpgradeInfoJSON. -func (mr *MockRunnerMockRecorder) CheckForUpgradeInfoJSON(ctx any, args ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckForUpgradeInfoJSON", reflect.TypeOf((*MockRunner)(nil).CheckForUpgradeInfoJSON), varargs...) -} - -// CheckLastKnownHeight mocks base method. -func (m *MockRunner) CheckLastKnownHeight(ctx context.Context, args ...any) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range args { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "CheckLastKnownHeight", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// CheckLastKnownHeight indicates an expected call of CheckLastKnownHeight. -func (mr *MockRunnerMockRecorder) CheckLastKnownHeight(ctx any, args ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckLastKnownHeight", reflect.TypeOf((*MockRunner)(nil).CheckLastKnownHeight), varargs...) -} - -// Start mocks base method. -func (m *MockRunner) Start(ctx context.Context, args ...any) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range args { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Start", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// Start indicates an expected call of Start. -func (mr *MockRunnerMockRecorder) Start(ctx any, args ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockRunner)(nil).Start), varargs...) -} - -// StartWithHaltHeight mocks base method. -func (m *MockRunner) StartWithHaltHeight(ctx context.Context, args ...any) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range args { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "StartWithHaltHeight", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// StartWithHaltHeight indicates an expected call of StartWithHaltHeight. -func (mr *MockRunnerMockRecorder) StartWithHaltHeight(ctx any, args ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartWithHaltHeight", reflect.TypeOf((*MockRunner)(nil).StartWithHaltHeight), varargs...) -} - -// Stop mocks base method. -func (m *MockRunner) Stop(ctx context.Context, args ...any) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range args { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Stop", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// Stop indicates an expected call of Stop. -func (mr *MockRunnerMockRecorder) Stop(ctx any, args ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, args...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockRunner)(nil).Stop), varargs...) -} diff --git a/tools/cosmovisor/internal/state_machine.dot b/tools/cosmovisor/internal/state_machine.dot deleted file mode 100644 index 6770d731574b..000000000000 --- a/tools/cosmovisor/internal/state_machine.dot +++ /dev/null @@ -1,28 +0,0 @@ -digraph { - compound=true; - node [shape=Mrecord]; - rankdir="LR"; - - CheckForManualUpgradeBatch [label="CheckForManualUpgradeBatch|entry / CheckForManualUpgradeBatch-fm"]; - CheckForUpgradeInfoJSON [label="CheckForUpgradeInfoJSON|entry / CheckForUpgradeInfoJSON-fm"]; - CheckLastKnownHeight [label="CheckLastKnownHeight|entry / CheckLastKnownHeight-fm"]; - DoUpgrade [label="DoUpgrade"]; - Run [label="Run|entry / Start-fm"]; - RunWithHaltHeight [label="RunWithHaltHeight|entry / StartWithHaltHeight-fm\nentry / CheckActualHeight-fm"]; - ShutdownAndRestart [label="ShutdownAndRestart|entry / Stop-fm"]; - CheckForManualUpgradeBatch -> CheckLastKnownHeight [label=<
      haveManualUpgrades
      >]; - CheckForManualUpgradeBatch -> Run [label=<
      noManualUpgrades
      >]; - CheckForUpgradeInfoJSON -> CheckForManualUpgradeBatch [label=<
      haveUpgradePlan
      >]; - CheckForUpgradeInfoJSON -> DoUpgrade [label=<
      noUpgradePlan
      >]; - CheckLastKnownHeight -> RunWithHaltHeight [label=<
      onReadLastKnownHeight [beforeManualUpgradeHeight]
      >]; - CheckLastKnownHeight -> DoUpgrade [label=<
      onReadLastKnownHeight [atManualUpgradeHeight]
      >]; - CheckLastKnownHeight -> FatalError [label=<
      onReadLastKnownHeight [pastManualUpgradeHeight]
      >]; - DoUpgrade -> FatalError [label=<
      onUpgradeError
      >]; - DoUpgrade -> CheckForManualUpgradeBatch [label=<
      onUpgradeSuccess [allowDaemonRestart]
      >]; - DoUpgrade -> Done [label=<
      onUpgradeSuccess [disableDaemonRestart]
      >]; - Run -> ShutdownAndRestart [label=<
      haveUpgradePlan
      onGotNewManualUpgrade
      >]; - RunWithHaltHeight -> ShutdownAndRestart [label=<
      haveUpgradePlan
      onGotActualHeight [haveWrongHaltHeight]
      onGotNewManualUpgrade
      onReachedHaltHeight
      >]; - ShutdownAndRestart -> CheckForUpgradeInfoJSON [label=<
      onProcessExit
      >]; - init [label="", shape=point]; - init -> CheckForUpgradeInfoJSON -} diff --git a/tools/cosmovisor/internal/state_machine.go b/tools/cosmovisor/internal/state_machine.go deleted file mode 100644 index 53bd74e7c533..000000000000 --- a/tools/cosmovisor/internal/state_machine.go +++ /dev/null @@ -1,207 +0,0 @@ -package internal - -import ( - "context" - "reflect" - - "github.com/qmuntal/stateless" - - "cosmossdk.io/tools/cosmovisor" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" -) - -// triggers -var ( - triggerGotUpgradeInfoJSON = "haveUpgradePlan" - triggerNoUpgradeInfoJSON = "noUpgradePlan" - triggerGotManualUpgradeBatch = "haveManualUpgrades" - triggerNoManualUpgradeBatch = "noManualUpgrades" - triggerReadLastKnownHeight = "onReadLastKnownHeight" - triggerGotActualHeight = "onGotActualHeight" - triggerGotNewManualUpgrade = "onGotNewManualUpgrade" - triggerReachedHaltHeight = "onReachedHaltHeight" - triggerProcessExit = "onProcessExit" - triggerUpgradeSuccess = "onUpgradeSuccess" - triggerUpgradeError = "onUpgradeError" -) - -// states -var ( - //computeRunPlan = "ComputeRunPlan" - checkForUpgradeInfoJSON = "CheckForUpgradeInfoJSON" - checkForManualUpgradeBatch = "CheckForManualUpgradeBatch" - checkLastKnownHeight = "CheckLastKnownHeight" - doUpgrade = "DoUpgrade" - run = "Run" - runWithHaltHeight = "RunWithHaltHeight" - shutdownAndRestart = "ShutdownAndRestart" - fatalError = "FatalError" - done = "Done" -) - -func beforeManualUpgradeHeight(_ context.Context, args ...any) bool { - return false -} - -func atManualUpgradeHeight(_ context.Context, args ...any) bool { - return false -} - -func pastManualUpgradeHeight(_ context.Context, args ...any) bool { - return false -} - -func haveWrongHaltHeight(_ context.Context, args ...any) bool { - return false -} - -func allowDaemonRestart(_ context.Context, args ...any) bool { - return true -} - -func disableDaemonRestart(_ context.Context, args ...any) bool { - return false -} - -func StateMachine(runner Runner) *stateless.StateMachine { - fsm := stateless.NewStateMachine(checkForUpgradeInfoJSON) - - // configure triggers for the state machine - fsm.SetTriggerParameters(triggerGotUpgradeInfoJSON, reflect.TypeOf(&upgradetypes.Plan{})) - fsm.SetTriggerParameters(triggerGotManualUpgradeBatch, reflect.TypeOf(cosmovisor.ManualUpgradeBatch{})) - fsm.SetTriggerParameters(triggerReadLastKnownHeight, reflect.TypeOf(uint64(0))) - fsm.SetTriggerParameters(triggerGotActualHeight, reflect.TypeOf(uint64(0))) - fsm.SetTriggerParameters(triggerProcessExit, reflect.TypeOf(error(nil))) - - // configure ComputeRunPlan state - //fsm.Configure(computeRunPlan). - // InitialTransition(checkForUpgradeInfoJSON) - // - fsm.Configure(checkForUpgradeInfoJSON). - //SubstateOf(computeRunPlan). - OnEntry(runner.CheckForUpgradeInfoJSON). - Permit(triggerGotUpgradeInfoJSON, checkForManualUpgradeBatch). - Permit(triggerNoUpgradeInfoJSON, doUpgrade) - - fsm.Configure(checkForManualUpgradeBatch). - //SubstateOf(computeRunPlan). - OnEntry(runner.CheckForManualUpgradeBatch). - Permit(triggerNoManualUpgradeBatch, run). - Permit(triggerGotManualUpgradeBatch, checkLastKnownHeight) - - fsm.Configure(checkLastKnownHeight). - //SubstateOf(computeRunPlan). - OnEntry(runner.CheckLastKnownHeight). - Permit(triggerReadLastKnownHeight, runWithHaltHeight, beforeManualUpgradeHeight). - Permit(triggerReadLastKnownHeight, doUpgrade, atManualUpgradeHeight). - Permit(triggerReadLastKnownHeight, fatalError, pastManualUpgradeHeight) - - // configure Run state - - fsm.Configure(run). - OnEntry(runner.Start). - Permit(triggerGotUpgradeInfoJSON, shutdownAndRestart). - Permit(triggerGotNewManualUpgrade, shutdownAndRestart) - - // configure RunWithHaltHeight state - - fsm.Configure(runWithHaltHeight). - Permit(triggerGotActualHeight, shutdownAndRestart, haveWrongHaltHeight). - OnEntry(runner.StartWithHaltHeight). - OnEntry(runner.CheckActualHeight). - Permit(triggerGotUpgradeInfoJSON, shutdownAndRestart). - Permit(triggerReachedHaltHeight, shutdownAndRestart). - Permit(triggerGotNewManualUpgrade, shutdownAndRestart) - - // configure ShutdownAndRestart state - fsm.Configure(shutdownAndRestart). - OnEntry(runner.Stop). - Permit(triggerProcessExit, checkForUpgradeInfoJSON) - - // configure DoUpgrade state - fsm.Configure(doUpgrade). - Permit(triggerUpgradeSuccess, checkForManualUpgradeBatch, allowDaemonRestart). - Permit(triggerUpgradeSuccess, done, disableDaemonRestart). - Permit(triggerUpgradeError, fatalError) - - return fsm - - //OnEntry(func(ctx context.Context, args ...any) error { - // // TODO read upgrade-info.json - // // if upgrade-info.json exists, read it and return the state - // panic("ReadUpgradeInfoJson state entered") - //}) -} - -type Watcher int - -type Runner interface { - CheckForUpgradeInfoJSON(ctx context.Context, args ...any) error - CheckForManualUpgradeBatch(ctx context.Context, args ...any) error - CheckLastKnownHeight(ctx context.Context, args ...any) error - CheckActualHeight(ctx context.Context, args ...any) error - Start(ctx context.Context, args ...any) error - StartWithHaltHeight(ctx context.Context, args ...any) error - Stop(ctx context.Context, args ...any) error -} - -//type MockRunner struct{} -// -//func (m MockRunner) CheckForUpgradeInfoJSON(ctx context.Context) error { -// return nil -//} -// -////func (m MockRunner) CheckForManualUpgradeBatch(ctx context.Context) {} -//// -////func (m MockRunner) CheckLastKnownHeight(ctx context.Context) {} -//// -////func (m MockRunner) WatchUpgradeInfoJson(ctx context.Context) {} -//// -////func (m MockRunner) WatchManualUpgradeBatch(ctx context.Context) {} -//// -////var _ Runner = &MockRunner{} -// -////type Event interface{} -//// -////type UpgradePlanEvent struct{} -////type UpgradeBatchEvent struct{} -////type ProcessExistEvent struct{} -////type Height struct{} -//// -////type StateMachine struct { -////} -//// -////type State interface { -//// OnEvent(event Event) State -////} -//// -//////func (sm *StateMachine) WaitingForXUpgrade(event Event) { -////// -//////} -////// -//////func (sm *StateMachine) WaitingForManualUpgrade(event Event) { -////// -//////} -//// -////// type WaitingForManualUpgrade struct { -////// manualUpgradePlan *cosmovisor.ManualUpgradePlan -////// } -////// -////// type WaitingForXUpgrade struct{} -////// -////// type WaitingForShutdown struct{} -////func Init() { -//// // check if have upgrade-info.json -//// // check if have upgrade-info.json.batch -////} -//// -////func WaitingForXUpgrade(event Event) { -//// switch _ := event.(type) { -//// case UpgradePlanEvent: -//// // handle received upgrade plan event -//// case UpgradeBatchEvent: -//// // handle received upgrade batch event -//// case ProcessExistEvent: -//// // check for upgrade-info.json -//// } -////} diff --git a/tools/cosmovisor/internal/state_machine.png b/tools/cosmovisor/internal/state_machine.png deleted file mode 100644 index b675d0e9ab7d19564173170372d597336b318a8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192656 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
      pUwzNHQGeLDtkX2SwWOIVkhe9j4f6Uc(R6IFr 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�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 diff --git a/tools/cosmovisor/internal/state_machine_test.go b/tools/cosmovisor/internal/state_machine_test.go deleted file mode 100644 index af613b9aad25..000000000000 --- a/tools/cosmovisor/internal/state_machine_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package internal - -import ( - "context" - "os" - "os/exec" - "reflect" - "testing" - - "github.com/qmuntal/stateless" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestStateMachineGraph(t *testing.T) { - ctrl := gomock.NewController(t) - mock := NewMockRunner(ctrl) - mock.EXPECT().CheckForUpgradeInfoJSON(gomock.Any()).Return(nil).AnyTimes() - err := os.WriteFile("state_machine.dot", []byte(StateMachine(mock).ToGraph()), 0644) - require.NoError(t, err, "Failed to write state machine graph to file") - err = exec.Command("dot", "-Tpng", "state_machine.dot", "-o", "state_machine.png").Run() - require.NoError(t, err, "Failed to generate PNG from state machine graph") -} - -func TestStateMachine(t *testing.T) { - ctrl := gomock.NewController(t) - mock := NewMockRunner(ctrl) - fsm := StateMachine(mock) - fsm.OnTransitioning(func(ctx context.Context, tx stateless.Transition) { - t.Logf("Transitioning from %s to %s with trigger %s", tx.Source, tx.Destination, tx.Trigger) - }) - fsm.OnTransitioned(func(ctx context.Context, tx stateless.Transition) { - t.Logf("Transitioned from %s to %s with trigger %s", tx.Source, tx.Destination, tx.Trigger) - }) - require.NoError(t, fsm.ActivateCtx(context.Background())) - state, err := fsm.State(context.Background()) - require.NoError(t, err, "Failed to get initial state") - t.Logf("Initial state: %s", state) -} - -func TestFSM2(t *testing.T) { - fsm := stateless.NewStateMachine("A") - fsm.SetTriggerParameters("go", reflect.TypeOf(""), reflect.TypeOf("")) - - fsm.Configure("A"). - Permit("go", "B") - - fsm.Configure("B"). - OnEntry(func(ctx context.Context, args ...any) error { - t.Log("Entering state B with args:", args) - return nil - }) - require.NoError(t, fsm.Fire("go", "arg1", "arg2")) -} From b1b7eb4ce652b7cea805976fa92571f65359ff73 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 3 Jun 2025 14:40:27 -0400 Subject: [PATCH 036/115] WIP on testing setup --- tools/cosmovisor/TEST_SCENARIOS.md | 5 +++ .../cmd/cosmovisor/mockchain_test.go | 38 +++++++++++++------ .../testdata/mockchain/cosmovisor/.gitignore | 1 - .../mockchain/cosmovisor/genesis/bin/mockd | 4 -- .../cosmovisor/upgrades/manual1/bin/mockd | 4 -- .../testdata/mockchain/data/.gitignore | 4 -- 6 files changed, 32 insertions(+), 24 deletions(-) delete mode 100644 tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore delete mode 100755 tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd delete mode 100755 tools/cosmovisor/testdata/mockchain/cosmovisor/upgrades/manual1/bin/mockd delete mode 100644 tools/cosmovisor/testdata/mockchain/data/.gitignore diff --git a/tools/cosmovisor/TEST_SCENARIOS.md b/tools/cosmovisor/TEST_SCENARIOS.md index f8ea397e2e11..13a983b189a6 100644 --- a/tools/cosmovisor/TEST_SCENARIOS.md +++ b/tools/cosmovisor/TEST_SCENARIOS.md @@ -1,3 +1,8 @@ +Ideally, scenarios should run in process with args passed manually to cobra so that we can: +- code coverage +- use the debugger + +## Scenarios - basic: start node, get upgrade-info.json, upgrade - manual upgrade added while running: - start node diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 8debbca69e5a..9fe2acb07d3f 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -1,17 +1,22 @@ package main import ( + "context" "fmt" "os" "path/filepath" "testing" + "time" "github.com/stretchr/testify/require" + + "cosmossdk.io/tools/cosmovisor" ) type MockChainSetup struct { Genesis string Upgrades map[string]string + Config *cosmovisor.Config } func mockNodeWrapper(args string) string { @@ -23,7 +28,7 @@ exec mock_node %s "$@" `, args) } -func (m MockChainSetup) Setup(t *testing.T) { +func (m MockChainSetup) Setup(t *testing.T) (string, string) { dir, err := os.MkdirTemp("", "mockchain") require.NoError(t, err) t.Cleanup(func() { @@ -48,23 +53,34 @@ func (m MockChainSetup) Setup(t *testing.T) { []byte(mockNodeWrapper(args)), 0o755), ) } + + // update config and save it + if m.Config == nil { + m.Config = &cosmovisor.Config{} + } + m.Config.Name = "mockd" + m.Config.Home = dir + m.Config.DataBackupPath = dir + cfgFile, err := m.Config.Export() + require.NoError(t, err) + t.Logf("Cosmovisor config: %s", cfgFile) + + return dir, cfgFile } func TestMockChain(t *testing.T) { - MockChainSetup{ + mockchainDir, cfgFile := MockChainSetup{ Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":14}'", Upgrades: map[string]string{ "gov1": "--halt-height 20 --block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":30}'", "manual1": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":15}'", }, + Config: &cosmovisor.Config{ + PollInterval: time.Second, + }, }.Setup(t) - //dir, err := os.Getwd() - //require.NoError(t, err) - //if !strings.HasSuffix(dir, "tools/cosmovisor/cmd/cosmovisor") { - // t.Fatalf("expected to be in tools/cosmovisor/cmd/cosmovisor, got %s", dir) - //} - //// switch to the root of the cosmovisor project - //t.Chdir(filepath.Join(dir, "..", "..")) - //// clean up previous test runs - //require.NoError(t, exec.Command("make", "clean").Run()) + + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"run", "--home", mockchainDir, "--cosmovisor-config", cfgFile}) + require.NoError(t, rootCmd.ExecuteContext(context.Background())) } diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore b/tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore deleted file mode 100644 index 1f4ddec6a80e..000000000000 --- a/tools/cosmovisor/testdata/mockchain/cosmovisor/.gitignore +++ /dev/null @@ -1 +0,0 @@ -current \ No newline at end of file diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd b/tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd deleted file mode 100755 index 9517c720c186..000000000000 --- a/tools/cosmovisor/testdata/mockchain/cosmovisor/genesis/bin/mockd +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -set -e - -exec mock_node "$@" --block-time 1s --upgrade-plan '{"name":"gov1","height":10}' \ No newline at end of file diff --git a/tools/cosmovisor/testdata/mockchain/cosmovisor/upgrades/manual1/bin/mockd b/tools/cosmovisor/testdata/mockchain/cosmovisor/upgrades/manual1/bin/mockd deleted file mode 100755 index 644fc6a2cba8..000000000000 --- a/tools/cosmovisor/testdata/mockchain/cosmovisor/upgrades/manual1/bin/mockd +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -set -e - -exec mock_node "$@" --block-time 1s --upgrade-plan '{"name":"gov1","height":10}' diff --git a/tools/cosmovisor/testdata/mockchain/data/.gitignore b/tools/cosmovisor/testdata/mockchain/data/.gitignore deleted file mode 100644 index 86d0cb2726c6..000000000000 --- a/tools/cosmovisor/testdata/mockchain/data/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file From 291e00fdd9983cd40456f8f5dc4d148ff8594580 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 3 Jun 2025 17:51:52 -0400 Subject: [PATCH 037/115] fixes from testing --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 15 +++++++++------ tools/cosmovisor/cmd/mock_node/main.go | 6 ++++-- tools/cosmovisor/internal/runner.go | 7 ++++++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 9fe2acb07d3f..3f3a6b4d61a0 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -20,11 +20,12 @@ type MockChainSetup struct { } func mockNodeWrapper(args string) string { - return fmt.Sprintf(` -#!/usr/bin/env bash + return fmt.Sprintf( + `#!/usr/bin/env bash set -e -exec mock_node %s "$@" +echo "$@" +exec mock_node %s "$@" `, args) } @@ -40,9 +41,9 @@ func (m MockChainSetup) Setup(t *testing.T) (string, string) { // create genesis wrapper genDir := filepath.Join(cosmovisorDir, "genesis", "bin") require.NoError(t, os.MkdirAll(genDir, 0o755)) + mockdPath := filepath.Join(genDir, "mockd") require.NoError(t, - os.WriteFile(filepath.Join(genDir, "mockd"), - []byte(mockNodeWrapper(m.Genesis)), 0o755), + os.WriteFile(mockdPath, []byte(mockNodeWrapper(m.Genesis)), 0o755), ) // create upgrade wrappers for name, args := range m.Upgrades { @@ -70,7 +71,7 @@ func (m MockChainSetup) Setup(t *testing.T) (string, string) { func TestMockChain(t *testing.T) { mockchainDir, cfgFile := MockChainSetup{ - Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":14}'", + Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":15}'", Upgrades: map[string]string{ "gov1": "--halt-height 20 --block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":30}'", "manual1": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":15}'", @@ -82,5 +83,7 @@ func TestMockChain(t *testing.T) { rootCmd := NewRootCmd() rootCmd.SetArgs([]string{"run", "--home", mockchainDir, "--cosmovisor-config", cfgFile}) + rootCmd.SetOut(os.Stdout) + rootCmd.SetErr(os.Stderr) require.NoError(t, rootCmd.ExecuteContext(context.Background())) } diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 662b30da5965..04cd1a43a8d7 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -108,7 +108,7 @@ func (n *MockNode) Run(ctx context.Context) error { upgradeHeight := n.haltHeight if n.upgradePlan != nil { upgradePlanHeight := uint64(n.upgradePlan.Height) - if upgradePlanHeight < upgradeHeight { + if upgradeHeight == 0 || upgradePlanHeight < upgradeHeight { upgradeHeight = upgradePlanHeight } } @@ -116,13 +116,14 @@ func (n *MockNode) Run(ctx context.Context) error { actualHeightFile := path.Join(n.homePath, "data", "actual-height") // try to read the actual-height file if it exists if bz, err := os.ReadFile(actualHeightFile); err == nil { + n.logger.Info("Reading existing height", "height", string(bz)) n.height, err = strconv.ParseUint(string(bz), 10, 64) if err != nil { return fmt.Errorf("failed to parse actual height from file: %w", err) } } - n.logger.Info("Starting mock node", "start_height", n.height, "block_time", n.blockTime, "upgrade_plan", n.upgradePlan, "halt_height", upgradeHeight) + n.logger.Info("Starting mock node", "start_height", n.height, "block_time", n.blockTime, "upgrade_plan", n.upgradePlan, "halt_height", n.haltHeight) srv := n.startHTTPServer() ticker := time.NewTicker(n.blockTime) defer ticker.Stop() @@ -149,6 +150,7 @@ func (n *MockNode) Run(ctx context.Context) error { } } if n.haltHeight > 0 { + // this log line matches what BaseApp does when it reaches the halt height n.logger.Error(fmt.Sprintf("halt per configuration height %d", n.height)) } else if n.upgradePlan != nil { n.logger.Info("Mock node reached upgrade height, writing upgrade-info.json", "upgrade_plan", n.upgradePlan) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 9f42f70d2464..4ca98d95363c 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -108,11 +108,13 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e if !ok { return nil } + r.logger.Info("Received upgrade-info.json") return ErrUpgradeNeeded{} case _, ok := <-manualUpgradesWatcher.Updated(): if !ok { return nil } + r.logger.Info("Received updates to upgrade-info.json.batch") if haltHeight == 0 { // TODO shutdown, no halt height set return ErrUpgradeNeeded{} @@ -121,14 +123,17 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e } case err := <-processRunner.Done(): // TODO handle process exit + r.logger.Warn("Process exited unexpectedly", "error", err) return err // TODO: case actualHeight := <-heightWatcher.Updated(): + r.logger.Warn("Got height update from watcher", "height", actualHeight) if !correctHeightConfirmed { // TODO read manual upgrade batch and check if we'd still be at the correct halt height correctHeightConfirmed = true } - if actualHeight >= haltHeight { + // signal an upgrade if we have a halt height and we are at or past it + if haltHeight > 0 && actualHeight >= haltHeight { return ErrUpgradeNeeded{} } // TODO error channels From 9445e4217b1f661a4a1fd2ab8befd462394f21f7 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 4 Jun 2025 10:35:31 -0400 Subject: [PATCH 038/115] WIP on testing --- .../cmd/cosmovisor/mockchain_test.go | 83 +++++++++++++++---- tools/cosmovisor/internal/errors.go | 6 +- tools/cosmovisor/internal/runner.go | 23 ++--- tools/cosmovisor/internal/testing.go | 19 +++++ 4 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 tools/cosmovisor/internal/testing.go diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 3f3a6b4d61a0..6f4f0172dfd9 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -5,18 +5,21 @@ import ( "fmt" "os" "path/filepath" + "sync" "testing" "time" "github.com/stretchr/testify/require" "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/internal" ) type MockChainSetup struct { - Genesis string - Upgrades map[string]string - Config *cosmovisor.Config + Genesis string + GovUpgrades map[string]string + ManualUpgrades map[string]string // to be added with the add-upgrade command + Config *cosmovisor.Config } func mockNodeWrapper(args string) string { @@ -45,8 +48,8 @@ func (m MockChainSetup) Setup(t *testing.T) (string, string) { require.NoError(t, os.WriteFile(mockdPath, []byte(mockNodeWrapper(m.Genesis)), 0o755), ) - // create upgrade wrappers - for name, args := range m.Upgrades { + // create gov upgrade wrappers + for name, args := range m.GovUpgrades { upgradeDir := filepath.Join(cosmovisorDir, "upgrades", name, "bin") require.NoError(t, os.MkdirAll(upgradeDir, 0o755)) require.NoError(t, @@ -54,6 +57,13 @@ func (m MockChainSetup) Setup(t *testing.T) (string, string) { []byte(mockNodeWrapper(args)), 0o755), ) } + // create manual upgrade wrappers + manualUpgradeDir := filepath.Join(dir, "manual-upgrades") + require.NoError(t, os.MkdirAll(manualUpgradeDir, 0o755)) + for name, args := range m.ManualUpgrades { + filename := filepath.Join(manualUpgradeDir, name) + require.NoError(t, os.WriteFile(filename, []byte(mockNodeWrapper(args)), 0o755)) + } // update config and save it if m.Config == nil { @@ -71,19 +81,64 @@ func (m MockChainSetup) Setup(t *testing.T) (string, string) { func TestMockChain(t *testing.T) { mockchainDir, cfgFile := MockChainSetup{ - Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":15}'", - Upgrades: map[string]string{ - "gov1": "--halt-height 20 --block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":30}'", - "manual1": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":15}'", + Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", + GovUpgrades: map[string]string{ + "gov1": "--halt-height 20 --block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":40}'", + }, + ManualUpgrades: map[string]string{ + "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", + "manual20": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", }, Config: &cosmovisor.Config{ PollInterval: time.Second, }, }.Setup(t) - rootCmd := NewRootCmd() - rootCmd.SetArgs([]string{"run", "--home", mockchainDir, "--cosmovisor-config", cfgFile}) - rootCmd.SetOut(os.Stdout) - rootCmd.SetErr(os.Stderr) - require.NoError(t, rootCmd.ExecuteContext(context.Background())) + var callbackQueue []func() + testCallback := func() { + for _, cb := range callbackQueue { + cb() + } + callbackQueue = nil // reset for next test + } + callbackQueue = append(callbackQueue, func() { + go func() { + time.Sleep(2 * time.Second) // wait for startup + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{ + "add-upgrade", + "manual20", + filepath.Join(mockchainDir, "manual-upgrades", "manual20"), + "--upgrade-height", + "20", + "--cosmovisor-config", + cfgFile, + }) + rootCmd.SetOut(os.Stdout) + rootCmd.SetErr(os.Stderr) + require.NoError(t, rootCmd.Execute()) + }() + }) + var wg sync.WaitGroup + wg.Add(1) + go func() { + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"run", "--home", mockchainDir, "--cosmovisor-config", cfgFile}) + rootCmd.SetOut(os.Stdout) + rootCmd.SetErr(os.Stderr) + ctx := internal.WithTestCallback(context.Background(), testCallback) + require.NoError(t, rootCmd.ExecuteContext(ctx)) + wg.Done() + }() + wg.Wait() + + // TODO: + // - [x] add callback on restart for checking state + // - [ ] add manual upgrade (manual20) at height 20 + // - [ ] then add another manual upgrade (manual10) at height 10 + // - [ ] manual20 should get picked up and the process should restart with halt-height 20 + // - [ ] then manual10 should get picked up and the process should restart with halt-height 10 + // - [ ] when manual10 gets applied, it should restart with halt-height 20 + // - [ ] when manual20 gets applied, it should restart with no halt-height + // - [ ] and then manual20 should trigger gov2 upgrade at height 30 } diff --git a/tools/cosmovisor/internal/errors.go b/tools/cosmovisor/internal/errors.go index 4960de4e334b..fd03e41f80a6 100644 --- a/tools/cosmovisor/internal/errors.go +++ b/tools/cosmovisor/internal/errors.go @@ -4,10 +4,10 @@ import ( "fmt" ) -type ErrUpgradeNeeded struct{} +type ErrRestartNeeded struct{} -func (e ErrUpgradeNeeded) Error() string { +func (e ErrRestartNeeded) Error() string { return fmt.Sprintf("upgrade needed") } -var _ error = ErrUpgradeNeeded{} +var _ error = ErrRestartNeeded{} diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 4ca98d95363c..9be0a89803d4 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -15,8 +15,6 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -type TestCallback func() - type Runner struct { runCfg RunConfig cfg *cosmovisor.Config @@ -37,15 +35,15 @@ func (r Runner) Start(ctx context.Context, args []string) error { // TODO handle cases where daemon shuts down without an upgrade, either have a retry count of fail in that case ideally backoff retry startsWithoutUpgrade := 0 for { + if testCallback := GetTestCallback(ctx); testCallback != nil { + testCallback() + } upgraded, haltHeight, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) if err != nil { return err } if upgraded { r.logger.Info("Upgrade completed, restarting process") - if !r.cfg.RestartAfterUpgrade { - r.logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") - } startsWithoutUpgrade = 0 } else { if startsWithoutUpgrade >= 5 { @@ -53,9 +51,13 @@ func (r Runner) Start(ctx context.Context, args []string) error { } startsWithoutUpgrade++ } + if !r.cfg.RestartAfterUpgrade { + r.logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") + return nil + } err = r.RunOnce(ctx, args, haltHeight) if err != nil { - var upgradeNeeded ErrUpgradeNeeded + var upgradeNeeded ErrRestartNeeded if ok := errors.As(err, &upgradeNeeded); ok { r.logger.Info("Upgrade needed") } else { @@ -85,10 +87,9 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e }) if haltHeight > 0 { - // TODO start height watcher + r.logger.Info("Setting --halt-height flag for manual upgrade", "halt_height", haltHeight) args = append(args, fmt.Sprintf("--halt-height=%d", haltHeight)) } - //// TODO start process runner cmd, err := r.createCmd(args) if err != nil { return err @@ -109,7 +110,7 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e return nil } r.logger.Info("Received upgrade-info.json") - return ErrUpgradeNeeded{} + return ErrRestartNeeded{} case _, ok := <-manualUpgradesWatcher.Updated(): if !ok { return nil @@ -117,7 +118,7 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e r.logger.Info("Received updates to upgrade-info.json.batch") if haltHeight == 0 { // TODO shutdown, no halt height set - return ErrUpgradeNeeded{} + return ErrRestartNeeded{} } else { // TODO check if this would change the halt height } @@ -134,7 +135,7 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e } // signal an upgrade if we have a halt height and we are at or past it if haltHeight > 0 && actualHeight >= haltHeight { - return ErrUpgradeNeeded{} + return ErrRestartNeeded{} } // TODO error channels } diff --git a/tools/cosmovisor/internal/testing.go b/tools/cosmovisor/internal/testing.go new file mode 100644 index 000000000000..e671f5706b6d --- /dev/null +++ b/tools/cosmovisor/internal/testing.go @@ -0,0 +1,19 @@ +package internal + +import "context" + +type TestCallback func() + +type testCallbackKey struct{} + +func WithTestCallback(ctx context.Context, cb TestCallback) context.Context { + return context.WithValue(ctx, testCallbackKey{}, cb) +} + +func GetTestCallback(ctx context.Context) TestCallback { + cb, ok := ctx.Value(testCallbackKey{}).(TestCallback) + if !ok { + return nil + } + return cb +} From c700952b0cbb7e6f1f2cb85a7322732ca3282248 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 4 Jun 2025 13:13:56 -0400 Subject: [PATCH 039/115] manual upgrade detection works --- tools/cosmovisor/args.go | 1 - .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 3 +- .../cmd/cosmovisor/mockchain_test.go | 3 +- tools/cosmovisor/internal/runner.go | 14 +++--- tools/cosmovisor/internal/upgrade.go | 1 + tools/cosmovisor/manual.go | 44 ++++++++++++++++--- 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index e0e27658ff2a..af58778acd86 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -433,7 +433,6 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) { // UpgradeInfo returns the current upgrade info func (cfg *Config) UpgradeInfo() (upgradetypes.Plan, error) { filename := cfg.UpgradeInfoFilePath() - fmt.Printf("Reading upgrade info from %q\n", filename) _, err := os.Lstat(filename) var bz []byte if err != nil { // no current directory diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 62fe8d6aa667..a873d43087f9 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -72,7 +72,8 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade panic(fmt.Errorf("invalid manual upgrade plan: %w", err)) } - if err := cosmovisor.AddManualUpgrade(cfg, plan, force); err != nil { + // TODO only do this once for a whole batch of upgrades + if err := cfg.AddManualUpgrades(force, plan); err != nil { panic(fmt.Errorf("failed to add manual upgrade: %w", err)) } diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 6f4f0172dfd9..e9b264d7105c 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -90,7 +90,8 @@ func TestMockChain(t *testing.T) { "manual20": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", }, Config: &cosmovisor.Config{ - PollInterval: time.Second, + PollInterval: time.Second, + RestartAfterUpgrade: true, }, }.Setup(t) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 9be0a89803d4..fed36b861848 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -44,6 +44,10 @@ func (r Runner) Start(ctx context.Context, args []string) error { } if upgraded { r.logger.Info("Upgrade completed, restarting process") + if !r.cfg.RestartAfterUpgrade { + r.logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") + return nil + } startsWithoutUpgrade = 0 } else { if startsWithoutUpgrade >= 5 { @@ -51,15 +55,11 @@ func (r Runner) Start(ctx context.Context, args []string) error { } startsWithoutUpgrade++ } - if !r.cfg.RestartAfterUpgrade { - r.logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") - return nil - } err = r.RunOnce(ctx, args, haltHeight) if err != nil { - var upgradeNeeded ErrRestartNeeded - if ok := errors.As(err, &upgradeNeeded); ok { - r.logger.Info("Upgrade needed") + var restartNeeded ErrRestartNeeded + if ok := errors.As(err, &restartNeeded); ok { + r.logger.Info("Restart needed") } else { return err } diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrade.go index 18a6ba127544..e585b76d1f0e 100644 --- a/tools/cosmovisor/internal/upgrade.go +++ b/tools/cosmovisor/internal/upgrade.go @@ -75,6 +75,7 @@ func NewUpgrader(cfg *cosmovisor.Config, logger log.Logger, upgradePlan upgradet } func (u *Upgrader) DoUpgrade() error { + u.logger.Info("Starting upgrade process") u.cfg.WaitRestartDelay() if err := u.doBackup(); err != nil { diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 6663e165a684..a375fc1c0915 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -36,28 +36,58 @@ func (cfg *Config) ParseManualUpgrades(bz []byte) (ManualUpgradeBatch, error) { return manualUpgrades, nil } -// AddManualUpgrade adds a manual upgrade plan. +// AddManualUpgrades adds a manual upgrade plan. // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. -func AddManualUpgrade(cfg *Config, plan *upgradetypes.Plan, forceOverwrite bool) error { +func (cfg *Config) AddManualUpgrades(forceOverwrite bool, plans ...*upgradetypes.Plan) error { // TODO only allow plans that are AFTER the last known height - manualUpgrades, err := cfg.ReadManualUpgrades() + existing, err := cfg.ReadManualUpgrades() if err != nil { return err } - var newUpgrades ManualUpgradeBatch - for _, existing := range manualUpgrades { - if existing.Name == plan.Name { + planMap := map[string]*upgradetypes.Plan{} + for _, existingPlan := range existing { + planMap[existingPlan.Name] = existingPlan + } + for _, plan := range plans { + if _, ok := planMap[plan.Name]; ok { if !forceOverwrite { return fmt.Errorf("upgrade with name %s already exists", plan.Name) } - newUpgrades = append(newUpgrades, plan) + } + planMap[plan.Name] = plan + } + + var newUpgrades ManualUpgradeBatch + for _, plan := range planMap { + newUpgrades = append(newUpgrades, plan) + } + + return cfg.saveManualUpgrade(newUpgrades) +} + +func (cfg *Config) RemoveManualUpgrade(height uint64) error { + manualUpgrades, err := cfg.ReadManualUpgrades() + if err != nil { + return err + } + + var newUpgrades ManualUpgradeBatch + for _, existing := range manualUpgrades { + if uint64(existing.Height) == height { + continue } else { newUpgrades = append(newUpgrades, existing) } } + if len(newUpgrades) == len(manualUpgrades) { + return nil + } + return cfg.saveManualUpgrade(newUpgrades) +} +func (cfg *Config) saveManualUpgrade(manualUpgrades ManualUpgradeBatch) error { sortUpgrades(manualUpgrades) // TODO we should not write the file every time we add an upgrade, but only once per command otherwise we can trigger spurious From 6a2f11844c1702d3a41d3a1b4ea52f2819b23ce0 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 4 Jun 2025 14:09:24 -0400 Subject: [PATCH 040/115] successful tests so far --- .../cmd/cosmovisor/mockchain_test.go | 7 ++-- tools/cosmovisor/internal/runner.go | 7 ++-- tools/cosmovisor/internal/upgrade.go | 38 ++++++++++++++----- .../internal/watchers/file_poll_watcher.go | 21 +++++++--- tools/cosmovisor/manual.go | 10 ++--- 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index e9b264d7105c..d7a4830c8f34 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -83,7 +83,7 @@ func TestMockChain(t *testing.T) { mockchainDir, cfgFile := MockChainSetup{ Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", GovUpgrades: map[string]string{ - "gov1": "--halt-height 20 --block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":40}'", + "gov1": "--block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":40}'", }, ManualUpgrades: map[string]string{ "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", @@ -136,10 +136,11 @@ func TestMockChain(t *testing.T) { // TODO: // - [x] add callback on restart for checking state // - [ ] add manual upgrade (manual20) at height 20 - // - [ ] then add another manual upgrade (manual10) at height 10 + // - [ ] then add other manual upgrades manual10 at height 10 and manual30 at height 30 as a batch // - [ ] manual20 should get picked up and the process should restart with halt-height 20 // - [ ] then manual10 should get picked up and the process should restart with halt-height 10 // - [ ] when manual10 gets applied, it should restart with halt-height 20 // - [ ] when manual20 gets applied, it should restart with no halt-height - // - [ ] and then manual20 should trigger gov2 upgrade at height 30 + // - [ ] and then manual20 should trigger gov2 upgrade at height 40 + // - [ ] } diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index fed36b861848..bc205368794a 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -50,6 +50,7 @@ func (r Runner) Start(ctx context.Context, args []string) error { } startsWithoutUpgrade = 0 } else { + // TODO check that the actual height is meaningfully progressing and we are not stuck in some manual upgrade loop where halt-height keeps getting set at the same height if startsWithoutUpgrade >= 5 { return fmt.Errorf("process restarted %d times without an upgrade, exiting", startsWithoutUpgrade) } @@ -111,13 +112,13 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e } r.logger.Info("Received upgrade-info.json") return ErrRestartNeeded{} - case _, ok := <-manualUpgradesWatcher.Updated(): + case manualUpgrades, ok := <-manualUpgradesWatcher.Updated(): if !ok { return nil } r.logger.Info("Received updates to upgrade-info.json.batch") - if haltHeight == 0 { - // TODO shutdown, no halt height set + if haltHeight == 0 && len(manualUpgrades) > 0 { + // shutdown, no halt height set return ErrRestartNeeded{} } else { // TODO check if this would change the halt height diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrade.go index e585b76d1f0e..d485b761a02f 100644 --- a/tools/cosmovisor/internal/upgrade.go +++ b/tools/cosmovisor/internal/upgrade.go @@ -23,7 +23,7 @@ type UpgradeCheckResult struct { func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint64) (upgraded bool, haltHeight uint64, err error) { logger.Info("Checking for upgrade-info.json") if upgradePlan, err := cfg.UpgradeInfo(); err == nil { - upgrader := NewUpgrader(cfg, logger, upgradePlan) + upgrader := NewUpgrader(cfg, logger, upgradePlan, false) err := upgrader.DoUpgrade() if err != nil { return false, 0, err @@ -44,7 +44,7 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint haltHeight = uint64(manualUpgrade.Height) if lastKnownHeight == haltHeight { logger.Info("At manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) - upgrader := NewUpgrader(cfg, logger, *manualUpgrade) + upgrader := NewUpgrader(cfg, logger, *manualUpgrade, true) err := upgrader.DoUpgrade() if err != nil { return false, 0, err @@ -61,16 +61,18 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint } type Upgrader struct { - cfg *cosmovisor.Config - logger log.Logger - upgradePlan upgradetypes.Plan + cfg *cosmovisor.Config + logger log.Logger + upgradePlan upgradetypes.Plan + isManualUpgrade bool } -func NewUpgrader(cfg *cosmovisor.Config, logger log.Logger, upgradePlan upgradetypes.Plan) *Upgrader { +func NewUpgrader(cfg *cosmovisor.Config, logger log.Logger, upgradePlan upgradetypes.Plan, isManualUpgrade bool) *Upgrader { return &Upgrader{ - cfg: cfg, - logger: logger, - upgradePlan: upgradePlan, + cfg: cfg, + logger: logger, + upgradePlan: upgradePlan, + isManualUpgrade: isManualUpgrade, } } @@ -78,6 +80,16 @@ func (u *Upgrader) DoUpgrade() error { u.logger.Info("Starting upgrade process") u.cfg.WaitRestartDelay() + currentBin, err := u.cfg.CurrentBin() + if err != nil { + return err + } + upgradeBin := u.cfg.UpgradeBin(u.upgradePlan.Name) + u.logger.Info("Current binary", "current_bin", currentBin, "upgrade_bin", upgradeBin) + if currentBin == upgradeBin { + return fmt.Errorf("current binary %s is already the upgrade binary %s, fatal error", currentBin, upgradeBin) + } + if err := u.doBackup(); err != nil { return err } @@ -94,6 +106,14 @@ func (u *Upgrader) DoUpgrade() error { return err } + if u.isManualUpgrade { + u.logger.Info("Removing completed manual upgrade plan", "height", u.upgradePlan.Height) + err := u.cfg.RemoveManualUpgrade(u.upgradePlan.Height) + if err != nil { + return fmt.Errorf("failed to remove manual upgrade at height %d: %w", u.upgradePlan.Height, err) + } + } + return nil } diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher.go b/tools/cosmovisor/internal/watchers/file_poll_watcher.go index f4fd40db30f5..50d454a48012 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher.go @@ -8,18 +8,27 @@ import ( ) func NewFilePollWatcher(ctx context.Context, filename string, pollInterval time.Duration) Watcher[[]byte] { + stat, err := os.Stat(filename) + var lastModTime time.Time + if err == nil { + lastModTime = stat.ModTime() + } check := func() ([]byte, error) { stat, err := os.Stat(filename) if err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("failed to stat file %s: %w", filename, err) } - } else if stat.Size() > 0 { - bz, err := os.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", filename, err) - } else { - return bz, nil + } else { + modTime := stat.ModTime() + if stat.Size() > 0 && !modTime.Equal(lastModTime) { + lastModTime = modTime + bz, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", filename, err) + } else { + return bz, nil + } } } return nil, os.ErrNotExist diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index a375fc1c0915..2c761582869c 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -64,10 +64,10 @@ func (cfg *Config) AddManualUpgrades(forceOverwrite bool, plans ...*upgradetypes newUpgrades = append(newUpgrades, plan) } - return cfg.saveManualUpgrade(newUpgrades) + return cfg.saveManualUpgrades(newUpgrades) } -func (cfg *Config) RemoveManualUpgrade(height uint64) error { +func (cfg *Config) RemoveManualUpgrade(height int64) error { manualUpgrades, err := cfg.ReadManualUpgrades() if err != nil { return err @@ -75,7 +75,7 @@ func (cfg *Config) RemoveManualUpgrade(height uint64) error { var newUpgrades ManualUpgradeBatch for _, existing := range manualUpgrades { - if uint64(existing.Height) == height { + if existing.Height == height { continue } else { newUpgrades = append(newUpgrades, existing) @@ -84,10 +84,10 @@ func (cfg *Config) RemoveManualUpgrade(height uint64) error { if len(newUpgrades) == len(manualUpgrades) { return nil } - return cfg.saveManualUpgrade(newUpgrades) + return cfg.saveManualUpgrades(newUpgrades) } -func (cfg *Config) saveManualUpgrade(manualUpgrades ManualUpgradeBatch) error { +func (cfg *Config) saveManualUpgrades(manualUpgrades ManualUpgradeBatch) error { sortUpgrades(manualUpgrades) // TODO we should not write the file every time we add an upgrade, but only once per command otherwise we can trigger spurious From cc0bbe65943237fa7d1721e5748b69818d91154a Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 4 Jun 2025 14:56:20 -0400 Subject: [PATCH 041/115] working manual upgrade swapping --- .../cmd/cosmovisor/mockchain_test.go | 66 ++++++++++++------- tools/cosmovisor/internal/runner.go | 6 +- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index d7a4830c8f34..389fcf31ae33 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -95,31 +95,51 @@ func TestMockChain(t *testing.T) { }, }.Setup(t) - var callbackQueue []func() + addManualUpgrade1 := func() { + time.Sleep(2 * time.Second) // wait for startup + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{ + "add-upgrade", + "manual20", + filepath.Join(mockchainDir, "manual-upgrades", "manual20"), + "--upgrade-height", + "20", + "--cosmovisor-config", + cfgFile, + }) + rootCmd.SetOut(os.Stdout) + rootCmd.SetErr(os.Stderr) + require.NoError(t, rootCmd.Execute()) + } + + addManualUpgrade2 := func() { + // TODO switch to batch upgrade at 10 and 40 + time.Sleep(2 * time.Second) // wait for startup + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{ + "add-upgrade", + "manual10", + filepath.Join(mockchainDir, "manual-upgrades", "manual10"), + "--upgrade-height", + "10", + "--cosmovisor-config", + cfgFile, + }) + rootCmd.SetOut(os.Stdout) + rootCmd.SetErr(os.Stderr) + require.NoError(t, rootCmd.Execute()) + } + + var callbackCount int testCallback := func() { - for _, cb := range callbackQueue { - cb() + callbackCount++ + switch callbackCount { + case 1: + go addManualUpgrade1() + case 2: + go addManualUpgrade2() } - callbackQueue = nil // reset for next test } - callbackQueue = append(callbackQueue, func() { - go func() { - time.Sleep(2 * time.Second) // wait for startup - rootCmd := NewRootCmd() - rootCmd.SetArgs([]string{ - "add-upgrade", - "manual20", - filepath.Join(mockchainDir, "manual-upgrades", "manual20"), - "--upgrade-height", - "20", - "--cosmovisor-config", - cfgFile, - }) - rootCmd.SetOut(os.Stdout) - rootCmd.SetErr(os.Stderr) - require.NoError(t, rootCmd.Execute()) - }() - }) var wg sync.WaitGroup wg.Add(1) go func() { @@ -133,6 +153,8 @@ func TestMockChain(t *testing.T) { }() wg.Wait() + require.Equal(t, 6, callbackCount) + // TODO: // - [x] add callback on restart for checking state // - [ ] add manual upgrade (manual20) at height 20 diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index bc205368794a..07c4dfc00739 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -121,7 +121,11 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e // shutdown, no halt height set return ErrRestartNeeded{} } else { - // TODO check if this would change the halt height + // restart if we need to change the halt height based on the upgrade + firstUpgrade := manualUpgrades.FirstUpgrade() + if uint64(firstUpgrade.Height) < haltHeight { + return ErrRestartNeeded{} + } } case err := <-processRunner.Done(): // TODO handle process exit From f0dac991441abc5e1b72fef3c8ac957a64b35f64 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 4 Jun 2025 15:55:21 -0400 Subject: [PATCH 042/115] full test passes --- .../cosmovisor/cmd/cosmovisor/mockchain_test.go | 16 +++++++++++++--- tools/cosmovisor/internal/runner.go | 13 +++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 389fcf31ae33..ba8ba869900c 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -130,14 +130,24 @@ func TestMockChain(t *testing.T) { require.NoError(t, rootCmd.Execute()) } + execCtx, cancel := context.WithCancel(context.Background()) + var callbackCount int testCallback := func() { callbackCount++ + t.Logf("Test callback called for the %dth time", callbackCount) switch callbackCount { + // first startup case 1: + // add one manual upgrade go addManualUpgrade1() + // first restart once we've add the first manual upgrade case 2: + // add a second batch of manual upgrades go addManualUpgrade2() + case 8: + // we've gotten to the test end so gracefully shutdown + cancel() } } var wg sync.WaitGroup @@ -147,13 +157,13 @@ func TestMockChain(t *testing.T) { rootCmd.SetArgs([]string{"run", "--home", mockchainDir, "--cosmovisor-config", cfgFile}) rootCmd.SetOut(os.Stdout) rootCmd.SetErr(os.Stderr) - ctx := internal.WithTestCallback(context.Background(), testCallback) - require.NoError(t, rootCmd.ExecuteContext(ctx)) + execCtx = internal.WithTestCallback(execCtx, testCallback) + require.NoError(t, rootCmd.ExecuteContext(execCtx)) wg.Done() }() wg.Wait() - require.Equal(t, 6, callbackCount) + require.Equal(t, 8, callbackCount) // TODO: // - [x] add callback on restart for checking state diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 07c4dfc00739..c6bf344a3ac2 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -61,13 +61,18 @@ func (r Runner) Start(ctx context.Context, args []string) error { var restartNeeded ErrRestartNeeded if ok := errors.As(err, &restartNeeded); ok { r.logger.Info("Restart needed") + } else if errors.Is(err, errDone) { + return nil } else { return err } + } } } +var errDone = errors.New("done") + func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) error { dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.cfg.UpgradeInfoDir(), []string{ r.cfg.UpgradeInfoFilePath(), @@ -77,8 +82,12 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e r.logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) } + // keep the original context for cancellation detection + parentCtx := ctx + // create child context for controlling watchers ctx, cancel := context.WithCancel(ctx) defer cancel() + upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker("http://localhost:8080/block") @@ -105,6 +114,10 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e correctHeightConfirmed := false for { select { + // listen to the parent context's cancellation + case <-parentCtx.Done(): + r.logger.Info("Parent context cancelled, shutting down") + return errDone case _, ok := <-upgradePlanWatcher.Updated(): // TODO check skip upgrade heights?? (although not sure why we need this as the node should not emit an upgrade plan if skip heights is enabled) if !ok { From 100f756b047411bdda3f4994ea4eaff0e5ad9d05 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 4 Jun 2025 18:04:48 -0400 Subject: [PATCH 043/115] WIP on more test conditions --- .../cmd/cosmovisor/mockchain_test.go | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index ba8ba869900c..46d94c6ad07f 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -80,6 +80,11 @@ func (m MockChainSetup) Setup(t *testing.T) (string, string) { } func TestMockChain(t *testing.T) { + pollInterval := time.Millisecond * 500 + cfg := &cosmovisor.Config{ + PollInterval: pollInterval, + RestartAfterUpgrade: true, + } mockchainDir, cfgFile := MockChainSetup{ Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", GovUpgrades: map[string]string{ @@ -89,14 +94,11 @@ func TestMockChain(t *testing.T) { "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", "manual20": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", }, - Config: &cosmovisor.Config{ - PollInterval: time.Second, - RestartAfterUpgrade: true, - }, + Config: cfg, }.Setup(t) addManualUpgrade1 := func() { - time.Sleep(2 * time.Second) // wait for startup + time.Sleep(pollInterval * 2) // wait for startup rootCmd := NewRootCmd() rootCmd.SetArgs([]string{ "add-upgrade", @@ -131,20 +133,42 @@ func TestMockChain(t *testing.T) { } execCtx, cancel := context.WithCancel(context.Background()) + defer cancel() // always cancel the context to make sure the sub-process shuts down var callbackCount int testCallback := func() { callbackCount++ t.Logf("Test callback called for the %dth time", callbackCount) + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) switch callbackCount { // first startup case 1: + // we should be starting with the genesis binary + require.Contains(t, currentBin, "genesis") // add one manual upgrade go addManualUpgrade1() // first restart once we've add the first manual upgrade case 2: + // ensure that the binary is the genesis binary + require.Contains(t, currentBin, "genesis") // add a second batch of manual upgrades go addManualUpgrade2() + case 3: + // ensure that the binary is still the genesis binary, we've just added another upgrade + require.Contains(t, currentBin, "genesis") + case 4: + // we're just doing the upgrade now, so still on genesis + require.Contains(t, currentBin, "genesis") + case 5: + // now we should be on manual10 right before the next upgrade + require.Contains(t, currentBin, "manual10") + case 6: + // now we should be on manual20 right before the next upgrade + require.Contains(t, currentBin, "manual20") + case 7: + // now we should be on gov1 right before the next upgrade + require.Contains(t, currentBin, "gov1") case 8: // we've gotten to the test end so gracefully shutdown cancel() From 68644ad977bb09f9d6ad9b0d44cfc3c7e9ebc147 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 5 Jun 2025 12:35:56 -0400 Subject: [PATCH 044/115] WIP on correct batch upgrade processing --- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 31 +++++------- .../cmd/cosmovisor/batch_upgrade.go | 11 ++++- .../cmd/cosmovisor/mockchain_test.go | 31 ++++++------ tools/cosmovisor/internal/runner.go | 48 ++++++++++++++----- tools/cosmovisor/internal/upgrade.go | 3 +- .../internal/watchers/http_block.go | 1 + tools/cosmovisor/internal/watchers/sniff.go | 1 - tools/cosmovisor/manual.go | 6 +++ 8 files changed, 82 insertions(+), 50 deletions(-) delete mode 100644 tools/cosmovisor/internal/watchers/sniff.go diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index a873d43087f9..0c09db655de9 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -29,7 +29,7 @@ func NewAddUpgradeCmd() *cobra.Command { // addUpgrade adds upgrade info to manifest // TODO batch-upgrade and add-upgrade should write to the same batch file -func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath string) error { +func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath string) (*upgradetypes.Plan, error) { logger := cfg.Logger(os.Stdout) if !cfg.DisableRecase { @@ -38,52 +38,44 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade if _, err := os.Stat(executablePath); err != nil { if os.IsNotExist(err) { - return fmt.Errorf("invalid executable path: %w", err) + return nil, fmt.Errorf("invalid executable path: %w", err) } - return fmt.Errorf("failed to load executable path: %w", err) + return nil, fmt.Errorf("failed to load executable path: %w", err) } // create upgrade dir upgradeLocation := cfg.UpgradeDir(upgradeName) if err := os.MkdirAll(path.Join(upgradeLocation, "bin"), 0o755); err != nil { - return fmt.Errorf("failed to create upgrade directory: %w", err) + return nil, fmt.Errorf("failed to create upgrade directory: %w", err) } // copy binary to upgrade dir executableData, err := os.ReadFile(executablePath) if err != nil { - return fmt.Errorf("failed to read binary: %w", err) + return nil, fmt.Errorf("failed to read binary: %w", err) } if err := saveOrAbort(cfg.UpgradeBin(upgradeName), executableData, force); err != nil { - return err + return nil, err } logger.Info(fmt.Sprintf("Using %s for %s upgrade", executablePath, upgradeName)) logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) + var plan *upgradetypes.Plan if upgradeHeight > 0 { - plan := &upgradetypes.Plan{ + plan = &upgradetypes.Plan{ Name: upgradeName, Height: upgradeHeight, } - if err := plan.ValidateBasic(); err != nil { - panic(fmt.Errorf("invalid manual upgrade plan: %w", err)) - } - - // TODO only do this once for a whole batch of upgrades - if err := cfg.AddManualUpgrades(force, plan); err != nil { - panic(fmt.Errorf("failed to add manual upgrade: %w", err)) - } - - logger.Info(fmt.Sprintf("added manual upgrade, node will be set to halt at height %d, and binary for upgrade %q will be activated", upgradeHeight, upgradeName)) } - return nil + return plan, nil } // GetConfig returns a Config using passed-in flag +// TODO we should make sure getConfigFromCmd gets used by call commands, it seems like some commands do this differently func getConfigFromCmd(cmd *cobra.Command) (*cosmovisor.Config, error) { configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig) if err != nil { @@ -116,7 +108,8 @@ func addUpgradeCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get upgrade-height flag: %w", err) } - return addUpgrade(cfg, force, upgradeHeight, upgradeName, executablePath) + plan, err := addUpgrade(cfg, force, upgradeHeight, upgradeName, executablePath) + return cfg.AddManualUpgrades(force, plan) } // saveOrAbort saves data to path or aborts if file exists and force is false diff --git a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go index 1baa4a81e4ac..acb4151e9504 100644 --- a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) func NewBatchAddUpgradeCmd() *cobra.Command { @@ -70,6 +71,7 @@ func addBatchUpgrade(cmd *cobra.Command, _ []string) error { // processUpgradeList takes in a list of upgrades and creates a batch upgrade file func processUpgradeList(cfg *cosmovisor.Config, upgradeList [][]string) error { + var upgradePlans []*upgradetypes.Plan for i, upgrade := range upgradeList { if len(upgrade) != 3 { return fmt.Errorf("argument at position %d (%s) is invalid", i, upgrade) @@ -82,11 +84,16 @@ func processUpgradeList(cfg *cosmovisor.Config, upgradeList [][]string) error { } // we use the same logic as the add-upgrade command here, appending to any existing manual upgrade data - if err := addUpgrade(cfg, true, upgradeHeight, upgradeName, upgradePath); err != nil { + plan, err := addUpgrade(cfg, true, upgradeHeight, upgradeName, upgradePath) + if err != nil { return err } + if plan != nil { + upgradePlans = append(upgradePlans, plan) + } } - return nil + + return cfg.AddManualUpgrades(true, upgradePlans...) } // processUpgradeFile takes in a CSV batch upgrade file, parses it and calls processUpgradeList diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 46d94c6ad07f..8bc67e8a9e7c 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -88,11 +88,12 @@ func TestMockChain(t *testing.T) { mockchainDir, cfgFile := MockChainSetup{ Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", GovUpgrades: map[string]string{ - "gov1": "--block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":40}'", + "gov1": "--block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":50}'", }, ManualUpgrades: map[string]string{ "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", "manual20": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", + "manual40": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":50}'", }, Config: cfg, }.Setup(t) @@ -115,15 +116,16 @@ func TestMockChain(t *testing.T) { } addManualUpgrade2 := func() { - // TODO switch to batch upgrade at 10 and 40 + batchInfo := fmt.Sprintf(`manual10:%s:10,manual40:%s:40`, + filepath.Join(mockchainDir, "manual-upgrades", "manual10"), + filepath.Join(mockchainDir, "manual-upgrades", "manual40"), + ) time.Sleep(2 * time.Second) // wait for startup rootCmd := NewRootCmd() rootCmd.SetArgs([]string{ - "add-upgrade", - "manual10", - filepath.Join(mockchainDir, "manual-upgrades", "manual10"), - "--upgrade-height", - "10", + "add-batch-upgrade", + "--upgrade-list", + batchInfo, "--cosmovisor-config", cfgFile, }) @@ -162,15 +164,15 @@ func TestMockChain(t *testing.T) { require.Contains(t, currentBin, "genesis") case 5: // now we should be on manual10 right before the next upgrade - require.Contains(t, currentBin, "manual10") + //require.Contains(t, currentBin, "manual10") case 6: - // now we should be on manual20 right before the next upgrade - require.Contains(t, currentBin, "manual20") + //require.Contains(t, currentBin, "manual10") case 7: - // now we should be on gov1 right before the next upgrade - require.Contains(t, currentBin, "gov1") + //require.Contains(t, currentBin, "manual20") case 8: - // we've gotten to the test end so gracefully shutdown + //require.Contains(t, currentBin, "manual20") + case 9: + //require.Contains(t, currentBin, "gov1") cancel() } } @@ -187,7 +189,8 @@ func TestMockChain(t *testing.T) { }() wg.Wait() - require.Equal(t, 8, callbackCount) + // TODO + //require.Equal(t, 9, callbackCount) // TODO: // - [x] add callback on restart for checking state diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index c6bf344a3ac2..57ab0cfb1337 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -32,8 +32,7 @@ func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) Runn } func (r Runner) Start(ctx context.Context, args []string) error { - // TODO handle cases where daemon shuts down without an upgrade, either have a retry count of fail in that case ideally backoff retry - startsWithoutUpgrade := 0 + // TODO handle cases where daemon shuts down without an upgrade or change to halt height, either have a retry count of fail in that case ideally backoff retry for { if testCallback := GetTestCallback(ctx); testCallback != nil { testCallback() @@ -48,13 +47,6 @@ func (r Runner) Start(ctx context.Context, args []string) error { r.logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") return nil } - startsWithoutUpgrade = 0 - } else { - // TODO check that the actual height is meaningfully progressing and we are not stuck in some manual upgrade loop where halt-height keeps getting set at the same height - if startsWithoutUpgrade >= 5 { - return fmt.Errorf("process restarted %d times without an upgrade, exiting", startsWithoutUpgrade) - } - startsWithoutUpgrade++ } err = r.RunOnce(ctx, args, haltHeight) if err != nil { @@ -132,11 +124,19 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e r.logger.Info("Received updates to upgrade-info.json.batch") if haltHeight == 0 && len(manualUpgrades) > 0 { // shutdown, no halt height set + r.logger.Info("No halt height set, but manual upgrades found, restarting process") return ErrRestartNeeded{} } else { // restart if we need to change the halt height based on the upgrade firstUpgrade := manualUpgrades.FirstUpgrade() + if firstUpgrade == nil { + // if we have no longer have an upgrade then we need to remove halt height + r.logger.Info("No upgrade found, removing halt height") + return ErrRestartNeeded{} + } if uint64(firstUpgrade.Height) < haltHeight { + // if we have an earlier halt height then we need to change the halt height + r.logger.Info("Earlier manual upgrade found, changing halt height", "current_halt_height", haltHeight, "needed_halt_height", firstUpgrade.Height) return ErrRestartNeeded{} } } @@ -147,12 +147,34 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e // TODO: case actualHeight := <-heightWatcher.Updated(): r.logger.Warn("Got height update from watcher", "height", actualHeight) + if haltHeight == 0 { + // we don't have a halt height, so we don't care to check anything about the actual height + continue + } if !correctHeightConfirmed { - // TODO read manual upgrade batch and check if we'd still be at the correct halt height - correctHeightConfirmed = true + // read manual upgrade batch and check if we'd still be at the correct halt height + manualUpgrades, err := r.cfg.ReadManualUpgrades() + if err != nil { + r.logger.Warn("Failed to read manual upgrades", "error", err) + continue + } + firstUpgrade := manualUpgrades.FirstUpgrade() + if firstUpgrade == nil { + // no upgrade found, so we shouldn't have a halt height + r.logger.Warn("No upgrade found, but halt height is set, removing halt height. This is unexpected because we didn't receive an update to upgrade-info.json.batch") + return ErrRestartNeeded{} + } + if uint64(firstUpgrade.Height) == haltHeight { + correctHeightConfirmed = true + } else { + // we're at the wrong halt height so we need to restart + r.logger.Info("We're at a different height expected, so we need to set a different halt height", "current_halt_height", haltHeight, "needed_halt_height", firstUpgrade.Height) + return ErrRestartNeeded{} + } } - // signal an upgrade if we have a halt height and we are at or past it - if haltHeight > 0 && actualHeight >= haltHeight { + // signal a restart if we're at or past the halt height + if actualHeight >= haltHeight { + r.logger.Info("Reached halt height, restarting process for upgrade") return ErrRestartNeeded{} } // TODO error channels diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrade.go index d485b761a02f..68188aaa1e41 100644 --- a/tools/cosmovisor/internal/upgrade.go +++ b/tools/cosmovisor/internal/upgrade.go @@ -50,9 +50,10 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint return false, 0, err } err = cfg.DeleteManualUpgradeAtHeight(haltHeight) + // TODO this is now the wrong halt-height return true, haltHeight, err } else if lastKnownHeight > haltHeight { - return false, haltHeight, fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d") + return false, haltHeight, fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d", manualUpgrade.Name, manualUpgrade.Height, lastKnownHeight) } logger.Info("Found pending manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) return false, haltHeight, nil diff --git a/tools/cosmovisor/internal/watchers/http_block.go b/tools/cosmovisor/internal/watchers/http_block.go index 8bf187e6f953..9fcdc3e917b1 100644 --- a/tools/cosmovisor/internal/watchers/http_block.go +++ b/tools/cosmovisor/internal/watchers/http_block.go @@ -9,6 +9,7 @@ import ( ) func NewHTTPRPCBLockChecker(url string) HeightChecker { + // TODO we want to include the ability to sniff for /block or /v1/block return httpRPCBlockChecker{ url: url, } diff --git a/tools/cosmovisor/internal/watchers/sniff.go b/tools/cosmovisor/internal/watchers/sniff.go deleted file mode 100644 index 38643bc0295e..000000000000 --- a/tools/cosmovisor/internal/watchers/sniff.go +++ /dev/null @@ -1 +0,0 @@ -package watchers diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 2c761582869c..e0583f05b7c6 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -40,6 +40,9 @@ func (cfg *Config) ParseManualUpgrades(bz []byte) (ManualUpgradeBatch, error) { // If an upgrade with the same name already exists, it will only be overwritten if forceOverwrite is true, // otherwise an error will be returned. func (cfg *Config) AddManualUpgrades(forceOverwrite bool, plans ...*upgradetypes.Plan) error { + if len(plans) == 0 { + return nil + } // TODO only allow plans that are AFTER the last known height existing, err := cfg.ReadManualUpgrades() if err != nil { @@ -51,6 +54,9 @@ func (cfg *Config) AddManualUpgrades(forceOverwrite bool, plans ...*upgradetypes planMap[existingPlan.Name] = existingPlan } for _, plan := range plans { + if err := plan.ValidateBasic(); err != nil { + return fmt.Errorf("invalid upgrade plan %s: %w", plan.Name, err) + } if _, ok := planMap[plan.Name]; ok { if !forceOverwrite { return fmt.Errorf("upgrade with name %s already exists", plan.Name) From 0b87586cb15c528abc1bdb0728271afec8e789f0 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 5 Jun 2025 17:49:30 -0400 Subject: [PATCH 045/115] most upgrade tests working, shutdown isn't --- .../cmd/cosmovisor/mockchain_test.go | 35 ++++++++++--------- tools/cosmovisor/cmd/mock_node/main.go | 2 +- tools/cosmovisor/internal/runner.go | 32 +++++++++++++---- .../internal/{upgrade.go => upgrader.go} | 30 +++++++++------- 4 files changed, 63 insertions(+), 36 deletions(-) rename tools/cosmovisor/internal/{upgrade.go => upgrader.go} (90%) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 8bc67e8a9e7c..64c2942299eb 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -144,37 +144,41 @@ func TestMockChain(t *testing.T) { currentBin, err := cfg.CurrentBin() require.NoError(t, err) switch callbackCount { - // first startup case 1: + // first startup // we should be starting with the genesis binary require.Contains(t, currentBin, "genesis") // add one manual upgrade go addManualUpgrade1() - // first restart once we've add the first manual upgrade case 2: - // ensure that the binary is the genesis binary + // first restart once we've add the first manual upgrade + // ensure that the binary is still the genesis binary require.Contains(t, currentBin, "genesis") // add a second batch of manual upgrades go addManualUpgrade2() case 3: - // ensure that the binary is still the genesis binary, we've just added another upgrade + // next restart after adding more manual upgrades + // ensure that the binary is still the genesis binary require.Contains(t, currentBin, "genesis") case 4: - // we're just doing the upgrade now, so still on genesis - require.Contains(t, currentBin, "genesis") + // should have upgraded to manual10 + require.Contains(t, currentBin, "manual10") case 5: - // now we should be on manual10 right before the next upgrade - //require.Contains(t, currentBin, "manual10") + // should have upgraded to manual20 + require.Contains(t, currentBin, "manual20") case 6: - //require.Contains(t, currentBin, "manual10") + // should have upgraded to gov1 + require.Contains(t, currentBin, "gov1") case 7: - //require.Contains(t, currentBin, "manual20") - case 8: - //require.Contains(t, currentBin, "manual20") - case 9: - //require.Contains(t, currentBin, "gov1") + // should have upgraded to manual40 + require.Contains(t, currentBin, "manual40") + // this is the end of our test so we shutdown here cancel() } + //cmd := exec.Command("tree", "-a") + //cmd.Dir = mockchainDir + //cmd.Stdout = os.Stdout + //require.NoError(t, cmd.Run()) } var wg sync.WaitGroup wg.Add(1) @@ -189,8 +193,7 @@ func TestMockChain(t *testing.T) { }() wg.Wait() - // TODO - //require.Equal(t, 9, callbackCount) + require.Equal(t, 7, callbackCount) // TODO: // - [x] add callback on restart for checking state diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 04cd1a43a8d7..770359960ec4 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -149,7 +149,7 @@ func (n *MockNode) Run(ctx context.Context) error { } } } - if n.haltHeight > 0 { + if n.haltHeight == upgradeHeight { // if we have a halt height and we've reached it - there could be an earlier gov upgrade // this log line matches what BaseApp does when it reaches the halt height n.logger.Error(fmt.Sprintf("halt per configuration height %d", n.height)) } else if n.upgradePlan != nil { diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 57ab0cfb1337..74d66ae180e9 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -34,10 +34,7 @@ func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) Runn func (r Runner) Start(ctx context.Context, args []string) error { // TODO handle cases where daemon shuts down without an upgrade or change to halt height, either have a retry count of fail in that case ideally backoff retry for { - if testCallback := GetTestCallback(ctx); testCallback != nil { - testCallback() - } - upgraded, haltHeight, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) + upgraded, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) if err != nil { return err } @@ -48,6 +45,15 @@ func (r Runner) Start(ctx context.Context, args []string) error { return nil } } + args, haltHeight, err := r.ComputeRunArgs(args) + if err != nil { + return err + } + + if testCallback := GetTestCallback(ctx); testCallback != nil { + testCallback() + } + err = r.RunOnce(ctx, args, haltHeight) if err != nil { var restartNeeded ErrRestartNeeded @@ -65,6 +71,22 @@ func (r Runner) Start(ctx context.Context, args []string) error { var errDone = errors.New("done") +func (r Runner) ComputeRunArgs(args []string) (argsOut []string, haltHeight uint64, err error) { + argsOut = args + r.logger.Info("Checking for upgrade-info.json.batch") + manualUpgradeBatch, err := r.cfg.ReadManualUpgrades() + if err != nil { + return nil, 0, err + } + manualUpgrade := manualUpgradeBatch.FirstUpgrade() + if manualUpgrade != nil { + haltHeight = uint64(manualUpgrade.Height) + r.logger.Info("Setting --halt-height flag for manual upgrade", "halt_height", haltHeight) + argsOut = append(argsOut, fmt.Sprintf("--halt-height=%d", haltHeight)) + } + return +} + func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) error { dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.cfg.UpgradeInfoDir(), []string{ r.cfg.UpgradeInfoFilePath(), @@ -89,8 +111,6 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e }) if haltHeight > 0 { - r.logger.Info("Setting --halt-height flag for manual upgrade", "halt_height", haltHeight) - args = append(args, fmt.Sprintf("--halt-height=%d", haltHeight)) } cmd, err := r.createCmd(args) if err != nil { diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrader.go similarity index 90% rename from tools/cosmovisor/internal/upgrade.go rename to tools/cosmovisor/internal/upgrader.go index 68188aaa1e41..961b203ba311 100644 --- a/tools/cosmovisor/internal/upgrade.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -20,20 +20,20 @@ type UpgradeCheckResult struct { HaltHeight uint64 } -func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint64) (upgraded bool, haltHeight uint64, err error) { +func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint64) (upgraded bool, err error) { logger.Info("Checking for upgrade-info.json") if upgradePlan, err := cfg.UpgradeInfo(); err == nil { upgrader := NewUpgrader(cfg, logger, upgradePlan, false) err := upgrader.DoUpgrade() if err != nil { - return false, 0, err + return false, err } - return true, 0, nil + return true, nil } logger.Info("Checking for upgrade-info.json.batch") manualUpgradeBatch, err := cfg.ReadManualUpgrades() if err != nil { - return false, 0, err + return false, err } logger.Info("Checking last known height") lastKnownHeight := knownHeight @@ -41,24 +41,21 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint lastKnownHeight = cfg.ReadLastKnownHeight() } if manualUpgrade := manualUpgradeBatch.FirstUpgrade(); manualUpgrade != nil { - haltHeight = uint64(manualUpgrade.Height) + haltHeight := uint64(manualUpgrade.Height) if lastKnownHeight == haltHeight { logger.Info("At manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) upgrader := NewUpgrader(cfg, logger, *manualUpgrade, true) err := upgrader.DoUpgrade() if err != nil { - return false, 0, err + return false, err } err = cfg.DeleteManualUpgradeAtHeight(haltHeight) - // TODO this is now the wrong halt-height - return true, haltHeight, err + return true, err } else if lastKnownHeight > haltHeight { - return false, haltHeight, fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d", manualUpgrade.Name, manualUpgrade.Height, lastKnownHeight) + return false, fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d", manualUpgrade.Name, manualUpgrade.Height, lastKnownHeight) } - logger.Info("Found pending manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) - return false, haltHeight, nil } - return false, 0, nil + return false, nil } type Upgrader struct { @@ -108,11 +105,18 @@ func (u *Upgrader) DoUpgrade() error { } if u.isManualUpgrade { - u.logger.Info("Removing completed manual upgrade plan", "height", u.upgradePlan.Height) + u.logger.Info("Removing completed manual upgrade plan", "height", u.upgradePlan.Height, "name", u.upgradePlan.Name) err := u.cfg.RemoveManualUpgrade(u.upgradePlan.Height) if err != nil { return fmt.Errorf("failed to remove manual upgrade at height %d: %w", u.upgradePlan.Height, err) } + } else { + u.logger.Info("Removing completed upgrade plan", "height", u.upgradePlan.Height, "name", u.upgradePlan.Name) + file := u.cfg.UpgradeInfoFilePath() + err := os.Remove(file) + if err != nil { + return fmt.Errorf("failed to remove upgrade-info.json: %w", err) + } } return nil From 427c5268d3950ef38ffc81bf93eb6c589bed9f47 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 5 Jun 2025 18:47:28 -0400 Subject: [PATCH 046/115] shutdown works with some sleep time --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 64c2942299eb..c825d35da739 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -172,13 +172,12 @@ func TestMockChain(t *testing.T) { case 7: // should have upgraded to manual40 require.Contains(t, currentBin, "manual40") - // this is the end of our test so we shutdown here - cancel() + // this is the end of our test so we shutdown after a bit here + go func() { + time.Sleep(pollInterval * 2) + cancel() + }() } - //cmd := exec.Command("tree", "-a") - //cmd.Dir = mockchainDir - //cmd.Stdout = os.Stdout - //require.NoError(t, cmd.Run()) } var wg sync.WaitGroup wg.Add(1) From bd0c895a2c90c66d85229b0a8d9bf39f6776d03b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 6 Jun 2025 10:47:34 -0400 Subject: [PATCH 047/115] integrate backoff manager --- tools/cosmovisor/internal/backoff.go | 47 ++++++++++++++++ tools/cosmovisor/internal/runner.go | 82 ++++++++++++++++------------ 2 files changed, 95 insertions(+), 34 deletions(-) create mode 100644 tools/cosmovisor/internal/backoff.go diff --git a/tools/cosmovisor/internal/backoff.go b/tools/cosmovisor/internal/backoff.go new file mode 100644 index 000000000000..ec39d23e6d5f --- /dev/null +++ b/tools/cosmovisor/internal/backoff.go @@ -0,0 +1,47 @@ +package internal + +import ( + "time" + + "github.com/cenkalti/backoff/v5" +) + +type RetryBackoffManager struct { + lastCmd string + lastArgs []string + backoff backoff.BackOff +} + +// NewRetryBackoffManager creates a new RetryBackoffManager instance. +func NewRetryBackoffManager() *RetryBackoffManager { + backoff := backoff.NewExponentialBackOff() + return &RetryBackoffManager{ + backoff: backoff, + } +} + +func (r *RetryBackoffManager) BeforeRun(cmd string, args []string) error { + reset := false + // we reset the backoff if the command or its arguments have changed + if r.lastCmd != cmd || len(r.lastArgs) != len(args) { + reset = true + } else { + n := min(len(r.lastArgs), len(args)) + for i := 0; i < n; i++ { + if r.lastArgs[i] != args[i] { + reset = true + break + } + } + } + if reset { + // if the command or arguments have changed, we reset the backoff and store the new command and arguments + r.backoff.Reset() + r.lastCmd = cmd + r.lastArgs = args + } else { + // if the command and arguments are the same, we wait for the next backoff interval + time.Sleep(r.backoff.NextBackOff()) + } + return nil +} diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 74d66ae180e9..346d00252e8f 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -32,12 +32,14 @@ func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) Runn } func (r Runner) Start(ctx context.Context, args []string) error { - // TODO handle cases where daemon shuts down without an upgrade or change to halt height, either have a retry count of fail in that case ideally backoff retry + retryMgr := NewRetryBackoffManager() for { + // First we check if we need to upgrade and if we do we perform the upgrade upgraded, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) if err != nil { return err } + // If we upgraded, we need to restart the process, but some configurations do not allow automatic restarts if upgraded { r.logger.Info("Upgrade completed, restarting process") if !r.cfg.RestartAfterUpgrade { @@ -45,17 +47,41 @@ func (r Runner) Start(ctx context.Context, args []string) error { return nil } } - args, haltHeight, err := r.ComputeRunArgs(args) + // Now we compute the command to run and figure out the halt height if needed + cmd, haltHeight, err := r.ComputeRunPlan(args) if err != nil { return err } + // Usually restarts should be due to either: + // 1. an upgrade that requires a restart + // 2. a change in the halt height due to a new manual upgrade plan + // There are also cases where an app could just shut down due to some error. + // If we're in that sort of situation, we want to retry running the command, but + // we apply a backoff strategy to avoid hammering the process in case of repeated failures. + // We pass the current command and args to the retry manager so it can check whether + // the command or its arguments have changed (e.g. if the binary was updated or the halt height changed), + // or if we're just in some sort of error restart loop. + // TODO add tests for this behavior + if err := retryMgr.BeforeRun(cmd.Path, cmd.Args); err != nil { + return err + } + + // In order to make in process testing feasible, we allow a test callback to be set + // and we call it here right before running the process. + // Without this it would be much harder to test the cosmovisor runner in a controlled but realistic scenario. if testCallback := GetTestCallback(ctx); testCallback != nil { testCallback() } - err = r.RunOnce(ctx, args, haltHeight) + // Now we actually run the process + err = r.RunProcess(ctx, cmd, haltHeight) if err != nil { + // There are three types of errors we're checking for here: + // 1. ErrRestartNeeded: this is a custom error that is returned whenever the run loop detects that a restart is needed. + // 2. errDone: this is a sentinel error that indicates that the cosmovisor process itself should be stopped gracefully. + // 3. Any other error: this is an unexpected error that should be logged and returned, causing cosmovisor to exit + // TODO is it right for cosmovisor to exit on any other error (basically a non-zero return code)? Maybe we should just log it and continue? var restartNeeded ErrRestartNeeded if ok := errors.As(err, &restartNeeded); ok { r.logger.Info("Restart needed") @@ -64,15 +90,26 @@ func (r Runner) Start(ctx context.Context, args []string) error { } else { return err } - } } } var errDone = errors.New("done") -func (r Runner) ComputeRunArgs(args []string) (argsOut []string, haltHeight uint64, err error) { - argsOut = args +func (r Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, err error) { + bin, err := r.cfg.CurrentBin() + if err != nil { + return nil, 0, fmt.Errorf("error creating symlink to genesis: %w", err) + } + + if err := plan.EnsureBinary(bin); err != nil { + return nil, 0, fmt.Errorf("current binary is invalid: %w", err) + } + + cmd = exec.Command(bin, args...) + cmd.Stdin = r.runCfg.StdIn + cmd.Stdout = r.runCfg.StdOut + cmd.Stderr = r.runCfg.StdErr r.logger.Info("Checking for upgrade-info.json.batch") manualUpgradeBatch, err := r.cfg.ReadManualUpgrades() if err != nil { @@ -82,12 +119,12 @@ func (r Runner) ComputeRunArgs(args []string) (argsOut []string, haltHeight uint if manualUpgrade != nil { haltHeight = uint64(manualUpgrade.Height) r.logger.Info("Setting --halt-height flag for manual upgrade", "halt_height", haltHeight) - argsOut = append(argsOut, fmt.Sprintf("--halt-height=%d", haltHeight)) + cmd.Args = append(cmd.Args, fmt.Sprintf("--halt-height=%d", haltHeight)) } return } -func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) error { +func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64) error { dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.cfg.UpgradeInfoDir(), []string{ r.cfg.UpgradeInfoFilePath(), r.cfg.UpgradeInfoBatchFilePath(), @@ -110,16 +147,11 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e return r.cfg.WriteLastKnownHeight(height) }) - if haltHeight > 0 { - } - cmd, err := r.createCmd(args) - if err != nil { - return err - } + r.logger.Info("Starting process %s with args %v", cmd.Path, cmd.Args) processRunner := RunProcess(cmd) defer func() { - // TODO always check height before shutting down - //_, _ = heightChecker.ReadNow() + // TODO always check for the latest block height before shutting down + _, _ = heightChecker.GetLatestBlockHeight() _ = processRunner.Shutdown(r.cfg.ShutdownGrace) }() @@ -202,24 +234,6 @@ func (r Runner) RunOnce(ctx context.Context, args []string, haltHeight uint64) e } } -func (r Runner) createCmd(args []string) (*exec.Cmd, error) { - bin, err := r.cfg.CurrentBin() - if err != nil { - return nil, fmt.Errorf("error creating symlink to genesis: %w", err) - } - - if err := plan.EnsureBinary(bin); err != nil { - return nil, fmt.Errorf("current binary is invalid: %w", err) - } - - r.logger.Info("running app", "path", bin, "args", args) - cmd := exec.Command(bin, args...) - cmd.Stdin = r.runCfg.StdIn - cmd.Stdout = r.runCfg.StdOut - cmd.Stderr = r.runCfg.StdErr - return cmd, nil -} - // RunConfig defines the configuration for running a command type RunConfig struct { StdIn io.Reader From 5a03e7c97a1d52f7c9df339f3d1d0eb0f51d6be1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 6 Jun 2025 10:52:16 -0400 Subject: [PATCH 048/115] backoff logging --- tools/cosmovisor/internal/backoff.go | 16 ++++++++++++---- tools/cosmovisor/internal/runner.go | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/cosmovisor/internal/backoff.go b/tools/cosmovisor/internal/backoff.go index ec39d23e6d5f..4ba79bbb63df 100644 --- a/tools/cosmovisor/internal/backoff.go +++ b/tools/cosmovisor/internal/backoff.go @@ -3,6 +3,7 @@ package internal import ( "time" + "cosmossdk.io/log" "github.com/cenkalti/backoff/v5" ) @@ -10,13 +11,15 @@ type RetryBackoffManager struct { lastCmd string lastArgs []string backoff backoff.BackOff + logger log.Logger } // NewRetryBackoffManager creates a new RetryBackoffManager instance. -func NewRetryBackoffManager() *RetryBackoffManager { - backoff := backoff.NewExponentialBackOff() +func NewRetryBackoffManager(logger log.Logger) *RetryBackoffManager { + backoffAlg := backoff.NewExponentialBackOff() return &RetryBackoffManager{ - backoff: backoff, + backoff: backoffAlg, + logger: logger, } } @@ -36,12 +39,17 @@ func (r *RetryBackoffManager) BeforeRun(cmd string, args []string) error { } if reset { // if the command or arguments have changed, we reset the backoff and store the new command and arguments + r.logger.Info("Resetting backoff due to command or arguments change") r.backoff.Reset() r.lastCmd = cmd r.lastArgs = args } else { // if the command and arguments are the same, we wait for the next backoff interval - time.Sleep(r.backoff.NextBackOff()) + duration := r.backoff.NextBackOff() + r.logger.Info("Applying backoff before restarting command", + "backoff", duration) + time.Sleep(duration) + r.logger.Info("Backoff time elapsed, restarting ") } return nil } diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 346d00252e8f..91d3d42c9fc8 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -32,7 +32,7 @@ func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) Runn } func (r Runner) Start(ctx context.Context, args []string) error { - retryMgr := NewRetryBackoffManager() + retryMgr := NewRetryBackoffManager(r.logger) for { // First we check if we need to upgrade and if we do we perform the upgrade upgraded, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) @@ -74,6 +74,8 @@ func (r Runner) Start(ctx context.Context, args []string) error { testCallback() } + // TODO should restart delay go here? or should it be handled when upgrading + // Now we actually run the process err = r.RunProcess(ctx, cmd, haltHeight) if err != nil { From 638c317772ba2d9be55d6fe60c45d27a1524d9a9 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 6 Jun 2025 14:06:47 -0400 Subject: [PATCH 049/115] delete refactored code, fix tests --- tools/cosmovisor/pre_upgrade.go | 103 ------- tools/cosmovisor/process.go | 447 ------------------------------- tools/cosmovisor/scanner.go | 113 -------- tools/cosmovisor/scanner_test.go | 20 +- 4 files changed, 16 insertions(+), 667 deletions(-) delete mode 100644 tools/cosmovisor/pre_upgrade.go delete mode 100644 tools/cosmovisor/process.go delete mode 100644 tools/cosmovisor/scanner.go diff --git a/tools/cosmovisor/pre_upgrade.go b/tools/cosmovisor/pre_upgrade.go deleted file mode 100644 index 741eb06c22cf..000000000000 --- a/tools/cosmovisor/pre_upgrade.go +++ /dev/null @@ -1,103 +0,0 @@ -package cosmovisor - -import ( - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" -) - -// doCustomPreUpgrade executes the custom preupgrade script if provided. -func (l Launcher) doCustomPreUpgrade() error { - if l.cfg.CustomPreUpgrade == "" { - return nil - } - - // check if preupgradeFile is executable file - preupgradeFile := filepath.Join(l.cfg.Home, "cosmovisor", l.cfg.CustomPreUpgrade) - l.logger.Info("looking for COSMOVISOR_CUSTOM_PREUPGRADE file", "file", preupgradeFile) - info, err := os.Stat(preupgradeFile) - if err != nil { - l.logger.Error("COSMOVISOR_CUSTOM_PREUPGRADE file missing", "file", preupgradeFile) - return err - } - if !info.Mode().IsRegular() { - _, f := filepath.Split(preupgradeFile) - return fmt.Errorf("COSMOVISOR_CUSTOM_PREUPGRADE: %s is not a regular file", f) - } - - // Set the execute bit for only the current user - // Given: Current user - Group - Everyone - // 0o RWX - RWX - RWX - oldMode := info.Mode().Perm() - newMode := oldMode | 0o100 - if oldMode != newMode { - if err := os.Chmod(preupgradeFile, newMode); err != nil { - l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") - return errors.New("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") - } - } - - // Run preupgradeFile - cmd := exec.Command(preupgradeFile, l.upgradePlan.Name, fmt.Sprintf("%d", l.upgradePlan.Height)) - cmd.Dir = l.cfg.Home - result, err := cmd.Output() - if err != nil { - return err - } - - l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE result", "command", preupgradeFile, "argv1", l.upgradePlan.Name, "argv2", fmt.Sprintf("%d", l.upgradePlan.Height), "result", result) - - return nil -} - -// doPreUpgrade runs the pre-upgrade command defined by the application and handles respective error codes. -// cfg contains the cosmovisor config from env var. -// doPreUpgrade runs the new APP binary in order to process the upgrade (post-upgrade for cosmovisor). -func (l *Launcher) doPreUpgrade() error { - counter := 0 - for { - if counter > l.cfg.PreUpgradeMaxRetries { - return fmt.Errorf("pre-upgrade command failed. reached max attempt of retries - %d", l.cfg.PreUpgradeMaxRetries) - } - - if err := l.executePreUpgradeCmd(); err != nil { - counter++ - - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - switch exitErr.ExitCode() { - case 1: - l.logger.Info("pre-upgrade command does not exist. continuing the upgrade.") - return nil - case 30: - return fmt.Errorf("pre-upgrade command failed : %w", err) - case 31: - l.logger.Error("pre-upgrade command failed. retrying", "error", err, "attempt", counter) - continue - } - } - } - - l.logger.Info("pre-upgrade successful. continuing the upgrade.") - return nil - } -} - -// executePreUpgradeCmd runs the pre-upgrade command defined by the application -// cfg contains the cosmovisor config from the env vars -func (l *Launcher) executePreUpgradeCmd() error { - bin, err := l.cfg.CurrentBin() - if err != nil { - return fmt.Errorf("error while getting current binary path: %w", err) - } - - result, err := exec.Command(bin, "pre-upgrade").Output() - if err != nil { - return err - } - - l.logger.Info("pre-upgrade result", "result", result) - return nil -} diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go deleted file mode 100644 index 63f052a5315e..000000000000 --- a/tools/cosmovisor/process.go +++ /dev/null @@ -1,447 +0,0 @@ -package cosmovisor - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strings" - "syscall" - "time" - - "cosmossdk.io/log" - "github.com/otiai10/copy" - - "cosmossdk.io/tools/cosmovisor/internal/watchers" - "github.com/cosmos/cosmos-sdk/x/upgrade/plan" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" -) - -/* -Execution Flow: -* Cosmovisor started -* load manual upgrade batches -* if have manual upgrade: - * load last known height - if have last known height: - * find proper manual upgrade plan - * add --halt-height to the args before running the app - else: - * find last manual upgrade plan?? - * start the app --halt-height - * check height -* start the app -* check height, find proper manual upgrade and restart app with proper --halt-height if needed -* initialize watchers for - * on upgrade-info.json found, stop app, signal upgrade - * on upgrade-info.json.batch found, start height checking and restart app with --halt-height if needed - * if halt height signal received (by log watching and checking height), stop app, signal upgrade - * if past halt height for a manual upgrade, stop app and report error (likely not safe to upgrade) - * process exit - * if process exits without signaling an upgrade plan, we manually check for upgrade-info.json -*/ - -type Launcher struct { - logger log.Logger - cfg *Config - ctx context.Context - cancel context.CancelFunc - // upgradePlanWatcher watches for data in an upgrade-info.json created by the running node - upgradePlanWatcher watchers.Watcher[upgradetypes.Plan] - // manualUpgradesWatcher watchers for data in an upgrade-info.json.batch created by the node operator - manualUpgradesWatcher watchers.Watcher[ManualUpgradeBatch] - haltHeightWatcher watchers.Watcher[uint64] - actualHeightWatcher watchers.Watcher[uint64] - heightChecker watchers.HeightChecker - upgradePlan *upgradetypes.Plan - manualUpgrade *upgradetypes.Plan -} - -func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGQUIT, syscall.SIGTERM) - dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, cfg.UpgradeInfoDir(), []string{ - cfg.UpgradeInfoFilePath(), - cfg.UpgradeInfoBatchFilePath(), - }) - if err != nil { - logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) - } - - nodeUpgradeWatcher := initWatcher[upgradetypes.Plan](ctx, cfg, dirWatcher, cfg.UpgradeInfoFilePath(), cfg.ParseUpgradeInfo) - manualUpgradesWatcher := initWatcher[ManualUpgradeBatch](ctx, cfg, dirWatcher, cfg.UpgradeInfoBatchFilePath(), cfg.ParseManualUpgrades) - - l := Launcher{ - logger: logger, - cfg: cfg, - ctx: ctx, - cancel: cancel, - upgradePlanWatcher: nodeUpgradeWatcher, - manualUpgradesWatcher: manualUpgradesWatcher, - } - - err = l.loadManualUpgrade() - if err != nil { - return Launcher{}, fmt.Errorf("error loading manual upgrades: %w", err) - } - - return l, nil -} - -func (l *Launcher) loadManualUpgrade() error { - //manualUpgradeBatch, err := l.cfg.ReadManualUpgrades() - //if err != nil { - // return Launcher{}, err - //} - //if manualUpgradeBatch == nil || len(manualUpgradeBatch) == 0 { - // return nil - //} - // - panic("TODO") -} - -func (l *Launcher) Watch() { - errChan := joinChannels(l.upgradePlanWatcher.Errors(), - l.manualUpgradesWatcher.Errors(), - l.haltHeightWatcher.Errors(), - l.actualHeightWatcher.Errors()) - for { - select { - case <-l.ctx.Done(): - - // TODO handle cosmovisor shutdown - return - case upgradePlan := <-l.upgradePlanWatcher.Updated(): - l.upgradePlan = &upgradePlan - // TODO upgrade plan received, positive signal to perform upgrade, no additional checks needed - case <-l.manualUpgradesWatcher.Updated(): - l.logger.Info("manual upgrades watcher updated") - // TODO received new manual upgrades batch: - // must establish current node height and select the first manual upgrade after current height, if any - // if one is found, node must be restarted with --halt-height - case <-l.haltHeightWatcher.Updated(): - // TODO check against manual upgrade height - case <-l.actualHeightWatcher.Updated(): - // TODO check against manual upgrade height - case err := <-errChan: - // TODO move error handling to a separate goroutine - // for now just log errors - l.logger.Error("error in upgrade plan watcher", "error", err) - } - } -} - -func (l *Launcher) watchErrors() { - -} - -// TODO fix this with WaitGroup -func joinChannels[T any](ch ...<-chan T) <-chan T { - out := make(chan T) - go func() { - defer close(out) - for _, c := range ch { - for msg := range c { - out <- msg - } - } - }() - return out -} - -//// BatchUpgradeWatcher starts a watcher loop that swaps upgrade manifests at the correct -//// height, given the batch upgrade file. It watches the current state of the chain -//// via the websocket API. -//func BatchUpgradeWatcher(ctx context.Context, cfg *Config, logger log.Logger) { -// // load batch file in memory -// uInfos, err := ReadManualUpgrades(cfg) -// if err != nil { -// logger.Warn("failed to load batch upgrade file", "error", err) -// uInfos = []upgradetypes.Plan{} -// } -// -// watcher, err := fsnotify.NewWatcher() -// if err != nil { -// logger.Warn("failed to init watcher", "error", err) -// return -// } -// defer watcher.Close() -// err = watcher.Add(filepath.Dir(cfg.UpgradeInfoBatchFilePath())) -// if err != nil { -// logger.Warn("watcher failed to add upgrade directory", "error", err) -// return -// } -// -// var conn *grpc.ClientConn -// var grpcErr error -// -// defer func() { -// if conn != nil { -// if err := conn.Close(); err != nil { -// logger.Warn("couldn't stop gRPC client", "error", err) -// } -// } -// }() -// -// // Wait for the chain process to be ready -//pollLoop: -// for { -// select { -// case <-ctx.Done(): -// return -// default: -// conn, grpcErr = grpc.NewClient(cfg.GRPCAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) -// if grpcErr == nil { -// break pollLoop -// } -// time.Sleep(time.Second) -// } -// } -// -// client := cmtservice.NewServiceClient(conn) -// -// var prevUpgradeHeight int64 = -1 -// -// logger.Info("starting the batch watcher loop") -// for { -// select { -// case event := <-watcher.Events: -// if event.Op&(fsnotify.Write|fsnotify.Create) != 0 { -// uInfos, err = loadBatchUpgradeFile(cfg) -// if err != nil { -// logger.Warn("failed to load batch upgrade file", "error", err) -// continue -// } -// } -// case <-ctx.Done(): -// return -// default: -// if len(uInfos) == 0 { -// // prevent spending extra CPU cycles -// time.Sleep(time.Second) -// continue -// } -// resp, err := client.GetLatestBlock(ctx, &cmtservice.GetLatestBlockRequest{}) -// if err != nil { -// logger.Warn("error getting latest block", "error", err) -// time.Sleep(time.Second) -// continue -// } -// -// h := resp.SdkBlock.Header.Height -// upcomingUpgrade := uInfos[0].Height -// // replace upgrade-info and upgrade-info batch file -// if h > prevUpgradeHeight && h < upcomingUpgrade { -// jsonBytes, err := json.Marshal(uInfos[0]) -// if err != nil { -// logger.Warn("error marshaling JSON for upgrade-info.json", "error", err, "upgrade", uInfos[0]) -// continue -// } -// if err := os.WriteFile(cfg.UpgradeInfoFilePath(), jsonBytes, 0o600); err != nil { -// logger.Warn("error writing upgrade-info.json", "error", err) -// continue -// } -// uInfos = uInfos[1:] -// -// jsonBytes, err = json.Marshal(uInfos) -// if err != nil { -// logger.Warn("error marshaling JSON for upgrade-info.json.batch", "error", err, "upgrades", uInfos) -// continue -// } -// if err := os.WriteFile(cfg.UpgradeInfoBatchFilePath(), jsonBytes, 0o600); err != nil { -// logger.Warn("error writing upgrade-info.json.batch", "error", err) -// // remove the upgrade-info.json.batch file to avoid non-deterministic behavior -// err := os.Remove(cfg.UpgradeInfoBatchFilePath()) -// if err != nil && !os.IsNotExist(err) { -// logger.Warn("error removing upgrade-info.json.batch", "error", err) -// return -// } -// continue -// } -// prevUpgradeHeight = upcomingUpgrade -// } -// -// // Add a small delay to avoid hammering the gRPC endpoint -// time.Sleep(time.Second) -// } -// } -//} - -// Run launches the app in a subprocess and returns when the subprocess (app) -// exits (either when it dies, or *after* a successful upgrade.) and upgrade finished. -// Returns true if the upgrade request was detected and the upgrade process started. -func (l Launcher) Run(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) { - bin, err := l.cfg.CurrentBin() - if err != nil { - return false, fmt.Errorf("error creating symlink to genesis: %w", err) - } - - if err := plan.EnsureBinary(bin); err != nil { - return false, fmt.Errorf("current binary is invalid: %w", err) - } - - l.logger.Info("running app", "path", bin, "args", args) - cmd := exec.Command(bin, args...) - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - if err := cmd.Start(); err != nil { - return false, fmt.Errorf("launching process %s %s failed: %w", bin, strings.Join(args, " "), err) - } - - //var wg sync.WaitGroup - //wg.Add(1) - //// TODO: replace BatchUpgradeWatcher - //go func() { - // defer wg.Done() - // BatchUpgradeWatcher(ctx, l.cfg, l.logger) - //}() - - //sigs := make(chan os.Signal, 1) - //signal.Notify(sigs, syscall.SIGQUIT, syscall.SIGTERM) - //go func() { - // sig := <-sigs - // cancel() - // wg.Wait() - // if err := cmd.Process.Signal(sig); err != nil { - // l.logger.Error("terminated", "error", err, "bin", bin) - // os.Exit(1) - // } - //}() - - if needsUpdate, err := l.WaitForUpgradeOrExit(cmd); err != nil || !needsUpdate { - return false, err - } - - if !IsSkipUpgradeHeight(args, *l.upgradePlan) { - l.cfg.WaitRestartDelay() - - if err := l.doBackup(); err != nil { - return false, err - } - - if err := l.doCustomPreUpgrade(); err != nil { - return false, err - } - - if err := UpgradeBinary(l.logger, l.cfg, *l.upgradePlan); err != nil { - return false, err - } - - if err = l.doPreUpgrade(); err != nil { - return false, err - } - - return true, nil - } - - //cancel() - //wg.Wait() - - return false, nil -} - -// WaitForUpgradeOrExit checks upgrade plan file created by the app. -// When it returns, the process (app) is finished. -// -// It returns (true, nil) if an upgrade should be initiated (and we killed the process) -// It returns (false, err) if the process died by itself -// It returns (false, nil) if the process exited normally without triggering an upgrade. This is very unlikely -// to happen with "start" but may happen with short-lived commands like `simd genesis export ...` -func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { - //// TODO we shouldn't be getting any current upgrade because we're only using upgrade-info.json to receive signals from the node - //currentUpgrade, err := l.cfg.UpgradeInfo() - //if err != nil { - // // upgrade info not found do nothing - // currentUpgrade = upgradetypes.Plan{} - //} - // - cmdDone := make(chan error) - go func() { - cmdDone <- cmd.Wait() - }() - - select { - // TODO add manual-upgrades watcher - // TODO replace with upgrade-info.json watcher - case upgradePlan := <-l.upgradePlanWatcher.Updated(): - l.upgradePlan = &upgradePlan - // upgrade - kill the process and restart - l.logger.Info("daemon shutting down in an attempt to restart") - - if l.cfg.ShutdownGrace > 0 { - // Interrupt signal - l.logger.Info("sent interrupt to app, waiting for exit") - _ = cmd.Process.Signal(syscall.SIGTERM) - - // Wait app exit - psChan := make(chan *os.ProcessState) - go func() { - pstate, _ := cmd.Process.Wait() - psChan <- pstate - }() - - // Timeout and kill - select { - case <-psChan: - // Normal Exit - l.logger.Info("app exited normally") - case <-time.After(l.cfg.ShutdownGrace): - l.logger.Info("DAEMON_SHUTDOWN_GRACE exceeded, killing app") - // Kill after grace period - _ = cmd.Process.Kill() - } - } else { - // Default: Immediate app kill - _ = cmd.Process.Kill() - } - case err := <-cmdDone: - // no error -> command exits normally (eg. short command like `gaiad version`) - if err == nil { - return false, nil - } - } - return true, nil -} - -func (l Launcher) doBackup() error { - // take backup if `UNSAFE_SKIP_BACKUP` is not set. - if !l.cfg.UnsafeSkipBackup { - // check if upgrade-info.json is not empty. - var uInfo upgradetypes.Plan - upgradeInfoFile, err := os.ReadFile(l.cfg.UpgradeInfoFilePath()) - if err != nil { - return fmt.Errorf("error while reading upgrade-info.json: %w", err) - } - - if err = json.Unmarshal(upgradeInfoFile, &uInfo); err != nil { - return err - } - - if uInfo.Name == "" { - return errors.New("upgrade-info.json is empty") - } - - // a destination directory, Format YYYY-MM-DD - st := time.Now() - ymd := fmt.Sprintf("%d-%d-%d", st.Year(), st.Month(), st.Day()) - dst := filepath.Join(l.cfg.DataBackupPath, fmt.Sprintf("data"+"-backup-%s", ymd)) - - l.logger.Info("starting to take backup of data directory", "backup start time", st) - - // copy the $DAEMON_HOME/data to a backup dir - if err = copy.Copy(filepath.Join(l.cfg.Home, "data"), dst); err != nil { - return fmt.Errorf("error while taking data backup: %w", err) - } - - // backup is done, lets check endtime to calculate total time taken for backup process - et := time.Now() - l.logger.Info("backup completed", "backup saved at", dst, "backup completion time", et, "time taken to complete backup", et.Sub(st)) - } - - return nil -} diff --git a/tools/cosmovisor/scanner.go b/tools/cosmovisor/scanner.go deleted file mode 100644 index 0d26d09d427d..000000000000 --- a/tools/cosmovisor/scanner.go +++ /dev/null @@ -1,113 +0,0 @@ -package cosmovisor - -import ( - "context" - "errors" - "time" - - "cosmossdk.io/tools/cosmovisor/internal/watchers" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" -) - -var errUntestAble = errors.New("untestable") - -func initWatcher[T any](ctx context.Context, cfg *Config, dirWatcher *watchers.FSNotifyWatcher, filename string, unmarshal func([]byte) (T, error)) watchers.Watcher[T] { - if dirWatcher != nil { - hybridWatcher := watchers.NewHybridWatcher(ctx, dirWatcher, filename, cfg.PollInterval) - return watchers.NewDataWatcher[T](ctx, hybridWatcher, unmarshal) - } else { - pollWatcher := watchers.NewFilePollWatcher(ctx, filename, cfg.PollInterval) - return watchers.NewDataWatcher[T](ctx, pollWatcher, unmarshal) - } -} - -type fileWatcher struct { - daemonHome string - filename string // full path to a watched file - interval time.Duration - - currentBin string - currentInfo upgradetypes.Plan - lastModTime time.Time - cancel chan bool - ticker *time.Ticker - - needsUpdate bool - initialized bool - disableRecase bool -} - -//// checkHeight checks if the current block height -//func (fw *fileWatcher) checkHeight() (int64, error) { -// if testing.Testing() { // we cannot test the command in the test environment -// return 0, errUntestAble -// } -// -// if fw.IsStop() { -// result, err := exec.Command(fw.currentBin, "config", "get", "config", "db_backend", "--home", fw.daemonHome).CombinedOutput() //nolint:gosec // we want to execute the config command -// if err != nil { -// result = []byte("goleveldb") // set default value, old version may not have config command -// } -// blockStoreDB, err := dbm.NewDB("blockstore", dbm.BackendType(result), filepath.Join(fw.daemonHome, "data")) -// if err != nil { -// return 0, err -// } -// defer blockStoreDB.Close() -// return store.NewBlockStore(blockStoreDB).Height(), nil -// } -// -// result, err := exec.Command(fw.currentBin, "status", "--home", fw.daemonHome).CombinedOutput() //nolint:gosec // we want to execute the status command -// if err != nil { -// return 0, err -// } -// -// type response struct { -// SyncInfo struct { -// LatestBlockHeight string `json:"latest_block_height"` -// } `json:"sync_info"` -// AnotherCasingSyncInfo struct { -// LatestBlockHeight string `json:"latest_block_height"` -// } `json:"SyncInfo"` -// } -// -// var resp response -// if err := json.Unmarshal(result, &resp); err != nil { -// return 0, err -// } -// -// if resp.SyncInfo.LatestBlockHeight != "" { -// return strconv.ParseInt(resp.SyncInfo.LatestBlockHeight, 10, 64) -// } else if resp.AnotherCasingSyncInfo.LatestBlockHeight != "" { -// return strconv.ParseInt(resp.AnotherCasingSyncInfo.LatestBlockHeight, 10, 64) -// } -// -// return 0, errors.New("latest block height is empty") -//} -// -//func parseUpgradeInfoFile(filename string, disableRecase bool) (upgradetypes.Plan, error) { -// f, err := os.ReadFile(filename) -// if err != nil { -// return upgradetypes.Plan{}, err -// } -// -// if len(f) == 0 { -// return upgradetypes.Plan{}, fmt.Errorf("empty upgrade-info.json in %q", filename) -// } -// -// var upgradePlan upgradetypes.Plan -// if err := json.Unmarshal(f, &upgradePlan); err != nil { -// return upgradetypes.Plan{}, err -// } -// -// // required values must be set -// if err := upgradePlan.ValidateBasic(); err != nil { -// return upgradetypes.Plan{}, fmt.Errorf("invalid upgrade-info.json content: %w, got: %v", err, upgradePlan) -// } -// -// // normalize name to prevent operator error in upgrade name case sensitivity errors. -// if !disableRecase { -// upgradePlan.Name = strings.ToLower(upgradePlan.Name) -// } -// -// return upgradePlan, nil -//} diff --git a/tools/cosmovisor/scanner_test.go b/tools/cosmovisor/scanner_test.go index 36de3b68074e..38b6ad2ccebe 100644 --- a/tools/cosmovisor/scanner_test.go +++ b/tools/cosmovisor/scanner_test.go @@ -1,6 +1,7 @@ package cosmovisor import ( + "os" "path/filepath" "testing" @@ -35,25 +36,25 @@ func TestParseUpgradeInfoFile(t *testing.T) { filename: "f2-bad-type.json", disableRecase: false, expectUpgrade: upgradetypes.Plan{}, - expectErr: "cannot unmarshal number into Go struct", + expectErr: "cannot unmarshal number into Go value", }, { filename: "f2-bad-type-2.json", disableRecase: false, expectUpgrade: upgradetypes.Plan{}, - expectErr: "height must be greater than 0: invalid request", + expectErr: `unknown field "heigh"`, }, { filename: "f3-empty.json", disableRecase: false, expectUpgrade: upgradetypes.Plan{}, - expectErr: "empty upgrade-info.json in", + expectErr: "EOF", }, { filename: "f4-empty-obj.json", disableRecase: false, expectUpgrade: upgradetypes.Plan{}, - expectErr: "invalid upgrade-info.json content: name cannot be empty", + expectErr: "name cannot be empty", }, { filename: "f5-partial-obj-1.json", @@ -90,3 +91,14 @@ func TestParseUpgradeInfoFile(t *testing.T) { }) } } + +func parseUpgradeInfoFile(filename string, disableRecase bool) (upgradetypes.Plan, error) { + cfg := &Config{ + DisableRecase: disableRecase, + } + bz, err := os.ReadFile(filename) + if err != nil { + return upgradetypes.Plan{}, err + } + return cfg.ParseUpgradeInfo(bz) +} From 990aeaf2448b9c8ff1260dc034a18964330c4a1e Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 6 Jun 2025 17:11:06 -0400 Subject: [PATCH 050/115] migrate most existing tests, add backoff retry count --- tools/cosmovisor/args.go | 5 + tools/cosmovisor/internal/backoff.go | 26 +++-- tools/cosmovisor/internal/runner.go | 4 +- tools/cosmovisor/process_test.go | 161 ++++++++++++++++----------- 4 files changed, 123 insertions(+), 73 deletions(-) diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index af58778acd86..a3e9d58950a6 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -72,6 +72,11 @@ type Config struct { TimeFormatLogs string `toml:"cosmovisor_timeformat_logs" mapstructure:"cosmovisor_timeformat_logs" default:"kitchen"` CustomPreUpgrade string `toml:"cosmovisor_custom_preupgrade" mapstructure:"cosmovisor_custom_preupgrade" default:""` DisableRecase bool `toml:"cosmovisor_disable_recase" mapstructure:"cosmovisor_disable_recase" default:"false"` + // MaxRestartRetries is the maximum number of times + // to restart the binary after spurious shutdowns, + // (those not due to valid upgrades or halt heights changes). + // A value of 0 means no limit. + MaxRestartRetries int `toml:"max_restart_retries" mapstructure:"max_restart_retries" default:"5"` } // Root returns the root directory where all info lives diff --git a/tools/cosmovisor/internal/backoff.go b/tools/cosmovisor/internal/backoff.go index 4ba79bbb63df..a563ba80dc14 100644 --- a/tools/cosmovisor/internal/backoff.go +++ b/tools/cosmovisor/internal/backoff.go @@ -1,6 +1,7 @@ package internal import ( + "fmt" "time" "cosmossdk.io/log" @@ -8,18 +9,21 @@ import ( ) type RetryBackoffManager struct { - lastCmd string - lastArgs []string - backoff backoff.BackOff - logger log.Logger + lastCmd string + lastArgs []string + backoff backoff.BackOff + retryCount int + maxRestarts int + logger log.Logger } // NewRetryBackoffManager creates a new RetryBackoffManager instance. -func NewRetryBackoffManager(logger log.Logger) *RetryBackoffManager { +func NewRetryBackoffManager(logger log.Logger, maxRestarts int) *RetryBackoffManager { backoffAlg := backoff.NewExponentialBackOff() return &RetryBackoffManager{ - backoff: backoffAlg, - logger: logger, + backoff: backoffAlg, + maxRestarts: maxRestarts, + logger: logger, } } @@ -39,15 +43,19 @@ func (r *RetryBackoffManager) BeforeRun(cmd string, args []string) error { } if reset { // if the command or arguments have changed, we reset the backoff and store the new command and arguments - r.logger.Info("Resetting backoff due to command or arguments change") r.backoff.Reset() + r.retryCount = 0 r.lastCmd = cmd r.lastArgs = args } else { + r.retryCount++ + if r.maxRestarts > 0 && r.retryCount >= r.maxRestarts { + return backoff.Permanent(fmt.Errorf("maximum number of restarts reached: %d", r.maxRestarts)) + } // if the command and arguments are the same, we wait for the next backoff interval duration := r.backoff.NextBackOff() r.logger.Info("Applying backoff before restarting command", - "backoff", duration) + "backoff_duration", duration.String()) time.Sleep(duration) r.logger.Info("Backoff time elapsed, restarting ") } diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 91d3d42c9fc8..2313f04f76b3 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -32,7 +32,7 @@ func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) Runn } func (r Runner) Start(ctx context.Context, args []string) error { - retryMgr := NewRetryBackoffManager(r.logger) + retryMgr := NewRetryBackoffManager(r.logger, r.cfg.MaxRestartRetries) for { // First we check if we need to upgrade and if we do we perform the upgrade upgraded, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) @@ -149,7 +149,7 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 return r.cfg.WriteLastKnownHeight(height) }) - r.logger.Info("Starting process %s with args %v", cmd.Path, cmd.Args) + r.logger.Info("Starting process", "path", cmd.Path, "args", cmd.Args) processRunner := RunProcess(cmd) defer func() { // TODO always check for the latest block height before shutting down diff --git a/tools/cosmovisor/process_test.go b/tools/cosmovisor/process_test.go index 7d91c90e17b6..3df4618459f3 100644 --- a/tools/cosmovisor/process_test.go +++ b/tools/cosmovisor/process_test.go @@ -4,6 +4,7 @@ package cosmovisor_test import ( "bytes" + "context" "fmt" "io/fs" "os" @@ -12,11 +13,11 @@ import ( "testing" "time" + "cosmossdk.io/log" "github.com/stretchr/testify/require" - "cosmossdk.io/log" "cosmossdk.io/tools/cosmovisor" - + "cosmossdk.io/tools/cosmovisor/internal" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -26,6 +27,8 @@ func init() { workDir, _ = os.Getwd() } +// TODO all these tests share the same setup so we can extract it to a common function + // TestLaunchProcess will try running the script a few times and watch upgrades work properly // and args are passed through func TestLaunchProcess(t *testing.T) { @@ -34,9 +37,10 @@ func TestLaunchProcess(t *testing.T) { t, fmt.Sprintf("%s/%s", workDir, "testdata/validate"), cosmovisor.Config{ - Name: "dummyd", - PollInterval: 15, - UnsafeSkipBackup: true, + Name: "dummyd", + PollInterval: 15, + UnsafeSkipBackup: true, + MaxRestartRetries: 1, }, ) @@ -53,33 +57,39 @@ func TestLaunchProcess(t *testing.T) { require.Equal(t, rPath, currentBin) - launcher, err := cosmovisor.NewLauncher(logger, cfg) - require.NoError(t, err) + runCfg := internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + } + runner := internal.NewRunner(cfg, runCfg, logger) upgradeFile := cfg.UpgradeInfoFilePath() args := []string{"foo", "bar", "1234", upgradeFile} - doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) + err = runner.Start(context.Background(), args) require.NoError(t, err) - require.True(t, doUpgrade) + //doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) + //require.NoError(t, err) + //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) // ensure this is upgraded now and produces new output currentBin, err = cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain2")) - require.NoError(t, err) + require.NoError(t, err) require.Equal(t, rPath, currentBin) args = []string{"second", "run", "--verbose"} stdout.Reset() stderr.Reset() - doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) - require.NoError(t, err) - require.False(t, doUpgrade) + err = runner.Start(context.Background(), args) + require.ErrorContains(t, err, "maximum number of restarts reached") + //doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) + //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) @@ -97,10 +107,11 @@ func TestPlanDisableRecase(t *testing.T) { t, fmt.Sprintf("%s/%s", workDir, "testdata/norecase"), cosmovisor.Config{ - Name: "dummyd", - PollInterval: 20, - UnsafeSkipBackup: true, - DisableRecase: true, + Name: "dummyd", + PollInterval: 20, + UnsafeSkipBackup: true, + DisableRecase: true, + MaxRestartRetries: 1, }, ) @@ -117,15 +128,20 @@ func TestPlanDisableRecase(t *testing.T) { require.Equal(t, rPath, currentBin) - launcher, err := cosmovisor.NewLauncher(logger, cfg) - require.NoError(t, err) + runCfg := internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + } + runner := internal.NewRunner(cfg, runCfg, logger) upgradeFile := cfg.UpgradeInfoFilePath() args := []string{"foo", "bar", "1234", upgradeFile} - doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) + //doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) + err = runner.Start(context.Background(), args) require.NoError(t, err) - require.True(t, doUpgrade) + //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) @@ -140,9 +156,10 @@ func TestPlanDisableRecase(t *testing.T) { stdout.Reset() stderr.Reset() - doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) - require.NoError(t, err) - require.False(t, doUpgrade) + err = runner.Start(context.Background(), args) + //runner.RunProcess(ctx, args) + require.ErrorContains(t, err, "maximum number of restarts reached") + //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) @@ -177,15 +194,19 @@ func TestLaunchProcessWithRestartDelay(t *testing.T) { require.NoError(t, err) require.Equal(t, rPath, currentBin) - launcher, err := cosmovisor.NewLauncher(logger, cfg) + runner := internal.NewRunner(cfg, internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + }, logger) require.NoError(t, err) upgradeFile := cfg.UpgradeInfoFilePath() start := time.Now() - doUpgrade, err := launcher.Run([]string{"foo", "bar", "1234", upgradeFile}, stdin, stdout, stderr) + err = runner.Start(context.Background(), []string{"foo", "bar", "1234", upgradeFile}) require.NoError(t, err) - require.True(t, doUpgrade) + //require.True(t, doUpgrade) // may not be the best way but the fastest way to check we meet the delay // in addition to comparing both the runtime of this test and TestLaunchProcess in addition @@ -201,10 +222,11 @@ func TestPlanShutdownGrace(t *testing.T) { t, fmt.Sprintf("%s/%s", workDir, "testdata/dontdie"), cosmovisor.Config{ - Name: "dummyd", - PollInterval: 15, - UnsafeSkipBackup: true, - ShutdownGrace: 2 * time.Second, + Name: "dummyd", + PollInterval: 15, + UnsafeSkipBackup: true, + ShutdownGrace: 2 * time.Second, + MaxRestartRetries: 1, }, ) @@ -219,15 +241,18 @@ func TestPlanShutdownGrace(t *testing.T) { require.NoError(t, err) require.Equal(t, rPath, currentBin) - launcher, err := cosmovisor.NewLauncher(logger, cfg) - require.NoError(t, err) + runner := internal.NewRunner(cfg, internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + }, logger) upgradeFile := cfg.UpgradeInfoFilePath() args := []string{"foo", "bar", "1234", upgradeFile} - doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) + err = runner.Start(context.Background(), args) require.NoError(t, err) - require.True(t, doUpgrade) + //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\nWARN Need Flush\nFlushed\n", upgradeFile), stdout.String()) @@ -242,9 +267,9 @@ func TestPlanShutdownGrace(t *testing.T) { stdout.Reset() stderr.Reset() - doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) - require.NoError(t, err) - require.False(t, doUpgrade) + err = runner.Start(context.Background(), args) + require.ErrorContains(t, err, "maximum number of restarts reached") + //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) @@ -269,6 +294,7 @@ func TestLaunchProcessWithDownloads(t *testing.T) { AllowDownloadBinaries: true, PollInterval: 100, UnsafeSkipBackup: true, + MaxRestartRetries: 1, }, ) @@ -282,15 +308,18 @@ func TestLaunchProcessWithDownloads(t *testing.T) { require.NoError(t, err) require.Equal(t, rPath, currentBin) - launcher, err := cosmovisor.NewLauncher(logger, cfg) - require.NoError(t, err) - stdin, _ := os.Open(os.DevNull) stdout, stderr := newBuffer(), newBuffer() + launcher := internal.NewRunner(cfg, internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + }, logger) + args := []string{"some", "args", upgradeFilename} - doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) + err = launcher.Start(context.Background(), args) require.NoError(t, err) - require.True(t, doUpgrade) + //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) currentBin, err = cfg.CurrentBin() @@ -304,13 +333,13 @@ func TestLaunchProcessWithDownloads(t *testing.T) { stdout.Reset() stderr.Reset() args = []string{"run", "--fast", upgradeFilename} - doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) + err = launcher.Start(context.Background(), args) require.NoError(t, err) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) // ended with one more upgrade - require.True(t, doUpgrade) + //require.True(t, doUpgrade) currentBin, err = cfg.CurrentBin() require.NoError(t, err) rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) @@ -321,9 +350,9 @@ func TestLaunchProcessWithDownloads(t *testing.T) { args = []string{"end", "--halt", upgradeFilename} stdout.Reset() stderr.Reset() - doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) - require.NoError(t, err) - require.False(t, doUpgrade) + err = launcher.Start(context.Background(), args) + require.ErrorContains(t, err, "maximum number of restarts reached") + //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) @@ -364,14 +393,18 @@ func TestLaunchProcessWithDownloadsAndMissingPreupgrade(t *testing.T) { rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - launcher, err := cosmovisor.NewLauncher(logger, cfg) + stdin, _ := os.Open(os.DevNull) + stdout, stderr := newBuffer(), newBuffer() + runner := internal.NewRunner(cfg, internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + }, logger) require.NoError(t, err) // Missing Preupgrade Script - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() args := []string{"some", "args", upgradeFilename} - _, err = launcher.Run(args, stdin, stdout, stderr) + err = runner.Start(context.Background(), args) require.ErrorContains(t, err, "missing.sh") require.ErrorIs(t, err, fs.ErrNotExist) @@ -406,16 +439,19 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - launcher, err := cosmovisor.NewLauncher(logger, cfg) - require.NoError(t, err) - stdin, _ := os.Open(os.DevNull) stdout, stderr := newBuffer(), newBuffer() + runner := internal.NewRunner(cfg, internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + }, logger) + args := []string{"some", "args", upgradeFilename} - doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) + err = runner.Start(context.Background(), args) require.NoError(t, err) - require.True(t, doUpgrade) + //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) currentBin, err = cfg.CurrentBin() @@ -432,13 +468,14 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { stdout.Reset() stderr.Reset() args = []string{"run", "--fast", upgradeFilename} - doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) + //doUpgrade, err = runner.Run(args, stdin, stdout, stderr) + err = runner.Start(context.Background(), args) require.NoError(t, err) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) // ended with one more upgrade - require.True(t, doUpgrade) + //require.True(t, doUpgrade) currentBin, err = cfg.CurrentBin() require.NoError(t, err) rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) @@ -452,9 +489,9 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { args = []string{"end", "--halt", upgradeFilename} stdout.Reset() stderr.Reset() - doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) + err = runner.Start(context.Background(), args) require.NoError(t, err) - require.False(t, doUpgrade) + //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) From 16e7a4b89191e425bec7f745116f93e9a3d610d9 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 6 Jun 2025 17:32:59 -0400 Subject: [PATCH 051/115] existing tests migrated --- tools/cosmovisor/internal/runner.go | 29 ++++++++++++----------------- tools/cosmovisor/process_test.go | 3 ++- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 2313f04f76b3..7a5aada18579 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -78,20 +78,17 @@ func (r Runner) Start(ctx context.Context, args []string) error { // Now we actually run the process err = r.RunProcess(ctx, cmd, haltHeight) - if err != nil { - // There are three types of errors we're checking for here: - // 1. ErrRestartNeeded: this is a custom error that is returned whenever the run loop detects that a restart is needed. - // 2. errDone: this is a sentinel error that indicates that the cosmovisor process itself should be stopped gracefully. - // 3. Any other error: this is an unexpected error that should be logged and returned, causing cosmovisor to exit - // TODO is it right for cosmovisor to exit on any other error (basically a non-zero return code)? Maybe we should just log it and continue? - var restartNeeded ErrRestartNeeded - if ok := errors.As(err, &restartNeeded); ok { - r.logger.Info("Restart needed") - } else if errors.Is(err, errDone) { - return nil - } else { - return err - } + // There are three types of cases we're checking for here: + // 1. ErrRestartNeeded: this is a custom error that is returned whenever the run loop detects that a restart is needed. + // 2. errDone: this is a sentinel error that indicates that the cosmovisor process itself should be stopped gracefully. + // 3. Any other error or the : this is an unexpected error that should be logged and returned, causing cosmovisor to exit + var restartNeeded ErrRestartNeeded + if ok := errors.As(err, &restartNeeded); ok { + r.logger.Info("Restart needed") + } else if errors.Is(err, errDone) { + return nil + } else { + r.logger.Error("Process exited", "error", err) } } } @@ -195,10 +192,8 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 } } case err := <-processRunner.Done(): - // TODO handle process exit - r.logger.Warn("Process exited unexpectedly", "error", err) + // we just return the error or absence of an error here, which will cause the process to restart with a backoff retry algorithm return err - // TODO: case actualHeight := <-heightWatcher.Updated(): r.logger.Warn("Got height update from watcher", "height", actualHeight) if haltHeight == 0 { diff --git a/tools/cosmovisor/process_test.go b/tools/cosmovisor/process_test.go index 3df4618459f3..c854d4499287 100644 --- a/tools/cosmovisor/process_test.go +++ b/tools/cosmovisor/process_test.go @@ -426,6 +426,7 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { PollInterval: 100, UnsafeSkipBackup: true, CustomPreUpgrade: "preupgrade.sh", + MaxRestartRetries: 1, }, ) @@ -490,7 +491,7 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { stdout.Reset() stderr.Reset() err = runner.Start(context.Background(), args) - require.NoError(t, err) + require.ErrorContains(t, err, "maximum number of restarts reached") //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) From fba2bacf79f3a72abb9489e59232323407cd52ac Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 10:09:31 -0400 Subject: [PATCH 052/115] remove dead test code --- tools/cosmovisor/process_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tools/cosmovisor/process_test.go b/tools/cosmovisor/process_test.go index c854d4499287..85a371c07a42 100644 --- a/tools/cosmovisor/process_test.go +++ b/tools/cosmovisor/process_test.go @@ -69,9 +69,6 @@ func TestLaunchProcess(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = runner.Start(context.Background(), args) require.NoError(t, err) - //doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) - //require.NoError(t, err) - //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) @@ -88,8 +85,6 @@ func TestLaunchProcess(t *testing.T) { err = runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - //doUpgrade, err = launcher.Run(args, stdin, stdout, stderr) - //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) @@ -138,10 +133,8 @@ func TestPlanDisableRecase(t *testing.T) { upgradeFile := cfg.UpgradeInfoFilePath() args := []string{"foo", "bar", "1234", upgradeFile} - //doUpgrade, err := launcher.Run(args, stdin, stdout, stderr) err = runner.Start(context.Background(), args) require.NoError(t, err) - //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) @@ -157,9 +150,7 @@ func TestPlanDisableRecase(t *testing.T) { stderr.Reset() err = runner.Start(context.Background(), args) - //runner.RunProcess(ctx, args) require.ErrorContains(t, err, "maximum number of restarts reached") - //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) @@ -206,7 +197,6 @@ func TestLaunchProcessWithRestartDelay(t *testing.T) { start := time.Now() err = runner.Start(context.Background(), []string{"foo", "bar", "1234", upgradeFile}) require.NoError(t, err) - //require.True(t, doUpgrade) // may not be the best way but the fastest way to check we meet the delay // in addition to comparing both the runtime of this test and TestLaunchProcess in addition @@ -252,7 +242,6 @@ func TestPlanShutdownGrace(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = runner.Start(context.Background(), args) require.NoError(t, err) - //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\nWARN Need Flush\nFlushed\n", upgradeFile), stdout.String()) @@ -269,7 +258,6 @@ func TestPlanShutdownGrace(t *testing.T) { err = runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) @@ -319,7 +307,6 @@ func TestLaunchProcessWithDownloads(t *testing.T) { args := []string{"some", "args", upgradeFilename} err = launcher.Start(context.Background(), args) require.NoError(t, err) - //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) currentBin, err = cfg.CurrentBin() @@ -339,7 +326,6 @@ func TestLaunchProcessWithDownloads(t *testing.T) { require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) // ended with one more upgrade - //require.True(t, doUpgrade) currentBin, err = cfg.CurrentBin() require.NoError(t, err) rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) @@ -352,7 +338,6 @@ func TestLaunchProcessWithDownloads(t *testing.T) { stderr.Reset() err = launcher.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) @@ -452,7 +437,6 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { err = runner.Start(context.Background(), args) require.NoError(t, err) - //require.True(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) currentBin, err = cfg.CurrentBin() @@ -469,14 +453,12 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { stdout.Reset() stderr.Reset() args = []string{"run", "--fast", upgradeFilename} - //doUpgrade, err = runner.Run(args, stdin, stdout, stderr) err = runner.Start(context.Background(), args) require.NoError(t, err) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) // ended with one more upgrade - //require.True(t, doUpgrade) currentBin, err = cfg.CurrentBin() require.NoError(t, err) rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) From 6cd12275ddb600e9552eda2289d3afa0e342d629 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 10:16:13 -0400 Subject: [PATCH 053/115] switch to known error for signaling upgrade completion --- tools/cosmovisor/internal/runner.go | 4 +++- tools/cosmovisor/process_test.go | 23 ++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 7a5aada18579..8c06c860f040 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -44,7 +44,7 @@ func (r Runner) Start(ctx context.Context, args []string) error { r.logger.Info("Upgrade completed, restarting process") if !r.cfg.RestartAfterUpgrade { r.logger.Info("DAEMON_RESTART_AFTER_UPGRADE is disabled, exiting process") - return nil + return ErrUpgradeNoDaemonRestart } } // Now we compute the command to run and figure out the halt height if needed @@ -95,6 +95,8 @@ func (r Runner) Start(ctx context.Context, args []string) error { var errDone = errors.New("done") +var ErrUpgradeNoDaemonRestart = errors.New("upgrade completed, but DAEMON_RESTART_AFTER_UPGRADE is disabled") + func (r Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, err error) { bin, err := r.cfg.CurrentBin() if err != nil { diff --git a/tools/cosmovisor/process_test.go b/tools/cosmovisor/process_test.go index 85a371c07a42..846419d49f99 100644 --- a/tools/cosmovisor/process_test.go +++ b/tools/cosmovisor/process_test.go @@ -68,7 +68,7 @@ func TestLaunchProcess(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = runner.Start(context.Background(), args) - require.NoError(t, err) + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) @@ -134,7 +134,7 @@ func TestPlanDisableRecase(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = runner.Start(context.Background(), args) - require.NoError(t, err) + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) @@ -196,7 +196,7 @@ func TestLaunchProcessWithRestartDelay(t *testing.T) { start := time.Now() err = runner.Start(context.Background(), []string{"foo", "bar", "1234", upgradeFile}) - require.NoError(t, err) + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) // may not be the best way but the fastest way to check we meet the delay // in addition to comparing both the runtime of this test and TestLaunchProcess in addition @@ -241,7 +241,7 @@ func TestPlanShutdownGrace(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = runner.Start(context.Background(), args) - require.NoError(t, err) + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) require.Empty(t, stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\nWARN Need Flush\nFlushed\n", upgradeFile), stdout.String()) @@ -306,7 +306,7 @@ func TestLaunchProcessWithDownloads(t *testing.T) { args := []string{"some", "args", upgradeFilename} err = launcher.Start(context.Background(), args) - require.NoError(t, err) + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) require.Empty(t, stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) currentBin, err = cfg.CurrentBin() @@ -321,11 +321,10 @@ func TestLaunchProcessWithDownloads(t *testing.T) { stderr.Reset() args = []string{"run", "--fast", upgradeFilename} err = launcher.Start(context.Background(), args) - require.NoError(t, err) - + // ended with one more upgrade + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) - // ended with one more upgrade currentBin, err = cfg.CurrentBin() require.NoError(t, err) rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) @@ -436,7 +435,7 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { args := []string{"some", "args", upgradeFilename} err = runner.Start(context.Background(), args) - require.NoError(t, err) + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) require.Empty(t, stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) currentBin, err = cfg.CurrentBin() @@ -454,11 +453,10 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { stderr.Reset() args = []string{"run", "--fast", upgradeFilename} err = runner.Start(context.Background(), args) - require.NoError(t, err) - + // ended with one more upgrade + require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) require.Empty(t, stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) - // ended with one more upgrade currentBin, err = cfg.CurrentBin() require.NoError(t, err) rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) @@ -474,7 +472,6 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { stderr.Reset() err = runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - //require.False(t, doUpgrade) require.Empty(t, stderr.String()) require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) From 54d625bc4d54e012d84af5683aeeea2fde989784 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 10:33:38 -0400 Subject: [PATCH 054/115] simplify test setup --- .../cmd/cosmovisor/mockchain_test.go | 3 +- tools/cosmovisor/internal/backoff.go | 1 + tools/cosmovisor/process_test.go | 433 ++++++++---------- 3 files changed, 187 insertions(+), 250 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index c825d35da739..a0857ac38531 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -16,7 +16,8 @@ import ( ) type MockChainSetup struct { - Genesis string + Genesis string + // TODO test setup should be similar to process_test.go GovUpgrades map[string]string ManualUpgrades map[string]string // to be added with the add-upgrade command Config *cosmovisor.Config diff --git a/tools/cosmovisor/internal/backoff.go b/tools/cosmovisor/internal/backoff.go index a563ba80dc14..9e2e1c599189 100644 --- a/tools/cosmovisor/internal/backoff.go +++ b/tools/cosmovisor/internal/backoff.go @@ -50,6 +50,7 @@ func (r *RetryBackoffManager) BeforeRun(cmd string, args []string) error { } else { r.retryCount++ if r.maxRestarts > 0 && r.retryCount >= r.maxRestarts { + // TODO we should return the last error that the process returned here return backoff.Permanent(fmt.Errorf("maximum number of restarts reached: %d", r.maxRestarts)) } // if the command and arguments are the same, we wait for the next backoff interval diff --git a/tools/cosmovisor/process_test.go b/tools/cosmovisor/process_test.go index 846419d49f99..43e150c1b6d0 100644 --- a/tools/cosmovisor/process_test.go +++ b/tools/cosmovisor/process_test.go @@ -29,67 +29,59 @@ func init() { // TODO all these tests share the same setup so we can extract it to a common function +type launchProcessFixture struct { + cfg *cosmovisor.Config + stdin *os.File + stdout *buffer + stderr *buffer + logger log.Logger + runner internal.Runner +} + // TestLaunchProcess will try running the script a few times and watch upgrades work properly // and args are passed through func TestLaunchProcess(t *testing.T) { - // binaries from testdata/validate directory - cfg := prepareConfig( - t, - fmt.Sprintf("%s/%s", workDir, "testdata/validate"), - cosmovisor.Config{ - Name: "dummyd", - PollInterval: 15, - UnsafeSkipBackup: true, - MaxRestartRetries: 1, - }, - ) + f := setupTestLaunchProcessFixture(t, "validate", cosmovisor.Config{ + Name: "dummyd", + PollInterval: 15, + UnsafeSkipBackup: true, + MaxRestartRetries: 1, + }) - logger := log.NewTestLogger(t).With(log.ModuleKey, "cosmosvisor") - - // should run the genesis binary and produce expected output - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() - currentBin, err := cfg.CurrentBin() + currentBin, err := f.cfg.CurrentBin() require.NoError(t, err) - rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) + rPath, err := filepath.EvalSymlinks(f.cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - runCfg := internal.RunConfig{ - StdIn: stdin, - StdOut: stdout, - StdErr: stderr, - } - - runner := internal.NewRunner(cfg, runCfg, logger) - upgradeFile := cfg.UpgradeInfoFilePath() + upgradeFile := f.cfg.UpgradeInfoFilePath() args := []string{"foo", "bar", "1234", upgradeFile} - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) - require.Empty(t, stderr.String()) - require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), f.stdout.String()) // ensure this is upgraded now and produces new output - currentBin, err = cfg.CurrentBin() + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) args = []string{"second", "run", "--verbose"} - stdout.Reset() - stderr.Reset() + f.stdout.Reset() + f.stderr.Reset() - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - require.Empty(t, stderr.String()) - require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", f.stdout.String()) // ended without other upgrade - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) @@ -97,172 +89,124 @@ func TestLaunchProcess(t *testing.T) { // TestPlanDisableRecase will test upgrades without lower case plan names func TestPlanDisableRecase(t *testing.T) { - // binaries from testdata/validate directory - cfg := prepareConfig( - t, - fmt.Sprintf("%s/%s", workDir, "testdata/norecase"), - cosmovisor.Config{ - Name: "dummyd", - PollInterval: 20, - UnsafeSkipBackup: true, - DisableRecase: true, - MaxRestartRetries: 1, - }, - ) - - logger := log.NewTestLogger(t).With(log.ModuleKey, "cosmosvisor") + f := setupTestLaunchProcessFixture(t, "norecase", cosmovisor.Config{ + Name: "dummyd", + PollInterval: 20, + UnsafeSkipBackup: true, + DisableRecase: true, + MaxRestartRetries: 1, + }) - // should run the genesis binary and produce expected output - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() - currentBin, err := cfg.CurrentBin() + currentBin, err := f.cfg.CurrentBin() require.NoError(t, err) - rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) + rPath, err := filepath.EvalSymlinks(f.cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - runCfg := internal.RunConfig{ - StdIn: stdin, - StdOut: stdout, - StdErr: stderr, - } - runner := internal.NewRunner(cfg, runCfg, logger) - - upgradeFile := cfg.UpgradeInfoFilePath() + upgradeFile := f.cfg.UpgradeInfoFilePath() args := []string{"foo", "bar", "1234", upgradeFile} - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) - require.Empty(t, stderr.String()) - require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\n", upgradeFile), stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\n", upgradeFile), f.stdout.String()) // ensure this is upgraded now and produces new output - currentBin, err = cfg.CurrentBin() + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("Chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("Chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) args = []string{"second", "run", "--verbose"} - stdout.Reset() - stderr.Reset() + f.stdout.Reset() + f.stderr.Reset() - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - require.Empty(t, stderr.String()) - require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", f.stdout.String()) // ended without other upgrade - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("Chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("Chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) } func TestLaunchProcessWithRestartDelay(t *testing.T) { - // binaries from testdata/validate directory - cfg := prepareConfig( - t, - fmt.Sprintf("%s/%s", workDir, "testdata/validate"), - cosmovisor.Config{ - Name: "dummyd", - RestartDelay: 5 * time.Second, - PollInterval: 20, - UnsafeSkipBackup: true, - }, - ) - - logger := log.NewTestLogger(t).With(log.ModuleKey, "cosmosvisor") + f := setupTestLaunchProcessFixture(t, "validate", cosmovisor.Config{ + Name: "dummyd", + RestartDelay: 5 * time.Second, + PollInterval: 20, + UnsafeSkipBackup: true, + }) // should run the genesis binary and produce expected output - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() - currentBin, err := cfg.CurrentBin() + currentBin, err := f.cfg.CurrentBin() require.NoError(t, err) - rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) + rPath, err := filepath.EvalSymlinks(f.cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - runner := internal.NewRunner(cfg, internal.RunConfig{ - StdIn: stdin, - StdOut: stdout, - StdErr: stderr, - }, logger) - require.NoError(t, err) - - upgradeFile := cfg.UpgradeInfoFilePath() + upgradeFile := f.cfg.UpgradeInfoFilePath() start := time.Now() - err = runner.Start(context.Background(), []string{"foo", "bar", "1234", upgradeFile}) + err = f.runner.Start(context.Background(), []string{"foo", "bar", "1234", upgradeFile}) require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) // may not be the best way but the fastest way to check we meet the delay // in addition to comparing both the runtime of this test and TestLaunchProcess in addition - if time.Since(start) < cfg.RestartDelay { + if time.Since(start) < f.cfg.RestartDelay { require.FailNow(t, "restart delay not met") } } // TestPlanShutdownGrace will test upgrades without lower case plan names func TestPlanShutdownGrace(t *testing.T) { - // binaries from testdata/validate directory - cfg := prepareConfig( - t, - fmt.Sprintf("%s/%s", workDir, "testdata/dontdie"), - cosmovisor.Config{ - Name: "dummyd", - PollInterval: 15, - UnsafeSkipBackup: true, - ShutdownGrace: 2 * time.Second, - MaxRestartRetries: 1, - }, - ) - - logger := log.NewTestLogger(t).With(log.ModuleKey, "cosmosvisor") + f := setupTestLaunchProcessFixture(t, "dontdie", cosmovisor.Config{ + Name: "dummyd", + PollInterval: 15, + UnsafeSkipBackup: true, + ShutdownGrace: 2 * time.Second, + MaxRestartRetries: 1, + }) // should run the genesis binary and produce expected output - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() - currentBin, err := cfg.CurrentBin() + currentBin, err := f.cfg.CurrentBin() require.NoError(t, err) - rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) + rPath, err := filepath.EvalSymlinks(f.cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - runner := internal.NewRunner(cfg, internal.RunConfig{ - StdIn: stdin, - StdOut: stdout, - StdErr: stderr, - }, logger) - - upgradeFile := cfg.UpgradeInfoFilePath() + upgradeFile := f.cfg.UpgradeInfoFilePath() args := []string{"foo", "bar", "1234", upgradeFile} - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) - require.Empty(t, stderr.String()) - require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\nWARN Need Flush\nFlushed\n", upgradeFile), stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\nWARN Need Flush\nFlushed\n", upgradeFile), f.stdout.String()) // ensure this is upgraded now and produces new output - currentBin, err = cfg.CurrentBin() + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) args = []string{"second", "run", "--verbose"} - stdout.Reset() - stderr.Reset() + f.stdout.Reset() + f.stderr.Reset() - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - require.Empty(t, stderr.String()) - require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", f.stdout.String()) // ended without other upgrade - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) } @@ -270,80 +214,68 @@ func TestPlanShutdownGrace(t *testing.T) { // TestLaunchProcess will try running the script a few times and watch upgrades work properly // and args are passed through func TestLaunchProcessWithDownloads(t *testing.T) { + f := setupTestLaunchProcessFixture(t, "download", cosmovisor.Config{ + Name: "autod", + AllowDownloadBinaries: true, + PollInterval: 100, + UnsafeSkipBackup: true, + MaxRestartRetries: 1, + }) + // test case upgrade path (binaries from testdata/download directory): // genesis -> chain2-zip_bin // chain2-zip_bin -> ref_to_chain3-zip_dir.json = (json for the next download instructions) -> chain3-zip_dir // chain3-zip_dir - doesn't upgrade - cfg := prepareConfig( - t, - fmt.Sprintf("%s/%s", workDir, "testdata/download"), - cosmovisor.Config{ - Name: "autod", - AllowDownloadBinaries: true, - PollInterval: 100, - UnsafeSkipBackup: true, - MaxRestartRetries: 1, - }, - ) - - logger := log.NewTestLogger(t).With(log.ModuleKey, "cosmovisor") - upgradeFilename := cfg.UpgradeInfoFilePath() + upgradeFilename := f.cfg.UpgradeInfoFilePath() // should run the genesis binary and produce expected output - currentBin, err := cfg.CurrentBin() + currentBin, err := f.cfg.CurrentBin() require.NoError(t, err) - rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) + rPath, err := filepath.EvalSymlinks(f.cfg.GenesisBin()) + require.NoError(t, err) require.Equal(t, rPath, currentBin) - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() - launcher := internal.NewRunner(cfg, internal.RunConfig{ - StdIn: stdin, - StdOut: stdout, - StdErr: stderr, - }, logger) - args := []string{"some", "args", upgradeFilename} - err = launcher.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) - require.Empty(t, stderr.String()) - require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) - currentBin, err = cfg.CurrentBin() + require.Empty(t, f.stderr.String()) + require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", f.stdout.String()) + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) // start chain2 - stdout.Reset() - stderr.Reset() + f.stdout.Reset() + f.stderr.Reset() args = []string{"run", "--fast", upgradeFilename} - err = launcher.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) // ended with one more upgrade require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) - require.Empty(t, stderr.String()) - require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) - currentBin, err = cfg.CurrentBin() + require.Empty(t, f.stderr.String()) + require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", f.stdout.String()) + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain3")) require.NoError(t, err) require.Equal(t, rPath, currentBin) // run the last chain args = []string{"end", "--halt", upgradeFilename} - stdout.Reset() - stderr.Reset() - err = launcher.Start(context.Background(), args) + f.stdout.Reset() + f.stderr.Reset() + err = f.runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - require.Empty(t, stderr.String()) - require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", f.stdout.String()) // and this doesn't upgrade - currentBin, err = cfg.CurrentBin() + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain3")) require.NoError(t, err) require.Equal(t, rPath, currentBin) } @@ -355,40 +287,28 @@ func TestLaunchProcessWithDownloadsAndMissingPreupgrade(t *testing.T) { // genesis -> chain2-zip_bin // chain2-zip_bin -> ref_to_chain3-zip_dir.json = (json for the next download instructions) -> chain3-zip_dir // chain3-zip_dir - doesn't upgrade - cfg := prepareConfig( - t, - fmt.Sprintf("%s/%s", workDir, "testdata/download"), - cosmovisor.Config{ - Name: "autod", - AllowDownloadBinaries: true, - PollInterval: 100, - UnsafeSkipBackup: true, - CustomPreUpgrade: "missing.sh", - }, - ) + f := setupTestLaunchProcessFixture(t, "download", cosmovisor.Config{ + Name: "autod", + AllowDownloadBinaries: true, + PollInterval: 100, + UnsafeSkipBackup: true, + CustomPreUpgrade: "missing.sh", + }) - logger := log.NewTestLogger(t).With(log.ModuleKey, "cosmovisor") - upgradeFilename := cfg.UpgradeInfoFilePath() + upgradeFilename := f.cfg.UpgradeInfoFilePath() // should run the genesis binary and produce expected output - currentBin, err := cfg.CurrentBin() + currentBin, err := f.cfg.CurrentBin() require.NoError(t, err) - rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) + rPath, err := filepath.EvalSymlinks(f.cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() - runner := internal.NewRunner(cfg, internal.RunConfig{ - StdIn: stdin, - StdOut: stdout, - StdErr: stderr, - }, logger) require.NoError(t, err) // Missing Preupgrade Script args := []string{"some", "args", upgradeFilename} - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorContains(t, err, "missing.sh") require.ErrorIs(t, err, fs.ErrNotExist) @@ -401,84 +321,71 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { // genesis -> chain2-zip_bin // chain2-zip_bin -> ref_to_chain3-zip_dir.json = (json for the next download instructions) -> chain3-zip_dir // chain3-zip_dir - doesn't upgrade - cfg := prepareConfig( - t, - fmt.Sprintf("%s/%s", workDir, "testdata/download"), - cosmovisor.Config{ - Name: "autod", - AllowDownloadBinaries: true, - PollInterval: 100, - UnsafeSkipBackup: true, - CustomPreUpgrade: "preupgrade.sh", - MaxRestartRetries: 1, - }, - ) + f := setupTestLaunchProcessFixture(t, "download", cosmovisor.Config{ + Name: "autod", + AllowDownloadBinaries: true, + PollInterval: 100, + UnsafeSkipBackup: true, + CustomPreUpgrade: "preupgrade.sh", + MaxRestartRetries: 1, + }) - buf := newBuffer() // inspect output using buf.String() - logger := log.NewLogger(buf).With(log.ModuleKey, "cosmovisor") - upgradeFilename := cfg.UpgradeInfoFilePath() + upgradeFilename := f.cfg.UpgradeInfoFilePath() // should run the genesis binary and produce expected output - currentBin, err := cfg.CurrentBin() + currentBin, err := f.cfg.CurrentBin() require.NoError(t, err) - rPath, err := filepath.EvalSymlinks(cfg.GenesisBin()) + rPath, err := filepath.EvalSymlinks(f.cfg.GenesisBin()) require.NoError(t, err) require.Equal(t, rPath, currentBin) - stdin, _ := os.Open(os.DevNull) - stdout, stderr := newBuffer(), newBuffer() - runner := internal.NewRunner(cfg, internal.RunConfig{ - StdIn: stdin, - StdOut: stdout, - StdErr: stderr, - }, logger) args := []string{"some", "args", upgradeFilename} - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) - require.Empty(t, stderr.String()) - require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) - currentBin, err = cfg.CurrentBin() + require.Empty(t, f.stderr.String()) + require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", f.stdout.String()) + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain2")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain2")) require.NoError(t, err) require.Equal(t, rPath, currentBin) // should have preupgrade.sh results - require.FileExists(t, filepath.Join(cfg.Home, "upgrade_name_chain2_height_49")) + require.FileExists(t, filepath.Join(f.cfg.Home, "upgrade_name_chain2_height_49")) // start chain2 - stdout.Reset() - stderr.Reset() + f.stdout.Reset() + f.stderr.Reset() args = []string{"run", "--fast", upgradeFilename} - err = runner.Start(context.Background(), args) + err = f.runner.Start(context.Background(), args) // ended with one more upgrade require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) - require.Empty(t, stderr.String()) - require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) - currentBin, err = cfg.CurrentBin() + require.Empty(t, f.stderr.String()) + require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", f.stdout.String()) + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain3")) require.NoError(t, err) require.Equal(t, rPath, currentBin) // should have preupgrade.sh results - require.FileExists(t, filepath.Join(cfg.Home, "upgrade_name_chain3_height_936")) + require.FileExists(t, filepath.Join(f.cfg.Home, "upgrade_name_chain3_height_936")) // run the last chain args = []string{"end", "--halt", upgradeFilename} - stdout.Reset() - stderr.Reset() - err = runner.Start(context.Background(), args) + f.stdout.Reset() + f.stderr.Reset() + err = f.runner.Start(context.Background(), args) require.ErrorContains(t, err, "maximum number of restarts reached") - require.Empty(t, stderr.String()) - require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) + require.Empty(t, f.stderr.String()) + require.Equal(t, "Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", f.stdout.String()) // and this doesn't upgrade - currentBin, err = cfg.CurrentBin() + currentBin, err = f.cfg.CurrentBin() require.NoError(t, err) - rPath, err = filepath.EvalSymlinks(cfg.UpgradeBin("chain3")) + rPath, err = filepath.EvalSymlinks(f.cfg.UpgradeBin("chain3")) require.NoError(t, err) require.Equal(t, rPath, currentBin) } @@ -564,6 +471,34 @@ type buffer struct { m sync.Mutex } +func setupTestLaunchProcessFixture(t *testing.T, testdataDir string, cfg cosmovisor.Config) *launchProcessFixture { + // binaries from testdata/validate directory + preppedCfg := prepareConfig( + t, + fmt.Sprintf("%s/testdata/%s", workDir, testdataDir), + cfg, + ) + + logger := log.NewTestLogger(t).With(log.ModuleKey, "cosmosvisor") + + // should run the genesis binary and produce expected output + stdin, _ := os.Open(os.DevNull) + stdout, stderr := newBuffer(), newBuffer() + + return &launchProcessFixture{ + cfg: preppedCfg, + stdin: stdin, + stdout: stdout, + stderr: stderr, + logger: logger, + runner: internal.NewRunner(preppedCfg, internal.RunConfig{ + StdIn: stdin, + StdOut: stdout, + StdErr: stderr, + }, logger), + } +} + func newBuffer() *buffer { return &buffer{} } From cb624f8e58e75b406870ddb7663d8684e4acf3c6 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 12:26:42 -0400 Subject: [PATCH 055/115] fix tests --- tools/cosmovisor/internal/watchers/data_watcher_test.go | 8 ++++++-- .../{poll_watcher_test.go => file_poll_watcher_test.go} | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) rename tools/cosmovisor/internal/watchers/{poll_watcher_test.go => file_poll_watcher_test.go} (95%) diff --git a/tools/cosmovisor/internal/watchers/data_watcher_test.go b/tools/cosmovisor/internal/watchers/data_watcher_test.go index 8d3c80f32e39..35aa68bb345d 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/data_watcher_test.go @@ -22,8 +22,12 @@ func TestDataWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile.json") ctx, cancel := context.WithCancel(context.Background()) - pollWatcher := NewPollWatcher(ctx, filename, time.Millisecond*100) - dataWatcher := NewDataWatcher[TestData](ctx, pollWatcher) + pollWatcher := NewFilePollWatcher(ctx, filename, time.Millisecond*100) + dataWatcher := NewDataWatcher[TestData](ctx, pollWatcher, func(contents []byte) (TestData, error) { + var data TestData + err := json.Unmarshal(contents, &data) + return data, err + }) expectedContent := TestData{ X: 10, diff --git a/tools/cosmovisor/internal/watchers/poll_watcher_test.go b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go similarity index 95% rename from tools/cosmovisor/internal/watchers/poll_watcher_test.go rename to tools/cosmovisor/internal/watchers/file_poll_watcher_test.go index afe6b5afdb40..15244c547482 100644 --- a/tools/cosmovisor/internal/watchers/poll_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go @@ -16,7 +16,7 @@ func TestPollWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile") ctx, cancel := context.WithCancel(context.Background()) - watcher := NewPollWatcher(ctx, filename, time.Millisecond*100) + watcher := NewFilePollWatcher(ctx, filename, time.Millisecond*100) expectedContent := []byte("test") go func() { // write some dummy data to the file From d0d6e661a8a77c4cef2d2da356e32f9e0fdce8ab Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 13:33:20 -0400 Subject: [PATCH 056/115] comments --- tools/cosmovisor/internal/runner.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 8c06c860f040..570445b36cce 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -63,6 +63,7 @@ func (r Runner) Start(ctx context.Context, args []string) error { // the command or its arguments have changed (e.g. if the binary was updated or the halt height changed), // or if we're just in some sort of error restart loop. // TODO add tests for this behavior + // TODO should DAEMON_RESTART_AFTER_UPGRADE apply here? it is explicitly about upgrades, but is the user's intention to prevent all restarts? if err := retryMgr.BeforeRun(cmd.Path, cmd.Args); err != nil { return err } @@ -81,13 +82,14 @@ func (r Runner) Start(ctx context.Context, args []string) error { // There are three types of cases we're checking for here: // 1. ErrRestartNeeded: this is a custom error that is returned whenever the run loop detects that a restart is needed. // 2. errDone: this is a sentinel error that indicates that the cosmovisor process itself should be stopped gracefully. - // 3. Any other error or the : this is an unexpected error that should be logged and returned, causing cosmovisor to exit + // 3. Any other error or the: this is an unexpected error that should trigger a restart of the process with a backoff strategy. var restartNeeded ErrRestartNeeded if ok := errors.As(err, &restartNeeded); ok { r.logger.Info("Restart needed") } else if errors.Is(err, errDone) { return nil } else { + // TODO we should capture this error and it should get returned if retry max restarts is reached r.logger.Error("Process exited", "error", err) } } @@ -97,6 +99,11 @@ var errDone = errors.New("done") var ErrUpgradeNoDaemonRestart = errors.New("upgrade completed, but DAEMON_RESTART_AFTER_UPGRADE is disabled") +// ComputeRunPlan computes the command to run based on the current configuration and arguments +// as well as determining the halt height if a manual upgrade is present. +// This is called to determine run arguments first and allows us to observe whether +// run arguments have changed or if the process is in a restart loop because of some error, +// which is important for the retry backoff manager. func (r Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, err error) { bin, err := r.cfg.CurrentBin() if err != nil { @@ -125,12 +132,15 @@ func (r Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, return } +// RunProcess runs the given command until either a upgrade is detected or the process exits. func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64) error { + // start the fsnotify watcher to watch for changes in the upgrade info directory dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.cfg.UpgradeInfoDir(), []string{ r.cfg.UpgradeInfoFilePath(), r.cfg.UpgradeInfoBatchFilePath(), }) if err != nil { + // if fsnotify is not available, we fall back to polling so we don't return an error here r.logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) } @@ -140,18 +150,25 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 ctx, cancel := context.WithCancel(ctx) defer cancel() + // start watchers for upgrade plans, manual upgrades and height updates upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker("http://localhost:8080/block") + // TODO should we have a separate poll interval for the height watcher? heightWatcher := watchers.NewHeightWatcher(ctx, heightChecker, r.cfg.PollInterval, func(height uint64) error { r.knownHeight = height return r.cfg.WriteLastKnownHeight(height) }) + if haltHeight > 0 { + // TODO start height watcher only if we have a halt height set + // currently the height watcher is always checking for the height even when we don't have a halt height set + } + r.logger.Info("Starting process", "path", cmd.Path, "args", cmd.Args) processRunner := RunProcess(cmd) defer func() { - // TODO always check for the latest block height before shutting down + // TODO always check for the latest block height before shutting down so that we have it in the last known height file _, _ = heightChecker.GetLatestBlockHeight() _ = processRunner.Shutdown(r.cfg.ShutdownGrace) }() @@ -197,7 +214,7 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 // we just return the error or absence of an error here, which will cause the process to restart with a backoff retry algorithm return err case actualHeight := <-heightWatcher.Updated(): - r.logger.Warn("Got height update from watcher", "height", actualHeight) + r.logger.Debug("Got height update from watcher", "height", actualHeight) if haltHeight == 0 { // we don't have a halt height, so we don't care to check anything about the actual height continue @@ -228,12 +245,13 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 r.logger.Info("Reached halt height, restarting process for upgrade") return ErrRestartNeeded{} } - // TODO error channels + // TODO listen to error channels } } } -// RunConfig defines the configuration for running a command +// RunConfig defines the configuration for running a command, +// essentially mapping its standard input, output, and error streams. type RunConfig struct { StdIn io.Reader StdOut io.Writer From d9affb927f967e50b70c89c818083f0245e91357 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 15:02:40 -0400 Subject: [PATCH 057/115] add additional notes --- tools/cosmovisor/internal/process.go | 1 + tools/cosmovisor/internal/runner.go | 1 + tools/cosmovisor/internal/upgrader.go | 3 +++ 3 files changed, 5 insertions(+) diff --git a/tools/cosmovisor/internal/process.go b/tools/cosmovisor/internal/process.go index 2cfea9b9d4d8..af923d4a4539 100644 --- a/tools/cosmovisor/internal/process.go +++ b/tools/cosmovisor/internal/process.go @@ -39,6 +39,7 @@ func (pr *ProcessRunner) Shutdown(shutdownGrace time.Duration) error { if proc == nil { return nil // process already exited } + // TODO make sure we don't kill a process that is not running if err := proc.Signal(syscall.SIGTERM); err != nil { return err } diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 570445b36cce..3ba396219d6a 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -182,6 +182,7 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 return errDone case _, ok := <-upgradePlanWatcher.Updated(): // TODO check skip upgrade heights?? (although not sure why we need this as the node should not emit an upgrade plan if skip heights is enabled) + // TODO should we double check we're at the right height in case operators manually create this file? if !ok { return nil } diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index 961b203ba311..0ff23a1e4f42 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -21,6 +21,8 @@ type UpgradeCheckResult struct { } func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint64) (upgraded bool, err error) { + // if we see upgrade-info.json, assume we are at the right height and upgrade + // TODO should we check the height when we have upgrade-info.json? logger.Info("Checking for upgrade-info.json") if upgradePlan, err := cfg.UpgradeInfo(); err == nil { upgrader := NewUpgrader(cfg, logger, upgradePlan, false) @@ -52,6 +54,7 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint err = cfg.DeleteManualUpgradeAtHeight(haltHeight) return true, err } else if lastKnownHeight > haltHeight { + // TODO should we just warn here? or actually return an error? return false, fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d", manualUpgrade.Name, manualUpgrade.Height, lastKnownHeight) } } From 94675f92604463cf752ddcf9155b793c06147ead Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 15:25:09 -0400 Subject: [PATCH 058/115] switch to just logging watcher errors --- tools/cosmovisor/internal/runner.go | 9 +- .../internal/watchers/data_watcher.go | 21 ++--- .../internal/watchers/data_watcher_test.go | 14 +--- .../internal/watchers/file_poll_watcher.go | 6 +- .../watchers/file_poll_watcher_test.go | 10 +-- .../internal/watchers/fsnotify_watcher.go | 11 +-- .../internal/watchers/height_watcher.go | 6 +- .../internal/watchers/hybrid_watcher.go | 16 +--- .../internal/watchers/log_watcher.go | 82 ------------------- .../internal/watchers/poll_watcher.go | 15 +--- tools/cosmovisor/internal/watchers/watcher.go | 13 +-- 11 files changed, 41 insertions(+), 162 deletions(-) delete mode 100644 tools/cosmovisor/internal/watchers/log_watcher.go diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 3ba396219d6a..4a9fb0c3f0f1 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -135,7 +135,7 @@ func (r Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, // RunProcess runs the given command until either a upgrade is detected or the process exits. func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64) error { // start the fsnotify watcher to watch for changes in the upgrade info directory - dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.cfg.UpgradeInfoDir(), []string{ + dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.logger, r.cfg.UpgradeInfoDir(), []string{ r.cfg.UpgradeInfoFilePath(), r.cfg.UpgradeInfoBatchFilePath(), }) @@ -151,11 +151,11 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 defer cancel() // start watchers for upgrade plans, manual upgrades and height updates - upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) - manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) + upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, r.logger, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) + manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, r.logger, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker("http://localhost:8080/block") // TODO should we have a separate poll interval for the height watcher? - heightWatcher := watchers.NewHeightWatcher(ctx, heightChecker, r.cfg.PollInterval, func(height uint64) error { + heightWatcher := watchers.NewHeightWatcher(ctx, r.logger, heightChecker, r.cfg.PollInterval, func(height uint64) error { r.knownHeight = height return r.cfg.WriteLastKnownHeight(height) }) @@ -246,7 +246,6 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 r.logger.Info("Reached halt height, restarting process for upgrade") return ErrRestartNeeded{} } - // TODO listen to error channels } } } diff --git a/tools/cosmovisor/internal/watchers/data_watcher.go b/tools/cosmovisor/internal/watchers/data_watcher.go index 7305c2dc3af9..49c637f78533 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher.go +++ b/tools/cosmovisor/internal/watchers/data_watcher.go @@ -2,19 +2,18 @@ package watchers import ( "context" + + "cosmossdk.io/log" ) type DataWatcher[T any] struct { outChan chan T - errChan chan error } -func NewDataWatcher[T any, I any](ctx context.Context, watcher Watcher[I], unmarshal func(I) (T, error)) *DataWatcher[T] { +func NewDataWatcher[T any, I any](ctx context.Context, logger log.Logger, watcher Watcher[I], unmarshal func(I) (T, error)) *DataWatcher[T] { outChan := make(chan T, 1) - errChan := make(chan error, 1) go func() { defer close(outChan) - defer close(errChan) for { select { case <-ctx.Done(): @@ -25,22 +24,16 @@ func NewDataWatcher[T any, I any](ctx context.Context, watcher Watcher[I], unmar } var data T data, err := unmarshal(contents) - // ignore errors because failing JSON unmarshal probably just means the file is incomplete if err == nil { outChan <- data + } else { + logger.Warn("failed to unmarshal data", "error", err) } - case err, ok := <-watcher.Errors(): - if !ok { - return - } - errChan <- err - } } }() return &DataWatcher[T]{ outChan: outChan, - errChan: errChan, } } @@ -48,7 +41,3 @@ func (d DataWatcher[T]) Updated() <-chan T { return d.outChan } - -func (d DataWatcher[T]) Errors() <-chan error { - return d.errChan -} diff --git a/tools/cosmovisor/internal/watchers/data_watcher_test.go b/tools/cosmovisor/internal/watchers/data_watcher_test.go index 35aa68bb345d..7f462db60e22 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/data_watcher_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "cosmossdk.io/log" "github.com/stretchr/testify/require" ) @@ -22,8 +23,9 @@ func TestDataWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile.json") ctx, cancel := context.WithCancel(context.Background()) - pollWatcher := NewFilePollWatcher(ctx, filename, time.Millisecond*100) - dataWatcher := NewDataWatcher[TestData](ctx, pollWatcher, func(contents []byte) (TestData, error) { + logger := log.NewTestLogger(t) + pollWatcher := NewFilePollWatcher(ctx, logger, filename, time.Millisecond*100) + dataWatcher := NewDataWatcher[TestData](ctx, logger, pollWatcher, func(contents []byte) (TestData, error) { var data TestData err := json.Unmarshal(contents, &data) return data, err @@ -64,11 +66,6 @@ func TestDataWatcher(t *testing.T) { return } actualContext = &content - case err, ok := <-dataWatcher.Errors(): - if !ok { - return - } - require.NoError(t, err) case <-ctx.Done(): return } @@ -81,7 +78,4 @@ func TestDataWatcher(t *testing.T) { // check that all the channels are closed _, open := <-dataWatcher.Updated() require.False(t, open) - _, open = <-dataWatcher.Errors() - require.False(t, open) - } diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher.go b/tools/cosmovisor/internal/watchers/file_poll_watcher.go index 50d454a48012..4183406f2959 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher.go @@ -5,9 +5,11 @@ import ( "fmt" "os" "time" + + "cosmossdk.io/log" ) -func NewFilePollWatcher(ctx context.Context, filename string, pollInterval time.Duration) Watcher[[]byte] { +func NewFilePollWatcher(ctx context.Context, logger log.Logger, filename string, pollInterval time.Duration) Watcher[[]byte] { stat, err := os.Stat(filename) var lastModTime time.Time if err == nil { @@ -33,5 +35,5 @@ func NewFilePollWatcher(ctx context.Context, filename string, pollInterval time. } return nil, os.ErrNotExist } - return NewPollWatcher[[]byte](ctx, check, pollInterval) + return NewPollWatcher[[]byte](ctx, logger, check, pollInterval) } diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go index 15244c547482..ab7d9bb08aac 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "cosmossdk.io/log" "github.com/stretchr/testify/require" ) @@ -16,7 +17,7 @@ func TestPollWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile") ctx, cancel := context.WithCancel(context.Background()) - watcher := NewFilePollWatcher(ctx, filename, time.Millisecond*100) + watcher := NewFilePollWatcher(ctx, log.NewTestLogger(t), filename, time.Millisecond*100) expectedContent := []byte("test") go func() { // write some dummy data to the file @@ -46,11 +47,6 @@ func TestPollWatcher(t *testing.T) { return } actualContent = bz - case err, ok := <-watcher.Errors(): - if !ok { - return - } - require.NoError(t, err) case <-ctx.Done(): return } @@ -63,7 +59,5 @@ func TestPollWatcher(t *testing.T) { // check that all the channels are closed _, open := <-watcher.Updated() require.False(t, open) - _, open = <-watcher.Errors() - require.False(t, open) } diff --git a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go index 9db5cff222c1..7690d16aab40 100644 --- a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go +++ b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go @@ -5,18 +5,18 @@ import ( "fmt" "os" + "cosmossdk.io/log" "github.com/fsnotify/fsnotify" ) type FSNotifyWatcher struct { watcher *fsnotify.Watcher outChan chan FileUpdate - errChan chan error } var _ Watcher[FileUpdate] = (*FSNotifyWatcher)(nil) -func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*FSNotifyWatcher, error) { +func NewFSNotifyWatcher(ctx context.Context, logger log.Logger, dir string, filenames []string) (*FSNotifyWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err @@ -68,7 +68,7 @@ func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*F if !ok { // channel closed return } - errChan <- fmt.Errorf("fsnotify error: %w", err) + logger.Error("fsnotify error", "error", err) } } }() @@ -76,7 +76,6 @@ func NewFSNotifyWatcher(ctx context.Context, dir string, filenames []string) (*F return &FSNotifyWatcher{ watcher: watcher, outChan: outChan, - errChan: errChan, }, nil } @@ -88,7 +87,3 @@ type FileUpdate struct { func (w *FSNotifyWatcher) Updated() <-chan FileUpdate { return w.outChan } - -func (w *FSNotifyWatcher) Errors() <-chan error { - return w.errChan -} diff --git a/tools/cosmovisor/internal/watchers/height_watcher.go b/tools/cosmovisor/internal/watchers/height_watcher.go index 92c7b3494d29..4977d900370b 100644 --- a/tools/cosmovisor/internal/watchers/height_watcher.go +++ b/tools/cosmovisor/internal/watchers/height_watcher.go @@ -3,6 +3,8 @@ package watchers import ( "context" "time" + + "cosmossdk.io/log" ) type HeightChecker interface { @@ -15,12 +17,12 @@ type HeightWatcher struct { onGetHeight func(uint64) error } -func NewHeightWatcher(ctx context.Context, checker HeightChecker, pollInterval time.Duration, onGetHeight func(uint64) error) *HeightWatcher { +func NewHeightWatcher(ctx context.Context, logger log.Logger, checker HeightChecker, pollInterval time.Duration, onGetHeight func(uint64) error) *HeightWatcher { watcher := &HeightWatcher{ checker: checker, onGetHeight: onGetHeight, } - watcher.PollWatcher = NewPollWatcher[uint64](ctx, func() (uint64, error) { + watcher.PollWatcher = NewPollWatcher[uint64](ctx, logger, func() (uint64, error) { return watcher.ReadNow() }, pollInterval) diff --git a/tools/cosmovisor/internal/watchers/hybrid_watcher.go b/tools/cosmovisor/internal/watchers/hybrid_watcher.go index 0827dfd6e11e..ad835d844ad2 100644 --- a/tools/cosmovisor/internal/watchers/hybrid_watcher.go +++ b/tools/cosmovisor/internal/watchers/hybrid_watcher.go @@ -3,6 +3,8 @@ package watchers import ( "context" "time" + + "cosmossdk.io/log" ) type HybridWatcher struct { @@ -12,8 +14,8 @@ type HybridWatcher struct { var _ Watcher[[]byte] = &HybridWatcher{} -func NewHybridWatcher(ctx context.Context, dirWatcher *FSNotifyWatcher, filename string, backupPollInterval time.Duration) *HybridWatcher { - pollWatcher := NewFilePollWatcher(ctx, filename, backupPollInterval) +func NewHybridWatcher(ctx context.Context, logger log.Logger, dirWatcher *FSNotifyWatcher, filename string, backupPollInterval time.Duration) *HybridWatcher { + pollWatcher := NewFilePollWatcher(ctx, logger, filename, backupPollInterval) outChan := make(chan []byte, 1) errChan := make(chan error, 1) @@ -36,16 +38,6 @@ func NewHybridWatcher(ctx context.Context, dirWatcher *FSNotifyWatcher, filename return } outChan <- update - case err, ok := <-dirWatcher.Errors(): - if !ok { - return - } - errChan <- err - case err, ok := <-pollWatcher.Errors(): - if !ok { - return - } - errChan <- err } } }() diff --git a/tools/cosmovisor/internal/watchers/log_watcher.go b/tools/cosmovisor/internal/watchers/log_watcher.go deleted file mode 100644 index 782b78a776cd..000000000000 --- a/tools/cosmovisor/internal/watchers/log_watcher.go +++ /dev/null @@ -1,82 +0,0 @@ -package watchers - -import ( - "bufio" - "context" - "fmt" - "io" - "os" - "regexp" -) - -func NewHaltHeightLogWatcher(ctx context.Context, log io.Reader, checker HeightChecker) Watcher[uint64] { - check := func(line string) (uint64, error) { - height, err := parseHaltHeightLogMessage(line) - if err != nil { - return 0, err - } - actualHeight, err := checker.GetLatestBlockHeight() - if err != nil { - return 0, err - } - if actualHeight < height { - // false positive, ignore this log line - return 0, os.ErrNotExist - } - return actualHeight, nil - } - return NewDataWatcher[uint64](ctx, NewLogWatcher(ctx, log, haltHeightRegex), check) -} - -var haltHeightRegex = regexp.MustCompile(`halt per configuration height (\d+)`) - -func parseHaltHeightLogMessage(line string) (uint64, error) { - matches := haltHeightRegex.FindStringSubmatch(line) - if len(matches) < 2 { - return 0, os.ErrNotExist // No match found - } - var height uint64 - _, err := fmt.Sscanf(matches[1], "%d", &height) - if err != nil { - return 0, err // Error parsing height - } - return height, nil -} - -type LogWatcher struct { - outChan chan string - errChan chan error -} - -func NewLogWatcher(ctx context.Context, log io.Reader, regex *regexp.Regexp) Watcher[string] { - out := make(chan string, 1) - err := make(chan error, 1) - go func() { - defer close(out) - defer close(err) - scanner := bufio.NewScanner(log) - for scanner.Scan() { - select { - case <-ctx.Done(): - return - default: - line := scanner.Text() - if regex.MatchString(line) { - out <- line - } - } - } - }() - return &LogWatcher{ - outChan: out, - errChan: err, - } -} - -func (l *LogWatcher) Updated() <-chan string { - return l.outChan -} - -func (l *LogWatcher) Errors() <-chan error { - return l.errChan -} diff --git a/tools/cosmovisor/internal/watchers/poll_watcher.go b/tools/cosmovisor/internal/watchers/poll_watcher.go index 3c5bf8d50696..ee0884fd1d77 100644 --- a/tools/cosmovisor/internal/watchers/poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/poll_watcher.go @@ -2,25 +2,23 @@ package watchers import ( "context" - "fmt" "os" "reflect" "time" + + "cosmossdk.io/log" ) type PollWatcher[T any] struct { outChan chan T - errChan chan error } -func NewPollWatcher[T any](ctx context.Context, checker func() (T, error), pollInterval time.Duration) *PollWatcher[T] { +func NewPollWatcher[T any](ctx context.Context, logger log.Logger, checker func() (T, error), pollInterval time.Duration) *PollWatcher[T] { outChan := make(chan T, 1) - errChan := make(chan error, 1) ticker := time.NewTicker(pollInterval) go func() { defer ticker.Stop() defer close(outChan) - defer close(errChan) for { select { @@ -30,7 +28,7 @@ func NewPollWatcher[T any](ctx context.Context, checker func() (T, error), pollI x, err := checker() if err != nil { if !os.IsNotExist(err) { - errChan <- fmt.Errorf("failed to check for updates: %w", err) + logger.Error("failed to check for updates: %w", "error", err) } } else { // to make PollWatcher generic on any type T (including []byte), we use reflect.DeepEqual and the default zero value of T @@ -44,14 +42,9 @@ func NewPollWatcher[T any](ctx context.Context, checker func() (T, error), pollI }() return &PollWatcher[T]{ outChan: outChan, - errChan: errChan, } } func (w *PollWatcher[T]) Updated() <-chan T { return w.outChan } - -func (w *PollWatcher[T]) Errors() <-chan error { - return w.errChan -} diff --git a/tools/cosmovisor/internal/watchers/watcher.go b/tools/cosmovisor/internal/watchers/watcher.go index 7b678ee99bde..ea302b592ee8 100644 --- a/tools/cosmovisor/internal/watchers/watcher.go +++ b/tools/cosmovisor/internal/watchers/watcher.go @@ -3,19 +3,20 @@ package watchers import ( "context" "time" + + "cosmossdk.io/log" ) type Watcher[T any] interface { Updated() <-chan T - Errors() <-chan error } -func InitWatcher[T any](ctx context.Context, pollInterval time.Duration, dirWatcher *FSNotifyWatcher, filename string, unmarshal func([]byte) (T, error)) Watcher[T] { +func InitWatcher[T any](ctx context.Context, logger log.Logger, pollInterval time.Duration, dirWatcher *FSNotifyWatcher, filename string, unmarshal func([]byte) (T, error)) Watcher[T] { if dirWatcher != nil { - hybridWatcher := NewHybridWatcher(ctx, dirWatcher, filename, pollInterval) - return NewDataWatcher[T](ctx, hybridWatcher, unmarshal) + hybridWatcher := NewHybridWatcher(ctx, logger, dirWatcher, filename, pollInterval) + return NewDataWatcher[T](ctx, logger, hybridWatcher, unmarshal) } else { - pollWatcher := NewFilePollWatcher(ctx, filename, pollInterval) - return NewDataWatcher[T](ctx, pollWatcher, unmarshal) + pollWatcher := NewFilePollWatcher(ctx, logger, filename, pollInterval) + return NewDataWatcher[T](ctx, logger, pollWatcher, unmarshal) } } From 83ef05218e25d85d5a4691b619953c41097d45c8 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 15:37:00 -0400 Subject: [PATCH 059/115] switch to ErrorHandler interface --- tools/cosmovisor/internal/runner.go | 7 ++-- .../internal/watchers/data_watcher.go | 6 +-- .../internal/watchers/data_watcher_test.go | 6 +-- .../internal/watchers/file_poll_watcher.go | 6 +-- .../watchers/file_poll_watcher_test.go | 3 +- .../internal/watchers/height_watcher.go | 6 +-- .../internal/watchers/hybrid_watcher.go | 6 +-- .../internal/watchers/poll_watcher.go | 6 +-- tools/cosmovisor/internal/watchers/watcher.go | 39 ++++++++++++++++--- 9 files changed, 53 insertions(+), 32 deletions(-) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 4a9fb0c3f0f1..450a0be0b152 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -151,11 +151,12 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 defer cancel() // start watchers for upgrade plans, manual upgrades and height updates - upgradePlanWatcher := watchers.InitWatcher[upgradetypes.Plan](ctx, r.logger, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) - manualUpgradesWatcher := watchers.InitWatcher[cosmovisor.ManualUpgradeBatch](ctx, r.logger, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) + eh := watchers.LoggerErrorHandler(r.logger) + upgradePlanWatcher := watchers.InitFileWatcher[upgradetypes.Plan](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) + manualUpgradesWatcher := watchers.InitFileWatcher[cosmovisor.ManualUpgradeBatch](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker("http://localhost:8080/block") // TODO should we have a separate poll interval for the height watcher? - heightWatcher := watchers.NewHeightWatcher(ctx, r.logger, heightChecker, r.cfg.PollInterval, func(height uint64) error { + heightWatcher := watchers.NewHeightWatcher(ctx, eh, heightChecker, r.cfg.PollInterval, func(height uint64) error { r.knownHeight = height return r.cfg.WriteLastKnownHeight(height) }) diff --git a/tools/cosmovisor/internal/watchers/data_watcher.go b/tools/cosmovisor/internal/watchers/data_watcher.go index 49c637f78533..bbfdce9b2675 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher.go +++ b/tools/cosmovisor/internal/watchers/data_watcher.go @@ -2,15 +2,13 @@ package watchers import ( "context" - - "cosmossdk.io/log" ) type DataWatcher[T any] struct { outChan chan T } -func NewDataWatcher[T any, I any](ctx context.Context, logger log.Logger, watcher Watcher[I], unmarshal func(I) (T, error)) *DataWatcher[T] { +func NewDataWatcher[T any, I any](ctx context.Context, errorHandler ErrorHandler, watcher Watcher[I], unmarshal func(I) (T, error)) *DataWatcher[T] { outChan := make(chan T, 1) go func() { defer close(outChan) @@ -27,7 +25,7 @@ func NewDataWatcher[T any, I any](ctx context.Context, logger log.Logger, watche if err == nil { outChan <- data } else { - logger.Warn("failed to unmarshal data", "error", err) + errorHandler.Warn("failed to unmarshal data", err) } } } diff --git a/tools/cosmovisor/internal/watchers/data_watcher_test.go b/tools/cosmovisor/internal/watchers/data_watcher_test.go index 7f462db60e22..d0d565464ea5 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/data_watcher_test.go @@ -23,9 +23,9 @@ func TestDataWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile.json") ctx, cancel := context.WithCancel(context.Background()) - logger := log.NewTestLogger(t) - pollWatcher := NewFilePollWatcher(ctx, logger, filename, time.Millisecond*100) - dataWatcher := NewDataWatcher[TestData](ctx, logger, pollWatcher, func(contents []byte) (TestData, error) { + eh := LoggerErrorHandler(log.NewTestLogger(t)) + pollWatcher := NewFilePollWatcher(ctx, eh, filename, time.Millisecond*100) + dataWatcher := NewDataWatcher[TestData](ctx, eh, pollWatcher, func(contents []byte) (TestData, error) { var data TestData err := json.Unmarshal(contents, &data) return data, err diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher.go b/tools/cosmovisor/internal/watchers/file_poll_watcher.go index 4183406f2959..a35d6627399f 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher.go @@ -5,11 +5,9 @@ import ( "fmt" "os" "time" - - "cosmossdk.io/log" ) -func NewFilePollWatcher(ctx context.Context, logger log.Logger, filename string, pollInterval time.Duration) Watcher[[]byte] { +func NewFilePollWatcher(ctx context.Context, errorHandler ErrorHandler, filename string, pollInterval time.Duration) Watcher[[]byte] { stat, err := os.Stat(filename) var lastModTime time.Time if err == nil { @@ -35,5 +33,5 @@ func NewFilePollWatcher(ctx context.Context, logger log.Logger, filename string, } return nil, os.ErrNotExist } - return NewPollWatcher[[]byte](ctx, logger, check, pollInterval) + return NewPollWatcher[[]byte](ctx, errorHandler, check, pollInterval) } diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go index ab7d9bb08aac..75ade08bb073 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go @@ -17,7 +17,8 @@ func TestPollWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile") ctx, cancel := context.WithCancel(context.Background()) - watcher := NewFilePollWatcher(ctx, log.NewTestLogger(t), filename, time.Millisecond*100) + eh := LoggerErrorHandler(log.NewTestLogger(t)) + watcher := NewFilePollWatcher(ctx, eh, filename, time.Millisecond*100) expectedContent := []byte("test") go func() { // write some dummy data to the file diff --git a/tools/cosmovisor/internal/watchers/height_watcher.go b/tools/cosmovisor/internal/watchers/height_watcher.go index 4977d900370b..d26091b956d1 100644 --- a/tools/cosmovisor/internal/watchers/height_watcher.go +++ b/tools/cosmovisor/internal/watchers/height_watcher.go @@ -3,8 +3,6 @@ package watchers import ( "context" "time" - - "cosmossdk.io/log" ) type HeightChecker interface { @@ -17,12 +15,12 @@ type HeightWatcher struct { onGetHeight func(uint64) error } -func NewHeightWatcher(ctx context.Context, logger log.Logger, checker HeightChecker, pollInterval time.Duration, onGetHeight func(uint64) error) *HeightWatcher { +func NewHeightWatcher(ctx context.Context, errorHandler ErrorHandler, checker HeightChecker, pollInterval time.Duration, onGetHeight func(uint64) error) *HeightWatcher { watcher := &HeightWatcher{ checker: checker, onGetHeight: onGetHeight, } - watcher.PollWatcher = NewPollWatcher[uint64](ctx, logger, func() (uint64, error) { + watcher.PollWatcher = NewPollWatcher[uint64](ctx, errorHandler, func() (uint64, error) { return watcher.ReadNow() }, pollInterval) diff --git a/tools/cosmovisor/internal/watchers/hybrid_watcher.go b/tools/cosmovisor/internal/watchers/hybrid_watcher.go index ad835d844ad2..1197722e3d4e 100644 --- a/tools/cosmovisor/internal/watchers/hybrid_watcher.go +++ b/tools/cosmovisor/internal/watchers/hybrid_watcher.go @@ -3,8 +3,6 @@ package watchers import ( "context" "time" - - "cosmossdk.io/log" ) type HybridWatcher struct { @@ -14,8 +12,8 @@ type HybridWatcher struct { var _ Watcher[[]byte] = &HybridWatcher{} -func NewHybridWatcher(ctx context.Context, logger log.Logger, dirWatcher *FSNotifyWatcher, filename string, backupPollInterval time.Duration) *HybridWatcher { - pollWatcher := NewFilePollWatcher(ctx, logger, filename, backupPollInterval) +func NewHybridWatcher(ctx context.Context, errorHandler ErrorHandler, dirWatcher *FSNotifyWatcher, filename string, backupPollInterval time.Duration) *HybridWatcher { + pollWatcher := NewFilePollWatcher(ctx, errorHandler, filename, backupPollInterval) outChan := make(chan []byte, 1) errChan := make(chan error, 1) diff --git a/tools/cosmovisor/internal/watchers/poll_watcher.go b/tools/cosmovisor/internal/watchers/poll_watcher.go index ee0884fd1d77..c15f3134ef80 100644 --- a/tools/cosmovisor/internal/watchers/poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/poll_watcher.go @@ -5,15 +5,13 @@ import ( "os" "reflect" "time" - - "cosmossdk.io/log" ) type PollWatcher[T any] struct { outChan chan T } -func NewPollWatcher[T any](ctx context.Context, logger log.Logger, checker func() (T, error), pollInterval time.Duration) *PollWatcher[T] { +func NewPollWatcher[T any](ctx context.Context, errorHandler ErrorHandler, checker func() (T, error), pollInterval time.Duration) *PollWatcher[T] { outChan := make(chan T, 1) ticker := time.NewTicker(pollInterval) go func() { @@ -28,7 +26,7 @@ func NewPollWatcher[T any](ctx context.Context, logger log.Logger, checker func( x, err := checker() if err != nil { if !os.IsNotExist(err) { - logger.Error("failed to check for updates: %w", "error", err) + errorHandler.Error("failed to check for updates", err) } } else { // to make PollWatcher generic on any type T (including []byte), we use reflect.DeepEqual and the default zero value of T diff --git a/tools/cosmovisor/internal/watchers/watcher.go b/tools/cosmovisor/internal/watchers/watcher.go index ea302b592ee8..26975092d83f 100644 --- a/tools/cosmovisor/internal/watchers/watcher.go +++ b/tools/cosmovisor/internal/watchers/watcher.go @@ -7,16 +7,45 @@ import ( "cosmossdk.io/log" ) +// Watcher is an interface that defines a generic watcher that emits updates of type T. type Watcher[T any] interface { + // Updated returns a channel that emits updates of type T. Updated() <-chan T } -func InitWatcher[T any](ctx context.Context, logger log.Logger, pollInterval time.Duration, dirWatcher *FSNotifyWatcher, filename string, unmarshal func([]byte) (T, error)) Watcher[T] { +// ErrorHandler is an interface for handling errors and warnings in watchers. +type ErrorHandler interface { + // Error handles an error as an error. + Error(msg string, err error) + // Warn handles an error as a warning. + Warn(msg string, err error) +} + +type loggerErrorHandler struct { + logger log.Logger +} + +func (h *loggerErrorHandler) Error(msg string, err error) { + h.logger.Error(msg, "error", err) +} + +func (h *loggerErrorHandler) Warn(msg string, err error) { + h.logger.Warn(msg, "error", err) +} + +// LoggerErrorHandler returns an ErrorHandler that logs errors and warnings using the provided logger. +func LoggerErrorHandler(logger log.Logger) ErrorHandler { + return &loggerErrorHandler{logger: logger} +} + +// InitFileWatcher initializes a file watcher which uses either both fsnotify and polling (hybrid watcher) or just polling, +// depending on whether a fsnotify directory watcher is provided. +func InitFileWatcher[T any](ctx context.Context, errorHandler ErrorHandler, pollInterval time.Duration, dirWatcher *FSNotifyWatcher, filename string, unmarshal func([]byte) (T, error)) Watcher[T] { if dirWatcher != nil { - hybridWatcher := NewHybridWatcher(ctx, logger, dirWatcher, filename, pollInterval) - return NewDataWatcher[T](ctx, logger, hybridWatcher, unmarshal) + hybridWatcher := NewHybridWatcher(ctx, errorHandler, dirWatcher, filename, pollInterval) + return NewDataWatcher[T](ctx, errorHandler, hybridWatcher, unmarshal) } else { - pollWatcher := NewFilePollWatcher(ctx, logger, filename, pollInterval) - return NewDataWatcher[T](ctx, logger, pollWatcher, unmarshal) + pollWatcher := NewFilePollWatcher(ctx, errorHandler, filename, pollInterval) + return NewDataWatcher[T](ctx, errorHandler, pollWatcher, unmarshal) } } From c9d0e11c564be60e0980730255681d92c2a12732 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 16:58:56 -0400 Subject: [PATCH 060/115] sniffing for /block or /v1/block --- tools/cosmovisor/args.go | 12 ++- .../cmd/cosmovisor/mockchain_test.go | 5 +- tools/cosmovisor/cmd/mock_node/main.go | 2 +- tools/cosmovisor/internal/runner.go | 2 +- .../internal/watchers/http_block.go | 62 ------------- .../internal/watchers/http_block_checker.go | 91 +++++++++++++++++++ tools/cosmovisor/manual.go | 16 ---- 7 files changed, 103 insertions(+), 87 deletions(-) delete mode 100644 tools/cosmovisor/internal/watchers/http_block.go create mode 100644 tools/cosmovisor/internal/watchers/http_block_checker.go diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index a3e9d58950a6..ef39c87dd740 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -36,6 +36,7 @@ const ( EnvInterval = "DAEMON_POLL_INTERVAL" EnvPreupgradeMaxRetries = "DAEMON_PREUPGRADE_MAX_RETRIES" EnvGRPCAddress = "DAEMON_GRPC_ADDRESS" + EnvRPCAddress = "DAEMON_RPC_ADDRESS" EnvDisableLogs = "COSMOVISOR_DISABLE_LOGS" EnvColorLogs = "COSMOVISOR_COLOR_LOGS" EnvTimeFormatLogs = "COSMOVISOR_TIMEFORMAT_LOGS" @@ -66,6 +67,7 @@ type Config struct { UnsafeSkipBackup bool `toml:"unsafe_skip_backup" mapstructure:"unsafe_skip_backup" default:"false"` DataBackupPath string `toml:"daemon_data_backup_dir" mapstructure:"daemon_data_backup_dir"` PreUpgradeMaxRetries int `toml:"daemon_preupgrade_max_retries" mapstructure:"daemon_preupgrade_max_retries" default:"0"` + RPCAddress string `toml:"daemon_rpc_address" mapstructure:"daemon_rpc_address" default:"http://localhost:26657"` GRPCAddress string `toml:"daemon_grpc_address" mapstructure:"daemon_grpc_address"` DisableLogs bool `toml:"cosmovisor_disable_logs" mapstructure:"cosmovisor_disable_logs" default:"false"` ColorLogs bool `toml:"cosmovisor_color_logs" mapstructure:"cosmovisor_color_logs" default:"true"` @@ -305,6 +307,11 @@ func GetConfigFromEnv(skipValidate bool) (*Config, error) { cfg.GRPCAddress = "localhost:9090" } + cfg.RPCAddress = os.Getenv(EnvRPCAddress) + if cfg.RPCAddress == "" { + cfg.RPCAddress = "http://localhost:26657" + } + if !skipValidate { errs = append(errs, cfg.validate()...) if len(errs) > 0 { @@ -657,11 +664,6 @@ func (cfg Config) Export() (string, error) { return path, nil } -func (cfg *Config) DeleteManualUpgradeAtHeight(height uint64) error { - // TODO - return nil -} - func askForConfirmation(str string) bool { var response string fmt.Printf("%s [y/n]: ", str) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index a0857ac38531..710393d28c70 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -81,10 +81,11 @@ func (m MockChainSetup) Setup(t *testing.T) (string, string) { } func TestMockChain(t *testing.T) { - pollInterval := time.Millisecond * 500 + pollInterval := time.Second cfg := &cosmovisor.Config{ PollInterval: pollInterval, RestartAfterUpgrade: true, + RPCAddress: "http://localhost:26657", // TODO this should be the default! } mockchainDir, cfgFile := MockChainSetup{ Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", @@ -93,7 +94,7 @@ func TestMockChain(t *testing.T) { }, ManualUpgrades: map[string]string{ "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", - "manual20": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", + "manual20": `--block-time 1s --upgrade-plan '{"name":"gov1","height":30}' --block-url "/v1/block"`, "manual40": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":50}'", }, Config: cfg, diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 770359960ec4..11e7a8326e03 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -45,7 +45,7 @@ x/upgrade upgrade-info.json behavior.`, cmd.Flags().StringVar(&upgradePlan, "upgrade-plan", "", "upgrade-info.json to create after the halt duration is reached. Either this flag or --halt-height must be specified but not both.") cmd.Flags().Uint64Var(&haltHeight, server.FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node. E") cmd.Flags().StringVar(&homePath, "home", "", "Home directory for the mock node. upgrade-info.json will be written to the data sub-directory of this directory. Defaults to the current directory.") - cmd.Flags().StringVar(&httpAddr, "http-addr", ":8080", "HTTP server address to serve block information. Defaults to :8080.") + cmd.Flags().StringVar(&httpAddr, "http-addr", ":26657", "HTTP server address to serve block information. Defaults to :26657.") cmd.Flags().StringVar(&blockUrl, "block-url", "/block", "URL at which the latest block information is served. Defaults to /block.") cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", 0, "Duration to wait before shutting down the node upon receiving a shutdown signal. Defaults to 0 (no delay).") // TODO add flag to use either jsonpb or encoding/json diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 450a0be0b152..f156ccb843ed 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -154,7 +154,7 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 eh := watchers.LoggerErrorHandler(r.logger) upgradePlanWatcher := watchers.InitFileWatcher[upgradetypes.Plan](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) manualUpgradesWatcher := watchers.InitFileWatcher[cosmovisor.ManualUpgradeBatch](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) - heightChecker := watchers.NewHTTPRPCBLockChecker("http://localhost:8080/block") + heightChecker := watchers.NewHTTPRPCBLockChecker(r.cfg.RPCAddress, r.logger) // TODO should we have a separate poll interval for the height watcher? heightWatcher := watchers.NewHeightWatcher(ctx, eh, heightChecker, r.cfg.PollInterval, func(height uint64) error { r.knownHeight = height diff --git a/tools/cosmovisor/internal/watchers/http_block.go b/tools/cosmovisor/internal/watchers/http_block.go deleted file mode 100644 index 9fcdc3e917b1..000000000000 --- a/tools/cosmovisor/internal/watchers/http_block.go +++ /dev/null @@ -1,62 +0,0 @@ -package watchers - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" -) - -func NewHTTPRPCBLockChecker(url string) HeightChecker { - // TODO we want to include the ability to sniff for /block or /v1/block - return httpRPCBlockChecker{ - url: url, - } -} - -type httpRPCBlockChecker struct { - url string -} - -func (j httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { - res, err := http.Get(j.url) - if err != nil { - return 0, fmt.Errorf("failed to get latest block height: %w", err) - } - defer res.Body.Close() - - bz, err := io.ReadAll(res.Body) - if err != nil { - return 0, fmt.Errorf("failed to read latest block height: %w", err) - } - - return getHeightFromRPCBlockResponse(bz) -} - -var _ HeightChecker = httpRPCBlockChecker{} - -type Header struct { - Height string `json:"height"` -} -type Block struct { - Header Header `json:"header"` -} -type Result struct { - Block Block `json:"block"` -} -type Response struct { - Result Result `json:"result"` -} - -func getHeightFromRPCBlockResponse(bz []byte) (uint64, error) { - - var response Response - err := json.Unmarshal(bz, &response) - if err != nil { - return 0, fmt.Errorf("failed to unmarshal block response: %w", err) - } - - height := response.Result.Block.Header.Height - return strconv.ParseUint(height, 10, 64) -} diff --git a/tools/cosmovisor/internal/watchers/http_block_checker.go b/tools/cosmovisor/internal/watchers/http_block_checker.go new file mode 100644 index 000000000000..5bddeb940121 --- /dev/null +++ b/tools/cosmovisor/internal/watchers/http_block_checker.go @@ -0,0 +1,91 @@ +package watchers + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + + "cosmossdk.io/log" +) + +func NewHTTPRPCBLockChecker(baseUrl string, logger log.Logger) HeightChecker { + return httpRPCBlockChecker{ + baseUrl: baseUrl, + logger: logger, + } +} + +type httpRPCBlockChecker struct { + baseUrl string + subUrl string + logger log.Logger +} + +func (j httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { + if j.subUrl != "" { + return j.getLatestBlockHeight(j.subUrl) + } + + height, err1 := j.getLatestBlockHeight("/v1/block") + if err1 == nil { + j.logger.Info("Successfully resolved latest block height from /v1/block", "url", j.baseUrl+"/v1/block") + // If we successfully got the height from /v1/block, we can cache the subUrl + j.subUrl = "/v1/block" + return height, nil + } + + height, err2 := j.getLatestBlockHeight("/block") + if err2 == nil { + j.logger.Info("Successfully resolved latest block height from /block", "url", j.baseUrl+"/block") + // If we successfully got the height from /block, we can cache the subUrl + j.subUrl = "/block" + return height, nil + } + + return 0, fmt.Errorf("failed to get latest block height from both /block and /v1/block RPC endpoints: %w", errors.Join(err1, err2)) +} + +func (j httpRPCBlockChecker) getLatestBlockHeight(subUrl string) (uint64, error) { + url := j.baseUrl + subUrl + res, err := http.Get(url) + if err != nil { + return 0, fmt.Errorf("failed to get latest block height: %w", err) + } + defer res.Body.Close() + + bz, err := io.ReadAll(res.Body) + if err != nil { + return 0, fmt.Errorf("failed to read latest block height: %w", err) + } + + return getHeightFromRPCBlockResponse(bz) +} + +var _ HeightChecker = httpRPCBlockChecker{} + +type Header struct { + Height string `json:"height"` +} +type Block struct { + Header Header `json:"header"` +} +type Result struct { + Block Block `json:"block"` +} +type Response struct { + Result Result `json:"result"` +} + +func getHeightFromRPCBlockResponse(bz []byte) (uint64, error) { + var response Response + err := json.Unmarshal(bz, &response) + if err != nil { + return 0, fmt.Errorf("failed to unmarshal block response: %w", err) + } + + height := response.Result.Block.Header.Height + return strconv.ParseUint(height, 10, 64) +} diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index e0583f05b7c6..21a6a65fa0e0 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -130,19 +130,3 @@ func (m ManualUpgradeBatch) FirstUpgrade() *upgradetypes.Plan { } return m[0] } - -//type ManualUpgradePlan struct { -// Name string `json:"name"` -// Height int64 `json:"height"` -// Info string `json:"info"` -//} -// -//func (m ManualUpgradePlan) ValidateBasic() error { -// if m.Name == "" { -// return fmt.Errorf("name cannot be empty") -// } -// if m.Height <= 0 { -// return fmt.Errorf("height must be greater than 0") -// } -// return nil -//} From 7b128d27cbca78904830ca9a786ca0c7c37548d5 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 17:31:25 -0400 Subject: [PATCH 061/115] refactor file deletion --- tools/cosmovisor/internal/upgrader.go | 54 ++++++++++++--------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index 0ff23a1e4f42..e9ce152b2aa4 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -25,11 +25,17 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint // TODO should we check the height when we have upgrade-info.json? logger.Info("Checking for upgrade-info.json") if upgradePlan, err := cfg.UpgradeInfo(); err == nil { - upgrader := NewUpgrader(cfg, logger, upgradePlan, false) - err := upgrader.DoUpgrade() + err := DoUpgrade(cfg, logger, upgradePlan) if err != nil { return false, err } + // remove the upgrade-info.json file after a successful upgrade, otherwise we will keep trying to upgrade + logger.Info("Removing completed upgrade plan", "height", upgradePlan.Height, "name", upgradePlan.Name) + file := cfg.UpgradeInfoFilePath() + err = os.Remove(file) + if err != nil { + return true, fmt.Errorf("failed to remove upgrade-info.json: %w", err) + } return true, nil } logger.Info("Checking for upgrade-info.json.batch") @@ -46,12 +52,16 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint haltHeight := uint64(manualUpgrade.Height) if lastKnownHeight == haltHeight { logger.Info("At manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) - upgrader := NewUpgrader(cfg, logger, *manualUpgrade, true) - err := upgrader.DoUpgrade() + err := DoUpgrade(cfg, logger, *manualUpgrade) if err != nil { return false, err } - err = cfg.DeleteManualUpgradeAtHeight(haltHeight) + // remove the manual upgrade plan after a successful upgrade, otherwise we will keep trying to upgrade + logger.Info("Removing completed manual upgrade plan", "height", manualUpgrade.Height, "name", manualUpgrade.Name) + err = cfg.RemoveManualUpgrade(manualUpgrade.Height) + if err != nil { + return true, fmt.Errorf("failed to remove manual upgrade at height %d: %w", manualUpgrade.Height, err) + } return true, err } else if lastKnownHeight > haltHeight { // TODO should we just warn here? or actually return an error? @@ -62,19 +72,18 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint } type Upgrader struct { - cfg *cosmovisor.Config - logger log.Logger - upgradePlan upgradetypes.Plan - isManualUpgrade bool + cfg *cosmovisor.Config + logger log.Logger + upgradePlan upgradetypes.Plan } -func NewUpgrader(cfg *cosmovisor.Config, logger log.Logger, upgradePlan upgradetypes.Plan, isManualUpgrade bool) *Upgrader { - return &Upgrader{ - cfg: cfg, - logger: logger, - upgradePlan: upgradePlan, - isManualUpgrade: isManualUpgrade, +func DoUpgrade(cfg *cosmovisor.Config, logger log.Logger, upgradePlan upgradetypes.Plan) error { + upgrader := &Upgrader{ + cfg: cfg, + logger: logger, + upgradePlan: upgradePlan, } + return upgrader.DoUpgrade() } func (u *Upgrader) DoUpgrade() error { @@ -107,21 +116,6 @@ func (u *Upgrader) DoUpgrade() error { return err } - if u.isManualUpgrade { - u.logger.Info("Removing completed manual upgrade plan", "height", u.upgradePlan.Height, "name", u.upgradePlan.Name) - err := u.cfg.RemoveManualUpgrade(u.upgradePlan.Height) - if err != nil { - return fmt.Errorf("failed to remove manual upgrade at height %d: %w", u.upgradePlan.Height, err) - } - } else { - u.logger.Info("Removing completed upgrade plan", "height", u.upgradePlan.Height, "name", u.upgradePlan.Name) - file := u.cfg.UpgradeInfoFilePath() - err := os.Remove(file) - if err != nil { - return fmt.Errorf("failed to remove upgrade-info.json: %w", err) - } - } - return nil } From b429d86e0318a5116da5b090a3b0dad9132b3b0b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 17:40:32 -0400 Subject: [PATCH 062/115] test case with node shutting down on upgrade --- .../cmd/cosmovisor/mockchain_test.go | 2 +- tools/cosmovisor/cmd/mock_node/main.go | 44 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 710393d28c70..8f2beb489131 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -94,7 +94,7 @@ func TestMockChain(t *testing.T) { }, ManualUpgrades: map[string]string{ "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", - "manual20": `--block-time 1s --upgrade-plan '{"name":"gov1","height":30}' --block-url "/v1/block"`, + "manual20": `--block-time 1s --upgrade-plan '{"name":"gov1","height":30}' --block-url "/v1/block" --shutdown-on-upgrade`, "manual40": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":50}'", }, Config: cfg, diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 11e7a8326e03..9fa8cf14f882 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -41,6 +41,7 @@ x/upgrade upgrade-info.json behavior.`, var httpAddr string var blockUrl string var shutdownDelay time.Duration + var shutdownOnUpgrade bool cmd.Flags().DurationVar(&blockTime, "block-time", 0, "Duration of time between blocks. This is required to simulate a progression of blocks over time.") cmd.Flags().StringVar(&upgradePlan, "upgrade-plan", "", "upgrade-info.json to create after the halt duration is reached. Either this flag or --halt-height must be specified but not both.") cmd.Flags().Uint64Var(&haltHeight, server.FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node. E") @@ -48,6 +49,7 @@ x/upgrade upgrade-info.json behavior.`, cmd.Flags().StringVar(&httpAddr, "http-addr", ":26657", "HTTP server address to serve block information. Defaults to :26657.") cmd.Flags().StringVar(&blockUrl, "block-url", "/block", "URL at which the latest block information is served. Defaults to /block.") cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", 0, "Duration to wait before shutting down the node upon receiving a shutdown signal. Defaults to 0 (no delay).") + cmd.Flags().BoolVar(&shutdownOnUpgrade, "shutdown-on-upgrade", false, "If true, the node will shutdown immediately after reaching the upgrade height. If false, it will continue running until a shutdown signal is received. Defaults to false.") // TODO add flag to use either jsonpb or encoding/json // TODO shutdown at upgrade height cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -65,14 +67,15 @@ x/upgrade upgrade-info.json behavior.`, } } node := &MockNode{ - height: 0, - blockTime: blockTime, - haltHeight: haltHeight, - homePath: homePath, - httpAddr: httpAddr, - blockUrl: blockUrl, - shutdownDelay: shutdownDelay, - logger: log.NewLogger(os.Stdout), + height: 0, + blockTime: blockTime, + haltHeight: haltHeight, + homePath: homePath, + httpAddr: httpAddr, + blockUrl: blockUrl, + shutdownDelay: shutdownDelay, + shutdownOnUpgrade: shutdownOnUpgrade, + logger: log.NewLogger(os.Stdout), } if upgradePlan != "" { node.upgradePlan = &upgradetypes.Plan{} @@ -92,18 +95,21 @@ x/upgrade upgrade-info.json behavior.`, } type MockNode struct { - height uint64 - blockTime time.Duration - upgradePlan *upgradetypes.Plan - haltHeight uint64 - homePath string - httpAddr string - blockUrl string - logger log.Logger - shutdownDelay time.Duration + height uint64 + blockTime time.Duration + upgradePlan *upgradetypes.Plan + haltHeight uint64 + homePath string + httpAddr string + blockUrl string + logger log.Logger + shutdownDelay time.Duration + shutdownOnUpgrade bool } func (n *MockNode) Run(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() ctx, _ = signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) upgradeHeight := n.haltHeight if n.upgradePlan != nil { @@ -170,6 +176,10 @@ func (n *MockNode) Run(ctx context.Context) error { return fmt.Errorf("failed to write upgrade-info.json: %w", err) } } + if n.shutdownOnUpgrade { + n.logger.Info("Mock node reached upgrade height, configured to shut down immediately") + return nil + } // Don't exit until we receive a shutdown signal n.logger.Info("Mock node reached upgrade height, waiting for shutdown signal") <-ctx.Done() From 7d3c0a35f3a23b27ed9b8a1348fc5f5ce7f2088d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 18:00:30 -0400 Subject: [PATCH 063/115] add additional test --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 8f2beb489131..f491b6b1f081 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -91,11 +91,12 @@ func TestMockChain(t *testing.T) { Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", GovUpgrades: map[string]string{ "gov1": "--block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":50}'", + "gov2": "--block-time 1s --upgrade-plan '{\"name\":\"gov3\",\"height\":70}'", }, ManualUpgrades: map[string]string{ "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", "manual20": `--block-time 1s --upgrade-plan '{"name":"gov1","height":30}' --block-url "/v1/block" --shutdown-on-upgrade`, - "manual40": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":50}'", + "manual40": "--block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":50}'", }, Config: cfg, }.Setup(t) @@ -174,6 +175,9 @@ func TestMockChain(t *testing.T) { case 7: // should have upgraded to manual40 require.Contains(t, currentBin, "manual40") + case 8: + // should have upgraded to manual40 + require.Contains(t, currentBin, "gov2") // this is the end of our test so we shutdown after a bit here go func() { time.Sleep(pollInterval * 2) @@ -194,7 +198,7 @@ func TestMockChain(t *testing.T) { }() wg.Wait() - require.Equal(t, 7, callbackCount) + require.Equal(t, 8, callbackCount) // TODO: // - [x] add callback on restart for checking state From 7a6ece7a6a1a59f328f0c7256476166033c72b7c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 9 Jun 2025 18:30:40 -0400 Subject: [PATCH 064/115] include test to check both json encodings work --- .../cmd/cosmovisor/mockchain_test.go | 2 +- tools/cosmovisor/cmd/mock_node/main.go | 58 +++++++++++-------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index f491b6b1f081..d6bca8c2541d 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -96,7 +96,7 @@ func TestMockChain(t *testing.T) { ManualUpgrades: map[string]string{ "manual10": "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", "manual20": `--block-time 1s --upgrade-plan '{"name":"gov1","height":30}' --block-url "/v1/block" --shutdown-on-upgrade`, - "manual40": "--block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":50}'", + "manual40": "--block-time 1s --upgrade-plan '{\"name\":\"gov2\",\"height\":50}' --upgrade-info-encoding-json", }, Config: cfg, }.Setup(t) diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 9fa8cf14f882..87e0d99476e8 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -24,8 +24,6 @@ import ( ) func main() { - // TODO response to GetLatestBlock, status, and write leveldb block number - cmd := &cobra.Command{ Use: "mock_node", Short: "A mock node for testing cosmovisor.", @@ -42,6 +40,7 @@ x/upgrade upgrade-info.json behavior.`, var blockUrl string var shutdownDelay time.Duration var shutdownOnUpgrade bool + var upgradeInfoEncodingJson bool cmd.Flags().DurationVar(&blockTime, "block-time", 0, "Duration of time between blocks. This is required to simulate a progression of blocks over time.") cmd.Flags().StringVar(&upgradePlan, "upgrade-plan", "", "upgrade-info.json to create after the halt duration is reached. Either this flag or --halt-height must be specified but not both.") cmd.Flags().Uint64Var(&haltHeight, server.FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node. E") @@ -50,6 +49,7 @@ x/upgrade upgrade-info.json behavior.`, cmd.Flags().StringVar(&blockUrl, "block-url", "/block", "URL at which the latest block information is served. Defaults to /block.") cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", 0, "Duration to wait before shutting down the node upon receiving a shutdown signal. Defaults to 0 (no delay).") cmd.Flags().BoolVar(&shutdownOnUpgrade, "shutdown-on-upgrade", false, "If true, the node will shutdown immediately after reaching the upgrade height. If false, it will continue running until a shutdown signal is received. Defaults to false.") + cmd.Flags().BoolVar(&upgradeInfoEncodingJson, "upgrade-info-encoding-json", false, "If true, the upgrade-info.json will be encoded using encoding/json instead of jsonpb. This is useful for testing compatibility with different JSON decoders. Defaults to false (uses jsonpb).") // TODO add flag to use either jsonpb or encoding/json // TODO shutdown at upgrade height cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -67,15 +67,16 @@ x/upgrade upgrade-info.json behavior.`, } } node := &MockNode{ - height: 0, - blockTime: blockTime, - haltHeight: haltHeight, - homePath: homePath, - httpAddr: httpAddr, - blockUrl: blockUrl, - shutdownDelay: shutdownDelay, - shutdownOnUpgrade: shutdownOnUpgrade, - logger: log.NewLogger(os.Stdout), + height: 0, + blockTime: blockTime, + haltHeight: haltHeight, + homePath: homePath, + httpAddr: httpAddr, + blockUrl: blockUrl, + shutdownDelay: shutdownDelay, + shutdownOnUpgrade: shutdownOnUpgrade, + upgradeInfoEncodingJson: upgradeInfoEncodingJson, + logger: log.NewLogger(os.Stdout), } if upgradePlan != "" { node.upgradePlan = &upgradetypes.Plan{} @@ -95,16 +96,17 @@ x/upgrade upgrade-info.json behavior.`, } type MockNode struct { - height uint64 - blockTime time.Duration - upgradePlan *upgradetypes.Plan - haltHeight uint64 - homePath string - httpAddr string - blockUrl string - logger log.Logger - shutdownDelay time.Duration - shutdownOnUpgrade bool + height uint64 + blockTime time.Duration + upgradePlan *upgradetypes.Plan + haltHeight uint64 + homePath string + httpAddr string + blockUrl string + logger log.Logger + shutdownDelay time.Duration + shutdownOnUpgrade bool + upgradeInfoEncodingJson bool } func (n *MockNode) Run(ctx context.Context) error { @@ -161,9 +163,17 @@ func (n *MockNode) Run(ctx context.Context) error { } else if n.upgradePlan != nil { n.logger.Info("Mock node reached upgrade height, writing upgrade-info.json", "upgrade_plan", n.upgradePlan) upgradeInfoPath := path.Join(n.homePath, "data", upgradetypes.UpgradeInfoFilename) - out, err := (&jsonpb.Marshaler{ - EmitDefaults: false, - }).MarshalToString(n.upgradePlan) + var out string + var err error + if n.upgradeInfoEncodingJson { + var bz []byte + bz, err = json.Marshal(n.upgradePlan) + out = string(bz) + } else { + out, err = (&jsonpb.Marshaler{ + EmitDefaults: false, + }).MarshalToString(n.upgradePlan) + } if err != nil { return fmt.Errorf("failed to marshal upgrade plan: %w", err) } From 85b75bdd36b7ad52f5923f5cf554d40ee28b7cde Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 10 Jun 2025 16:04:45 -0400 Subject: [PATCH 065/115] make more code internal --- tools/cosmovisor/args_test.go | 1 + .../cmd/cosmovisor/prepare_upgrade.go | 3 +- .../cosmovisor/{ => internal}/process_test.go | 33 ++++++++++--------- tools/cosmovisor/{ => internal}/skip.go | 5 +-- tools/cosmovisor/{ => internal}/upgrade.go | 5 +-- .../cosmovisor/{ => internal}/upgrade_test.go | 13 ++++---- tools/cosmovisor/internal/upgrader.go | 2 +- 7 files changed, 35 insertions(+), 27 deletions(-) rename tools/cosmovisor/{ => internal}/process_test.go (95%) rename tools/cosmovisor/{ => internal}/skip.go (89%) rename tools/cosmovisor/{ => internal}/upgrade.go (94%) rename tools/cosmovisor/{ => internal}/upgrade_test.go (95%) diff --git a/tools/cosmovisor/args_test.go b/tools/cosmovisor/args_test.go index 6394bfa03400..ae2c83636de7 100644 --- a/tools/cosmovisor/args_test.go +++ b/tools/cosmovisor/args_test.go @@ -472,6 +472,7 @@ var newConfig = func( UnsafeSkipBackup: skipBackup, DataBackupPath: dataBackupPath, GRPCAddress: grpcAddress, + RPCAddress: "http://localhost:26657", PreUpgradeMaxRetries: preupgradeMaxRetries, DisableLogs: disableLogs, ColorLogs: colorLogs, diff --git a/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go index b138fb271a50..7ae5bf0580cf 100644 --- a/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/internal" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" @@ -66,7 +67,7 @@ func prepareUpgradeHandler(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to parse upgrade info: %w", err) } - binaryURL, err := cosmovisor.GetBinaryURL(upgradeInfoParsed.Binaries) + binaryURL, err := internal.GetBinaryURL(upgradeInfoParsed.Binaries) if err != nil { return fmt.Errorf("binary URL not found in upgrade plan. Cannot prepare for upgrade: %w", err) } diff --git a/tools/cosmovisor/process_test.go b/tools/cosmovisor/internal/process_test.go similarity index 95% rename from tools/cosmovisor/process_test.go rename to tools/cosmovisor/internal/process_test.go index 43e150c1b6d0..f340b0420994 100644 --- a/tools/cosmovisor/process_test.go +++ b/tools/cosmovisor/internal/process_test.go @@ -1,6 +1,6 @@ //go:build linux || darwin -package cosmovisor_test +package internal import ( "bytes" @@ -17,14 +17,17 @@ import ( "github.com/stretchr/testify/require" "cosmossdk.io/tools/cosmovisor" - "cosmossdk.io/tools/cosmovisor/internal" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) var workDir string func init() { - workDir, _ = os.Getwd() + dir, err := os.Getwd() + if err != nil { + panic(err) + } + workDir = filepath.Join(dir, "..") } // TODO all these tests share the same setup so we can extract it to a common function @@ -35,7 +38,7 @@ type launchProcessFixture struct { stdout *buffer stderr *buffer logger log.Logger - runner internal.Runner + runner Runner } // TestLaunchProcess will try running the script a few times and watch upgrades work properly @@ -60,7 +63,7 @@ func TestLaunchProcess(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = f.runner.Start(context.Background(), args) - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) require.Empty(t, f.stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), f.stdout.String()) @@ -109,7 +112,7 @@ func TestPlanDisableRecase(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = f.runner.Start(context.Background(), args) - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) require.Empty(t, f.stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\n", upgradeFile), f.stdout.String()) @@ -155,7 +158,7 @@ func TestLaunchProcessWithRestartDelay(t *testing.T) { start := time.Now() err = f.runner.Start(context.Background(), []string{"foo", "bar", "1234", upgradeFile}) - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) // may not be the best way but the fastest way to check we meet the delay // in addition to comparing both the runtime of this test and TestLaunchProcess in addition @@ -185,7 +188,7 @@ func TestPlanShutdownGrace(t *testing.T) { args := []string{"foo", "bar", "1234", upgradeFile} err = f.runner.Start(context.Background(), args) - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) require.Empty(t, f.stderr.String()) require.Equal(t, fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"Chain2\" NEEDED at height: 49: {}\nWARN Need Flush\nFlushed\n", upgradeFile), f.stdout.String()) @@ -238,7 +241,7 @@ func TestLaunchProcessWithDownloads(t *testing.T) { args := []string{"some", "args", upgradeFilename} err = f.runner.Start(context.Background(), args) - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) require.Empty(t, f.stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", f.stdout.String()) currentBin, err = f.cfg.CurrentBin() @@ -254,7 +257,7 @@ func TestLaunchProcessWithDownloads(t *testing.T) { args = []string{"run", "--fast", upgradeFilename} err = f.runner.Start(context.Background(), args) // ended with one more upgrade - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) require.Empty(t, f.stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", f.stdout.String()) currentBin, err = f.cfg.CurrentBin() @@ -342,7 +345,7 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { args := []string{"some", "args", upgradeFilename} err = f.runner.Start(context.Background(), args) - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) require.Empty(t, f.stderr.String()) require.Equal(t, "Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", f.stdout.String()) currentBin, err = f.cfg.CurrentBin() @@ -361,7 +364,7 @@ func TestLaunchProcessWithDownloadsAndPreupgrade(t *testing.T) { args = []string{"run", "--fast", upgradeFilename} err = f.runner.Start(context.Background(), args) // ended with one more upgrade - require.ErrorIs(t, err, internal.ErrUpgradeNoDaemonRestart) + require.ErrorIs(t, err, ErrUpgradeNoDaemonRestart) require.Empty(t, f.stderr.String()) require.Equal(t, "Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", f.stdout.String()) currentBin, err = f.cfg.CurrentBin() @@ -421,7 +424,7 @@ func TestSkipUpgrade(t *testing.T) { for i := range cases { tc := cases[i] require := require.New(t) - h := cosmovisor.IsSkipUpgradeHeight(tc.args, tc.upgradeInfo) + h := IsSkipUpgradeHeight(tc.args, tc.upgradeInfo) require.Equal(h, tc.expectRes) } } @@ -460,7 +463,7 @@ func TestUpgradeSkipHeights(t *testing.T) { for i := range cases { tc := cases[i] require := require.New(t) - h := cosmovisor.UpgradeSkipHeights(tc.args) + h := UpgradeSkipHeights(tc.args) require.Equal(h, tc.expectRes) } } @@ -491,7 +494,7 @@ func setupTestLaunchProcessFixture(t *testing.T, testdataDir string, cfg cosmovi stdout: stdout, stderr: stderr, logger: logger, - runner: internal.NewRunner(preppedCfg, internal.RunConfig{ + runner: NewRunner(preppedCfg, RunConfig{ StdIn: stdin, StdOut: stdout, StdErr: stderr, diff --git a/tools/cosmovisor/skip.go b/tools/cosmovisor/internal/skip.go similarity index 89% rename from tools/cosmovisor/skip.go rename to tools/cosmovisor/internal/skip.go index 677eed56ac73..881da30f2aa2 100644 --- a/tools/cosmovisor/skip.go +++ b/tools/cosmovisor/internal/skip.go @@ -1,10 +1,11 @@ -package cosmovisor +package internal import ( "fmt" "strconv" "strings" + "cosmossdk.io/tools/cosmovisor" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -25,7 +26,7 @@ func IsSkipUpgradeHeight(args []string, upgradeInfo upgradetypes.Plan) bool { func UpgradeSkipHeights(args []string) []int { var heights []int for i, arg := range args { - if arg == fmt.Sprintf("--%s", FlagSkipUpgradeHeight) { + if arg == fmt.Sprintf("--%s", cosmovisor.FlagSkipUpgradeHeight) { j := i + 1 for j < len(args) { diff --git a/tools/cosmovisor/upgrade.go b/tools/cosmovisor/internal/upgrade.go similarity index 94% rename from tools/cosmovisor/upgrade.go rename to tools/cosmovisor/internal/upgrade.go index e9953d8d5108..d9cc30825285 100644 --- a/tools/cosmovisor/upgrade.go +++ b/tools/cosmovisor/internal/upgrade.go @@ -1,4 +1,4 @@ -package cosmovisor +package internal import ( "errors" @@ -8,6 +8,7 @@ import ( "cosmossdk.io/log" + "cosmossdk.io/tools/cosmovisor" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -15,7 +16,7 @@ import ( // UpgradeBinary will be called after an upgrade has been confirmed and the process has terminated. // We can now make any changes to the underlying directory without interference and leave it // in the upgraded state so that the app can restart with the new binary. -func UpgradeBinary(logger log.Logger, cfg *Config, p upgradetypes.Plan) error { +func UpgradeBinary(logger log.Logger, cfg *cosmovisor.Config, p upgradetypes.Plan) error { logger.Info("Upgrading binary", "name", p.Name) // simplest case is to switch the link err := plan.EnsureBinary(cfg.UpgradeBin(p.Name)) diff --git a/tools/cosmovisor/upgrade_test.go b/tools/cosmovisor/internal/upgrade_test.go similarity index 95% rename from tools/cosmovisor/upgrade_test.go rename to tools/cosmovisor/internal/upgrade_test.go index 8a3953c46fe0..f1b442d315a1 100644 --- a/tools/cosmovisor/upgrade_test.go +++ b/tools/cosmovisor/internal/upgrade_test.go @@ -1,6 +1,6 @@ //go:build darwin || linux -package cosmovisor_test +package internal import ( "fmt" @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/suite" "cosmossdk.io/log" + "cosmossdk.io/tools/cosmovisor" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" @@ -141,7 +142,7 @@ func (s *upgradeTestSuite) TestUpgradeBinaryNoDownloadUrl() { // do upgrade ignores bad files for _, name := range []string{"missing", "nobin"} { info := upgradetypes.Plan{Name: name} - err = cosmovisor.UpgradeBinary(logger, cfg, info) + err = UpgradeBinary(logger, cfg, info) s.Require().Error(err, name) currentBin, err := cfg.CurrentBin() s.Require().NoError(err) @@ -156,7 +157,7 @@ func (s *upgradeTestSuite) TestUpgradeBinaryNoDownloadUrl() { for _, upgrade := range []string{"chain2", "chain3"} { // now set it to a valid upgrade and make sure CurrentBin is now set properly info := upgradetypes.Plan{Name: upgrade} - err = cosmovisor.UpgradeBinary(logger, cfg, info) + err = UpgradeBinary(logger, cfg, info) s.Require().NoError(err) // we should see current point to the new upgrade dir upgradeBin := cfg.UpgradeBin(upgrade) @@ -228,10 +229,10 @@ func (s *upgradeTestSuite) TestUpgradeBinary() { plan := upgradetypes.Plan{ Name: "amazonas", - Info: fmt.Sprintf(`{"binaries":{"%s": "%s"}}`, cosmovisor.OSArch(), url), + Info: fmt.Sprintf(`{"binaries":{"%s": "%s"}}`, OSArch(), url), } - err = cosmovisor.UpgradeBinary(logger, cfg, plan) + err = UpgradeBinary(logger, cfg, plan) if !tc.canDownload { s.Require().Error(err) } else { @@ -250,7 +251,7 @@ func (s *upgradeTestSuite) TestOsArch() { "darwin/arm64", } - s.Require().True(slices.Contains(hosts, cosmovisor.OSArch())) + s.Require().True(slices.Contains(hosts, OSArch())) } // copyTestData will make a tempdir and then diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index e9ce152b2aa4..4f65583acd3a 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -108,7 +108,7 @@ func (u *Upgrader) DoUpgrade() error { return err } - if err := cosmovisor.UpgradeBinary(u.logger, u.cfg, u.upgradePlan); err != nil { + if err := UpgradeBinary(u.logger, u.cfg, u.upgradePlan); err != nil { return err } From 06f8c53f4de4906ff33b4019b4594f61f8392a94 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 12 Jun 2025 10:46:41 -0400 Subject: [PATCH 066/115] update logs, cleanup --- tools/cosmovisor/TEST_SCENARIOS.md | 22 -------- tools/cosmovisor/internal/runner.go | 8 +-- tools/cosmovisor/state_machine.mermaid | 28 ----------- tools/cosmovisor/state_machine.puml | 67 +++++++++++++++++++++++++ tools/cosmovisor/state_machine2.mermaid | 25 --------- tools/cosmovisor/state_machine3.puml | 53 ------------------- 6 files changed, 72 insertions(+), 131 deletions(-) delete mode 100644 tools/cosmovisor/TEST_SCENARIOS.md delete mode 100644 tools/cosmovisor/state_machine.mermaid create mode 100644 tools/cosmovisor/state_machine.puml delete mode 100644 tools/cosmovisor/state_machine2.mermaid delete mode 100644 tools/cosmovisor/state_machine3.puml diff --git a/tools/cosmovisor/TEST_SCENARIOS.md b/tools/cosmovisor/TEST_SCENARIOS.md deleted file mode 100644 index 13a983b189a6..000000000000 --- a/tools/cosmovisor/TEST_SCENARIOS.md +++ /dev/null @@ -1,22 +0,0 @@ -Ideally, scenarios should run in process with args passed manually to cobra so that we can: -- code coverage -- use the debugger - -## Scenarios -- basic: start node, get upgrade-info.json, upgrade -- manual upgrade added while running: - - start node - - get upgrade-info.json.batch - - restart with halt height - - reach halt height - - upgrade -- manual upgrade added while running but get upgrade-info.json: - - start node - - get upgrade-info.json.batch - - restart with halt height - - get upgrade-info.json - - upgrade - - restart with halt height - - reach halt height - - upgrade -- start with halt height \ No newline at end of file diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index f156ccb843ed..ba7883307053 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -85,12 +85,14 @@ func (r Runner) Start(ctx context.Context, args []string) error { // 3. Any other error or the: this is an unexpected error that should trigger a restart of the process with a backoff strategy. var restartNeeded ErrRestartNeeded if ok := errors.As(err, &restartNeeded); ok { - r.logger.Info("Restart needed") + r.logger.Info("Process shutdown complete, restart needed") } else if errors.Is(err, errDone) { + r.logger.Info("Shutting down Cosmovisor process gracefully") return nil + } else if err != nil { + r.logger.Error("Process exited with error, attempting to restart", "error", err) } else { - // TODO we should capture this error and it should get returned if retry max restarts is reached - r.logger.Error("Process exited", "error", err) + r.logger.Info("Process exited without error, restarting") } } } diff --git a/tools/cosmovisor/state_machine.mermaid b/tools/cosmovisor/state_machine.mermaid deleted file mode 100644 index bbf9ed0378ba..000000000000 --- a/tools/cosmovisor/state_machine.mermaid +++ /dev/null @@ -1,28 +0,0 @@ -stateDiagram-v2 - [*] --> Init - Init --> WaitForXUpgrade: no upgrade-info.json.batch - Init --> ??: have upgrade-info.json - Init --> StartWithHaltHeight: have upgrade-info.json.batch - WaitForXUpgrade --> KillProcessAndUpgrade: got upgrade-info.json - WaitForXUpgrade --> KillProcessAndAddHaltHeight: got upgrade-info.json.batch - WaitForXUpgrade --> CheckForUpgradeInfo: process exited - CheckForUpgradeInfo --> ShutdownAndError: no upgrade-info.json - StartWithHaltHeight --> WaitForManualUpgrade - WaitForManualUpgrade --> KillProcessAndUpgrade: got upgrade-info.json - WaitForManualUpgrade --> KillProcessAndAddHaltHeight: got upgrade-info.json.batch - WaitForManualUpgrade --> KillProcessAndUpgrade: height == halt-height - WaitForManualUpgrade --> ShutdownAndError: height > halt-height - WaitForManualUpgrade --> CheckForManualUpgrade: process exited - CheckForManualUpgrade --> ShutdownAndError: can't confirm upgrade - CheckForManualUpgrade --> DoUpgrade: confirmed halt-height or upgrade-info.json - KillProcessAndAddHaltHeight --> StartWithHaltHeight: process existed - KillProcessAndUpgrade --> DoUpgrade: process exited - DoUpgrade --> CheckSkipUpgradeHeight - CheckSkipUpgradeHeight --> DoBackup - CheckSkipUpgradeHeight --> Init: skip upgrade - DoBackup --> CustomPreUpgrade - CustomPreUpgrade --> SwitchToNewBinary - SwitchToNewBinary --> RunPreUpgrade - RunPreUpgrade --> Init: have new binary - RunPreUpgrade --> ShutdownAndError: no new binary - RunPreUpgrade --> GracefulShutdown: daemon restart disabled diff --git a/tools/cosmovisor/state_machine.puml b/tools/cosmovisor/state_machine.puml new file mode 100644 index 000000000000..0907667c9aa2 --- /dev/null +++ b/tools/cosmovisor/state_machine.puml @@ -0,0 +1,67 @@ +@startuml + +[*] -> ComputeRunPlan + + +state UpgradeIfNeeded { + state ReadUpgradeInfo { + } + state ReadManualUpgradeBatch { + } + ReadUpgradeInfo --> ReadManualUpgradeBatch + ReadManualUpgradeBatch --> ReadLatestHeight +} + +UpgradeIfNeeded --> ComputeRunPlan + +state ComputeRunPlan { + state ReadManualUpgradeBatch { + } + ReadManualUpgradeBatch --> ReadLatestHeight +} + +ComputeRunPlan --> RunProcess + +state RunProcess { +} + +'ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json +'ReadLatestHeight --> RunWithHaltHeight: have manual-upgrade-batch.json +'ReadLatestHeight --> Error: have unapplied upgrades before current height? +'ReadManualUpgradeBatch --> Run: no manual-upgrade-batch.json +' +''ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json +''ComputeRunPlan --> Run +''ComputeRunPlan --> RunWithHaltHeight +'' +'state DoUpgrade { +' DoBackup --> CustomPreUpgrade +' CustomPreUpgrade --> SwitchToNewBinary +' SwitchToNewBinary --> RunPreUpgrade +' RunPreUpgrade --> ComputeRunPlan +'} +'' +'state Run { +' state ConfirmDesiredHaltHeight { +' } +' ConfirmDesiredHaltHeight --> WatchForSignals: true +' state WatchForSignals { +' } +'} +' +'WatchForSignals --> CheckHaveUpgrade: got upgrade-info.json +' +'state CheckHaveUpgrade { +' CheckSkipUpgradeHeight --> Run +'} +' +'CheckSkipUpgradeHeight --> ShutdownNode +'ConfirmDesiredHaltHeight --> ShutdownNode: false +' +'state ShutdownNode { +' SendShutdownSignal --> WaitForShutdown +'} +' +'WaitForShutdown --> ComputeRunPlan +' +@enduml diff --git a/tools/cosmovisor/state_machine2.mermaid b/tools/cosmovisor/state_machine2.mermaid deleted file mode 100644 index 69001d04a789..000000000000 --- a/tools/cosmovisor/state_machine2.mermaid +++ /dev/null @@ -1,25 +0,0 @@ -stateDiagram-v2 - [*] --> ComputeRunPlan - state ComputeRunPlan { - ReadUpgradeInfo --> ReadManualUpgradesBatch - ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json - ReadManualUpgradesBatch --> Run: no manual upgrades - ReadManualUpgradesBatch --> RunWithHaltHeight: have manual-upgrades-batch.json - ReadLastKnownHeight - } - - state DoUpgrade { - CheckSkipUpgradeHeight --> DoBackup - DoBackup --> CustomPreUpgrade - CustomPreUpgrade --> SwitchToNewBinary - SwitchToNewBinary --> RunPreUpgrade - RunPreUpgrade - } - - DoUpgrade --> ComputeRunPlan - -%% state Run { -%% } -%% -%% state RunWithHaltHeight { -%% } \ No newline at end of file diff --git a/tools/cosmovisor/state_machine3.puml b/tools/cosmovisor/state_machine3.puml deleted file mode 100644 index c40cb018689a..000000000000 --- a/tools/cosmovisor/state_machine3.puml +++ /dev/null @@ -1,53 +0,0 @@ -@startuml - -[*] -> ComputeRunPlan - -state ComputeRunPlan { - state ReadUpgradeInfo { - } - state ReadManualUpgradeBatch { - } - ReadUpgradeInfo --> ReadManualUpgradeBatch - ReadManualUpgradeBatch --> ReadLatestHeight -} - -ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json -ReadLatestHeight --> RunWithHaltHeight: have manual-upgrade-batch.json -ReadLatestHeight --> Error: have unapplied upgrades before current height? -ReadManualUpgradeBatch --> Run: no manual-upgrade-batch.json - -'ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json -'ComputeRunPlan --> Run -'ComputeRunPlan --> RunWithHaltHeight -' -state DoUpgrade { - DoBackup --> CustomPreUpgrade - CustomPreUpgrade --> SwitchToNewBinary - SwitchToNewBinary --> RunPreUpgrade - RunPreUpgrade --> ComputeRunPlan -} -' -state Run { - state ConfirmDesiredHaltHeight { - } - ConfirmDesiredHaltHeight --> WatchForSignals: true - state WatchForSignals { - } -} - -WatchForSignals --> CheckHaveUpgrade: got upgrade-info.json - -state CheckHaveUpgrade { - CheckSkipUpgradeHeight --> Run -} - -CheckSkipUpgradeHeight --> ShutdownNode -ConfirmDesiredHaltHeight --> ShutdownNode: false - -state ShutdownNode { - SendShutdownSignal --> WaitForShutdown -} - -WaitForShutdown --> ComputeRunPlan - -@enduml From f971fa25afbafb92b306d98f37eafbef775425ff Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 12 Jun 2025 13:39:34 -0400 Subject: [PATCH 067/115] refactor show manual upgrades command, make config loading more consistent --- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 15 ---------- tools/cosmovisor/cmd/cosmovisor/config.go | 18 +++++++++++- .../cmd/cosmovisor/prepare_upgrade.go | 10 ++----- tools/cosmovisor/cmd/cosmovisor/root.go | 2 +- tools/cosmovisor/cmd/cosmovisor/run.go | 15 ---------- .../cosmovisor/cmd/cosmovisor/show_upgrade.go | 28 ++++++++----------- 6 files changed, 31 insertions(+), 57 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 0c09db655de9..4e9d98fb1e07 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -74,21 +74,6 @@ func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgrade return plan, nil } -// GetConfig returns a Config using passed-in flag -// TODO we should make sure getConfigFromCmd gets used by call commands, it seems like some commands do this differently -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) diff --git a/tools/cosmovisor/cmd/cosmovisor/config.go b/tools/cosmovisor/cmd/cosmovisor/config.go index d651e5fb3839..c7e6107471a2 100644 --- a/tools/cosmovisor/cmd/cosmovisor/config.go +++ b/tools/cosmovisor/cmd/cosmovisor/config.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" @@ -13,7 +15,7 @@ var configCmd = &cobra.Command{ otherwise it will display the config from the environment variables.`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - cfg, err := cosmovisor.GetConfigFromFile(cmd.Flag(cosmovisor.FlagCosmovisorConfig).Value.String()) + cfg, err := getConfigFromCmd(cmd) if err != nil { return err } @@ -22,3 +24,17 @@ otherwise it will display the config from the environment variables.`, return nil }, } + +// getConfigFromCmd retrieves the cosmovisor configuration from the command flags. +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 +} diff --git a/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go index 7ae5bf0580cf..fc6589b77664 100644 --- a/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go @@ -12,7 +12,6 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" - "cosmossdk.io/tools/cosmovisor" "cosmossdk.io/tools/cosmovisor/internal" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" @@ -35,14 +34,9 @@ gRPC must be enabled on the node for this command to work.`, } func prepareUpgradeHandler(cmd *cobra.Command, _ []string) error { - configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig) + cfg, err := getConfigFromCmd(cmd) if err != nil { - return fmt.Errorf("failed to get config flag: %w", err) - } - - cfg, err := cosmovisor.GetConfigFromFile(configPath) - if err != nil { - return fmt.Errorf("failed to get config: %w", err) + return err } logger := cfg.Logger(cmd.OutOrStdout()) diff --git a/tools/cosmovisor/cmd/cosmovisor/root.go b/tools/cosmovisor/cmd/cosmovisor/root.go index 92b1af2e11cf..59d51b68bbc8 100644 --- a/tools/cosmovisor/cmd/cosmovisor/root.go +++ b/tools/cosmovisor/cmd/cosmovisor/root.go @@ -19,7 +19,7 @@ func NewRootCmd() *cobra.Command { configCmd, NewVersionCmd(), NewAddUpgradeCmd(), - NewShowUpgradeInfoCmd(), + NewShowManualUpgradesCmd(), NewBatchAddUpgradeCmd(), NewPrepareUpgradeCmd(), ) diff --git a/tools/cosmovisor/cmd/cosmovisor/run.go b/tools/cosmovisor/cmd/cosmovisor/run.go index 28c3c4741db9..ffc3ca9b410e 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run.go +++ b/tools/cosmovisor/cmd/cosmovisor/run.go @@ -51,21 +51,6 @@ func run(ctx context.Context, cfgPath string, args []string, options ...RunOptio logger := cfg.Logger(runCfg.StdOut) runner := internal.NewRunner(cfg, runCfg, logger) return runner.Start(ctx, args) - //launcher, err := cosmovisor.NewLauncher(logger, cfg) - //if err != nil { - // return err - //} - // - //doUpgrade, err := launcher.Run(args, runCfg.StdIn, runCfg.StdOut, runCfg.StdErr) - //// if RestartAfterUpgrade, we launch after a successful upgrade (given that condition launcher.Run returns nil) - //for cfg.RestartAfterUpgrade && err == nil && doUpgrade { - // logger.Info("upgrade detected, relaunching", "app", cfg.Name) - // doUpgrade, err = launcher.Run(args, runCfg.StdIn, runCfg.StdOut, runCfg.StdErr) - //} - // - //if doUpgrade && err == nil { - // logger.Info("upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.") - //} } func parseCosmovisorConfig(args []string) (string, []string, error) { diff --git a/tools/cosmovisor/cmd/cosmovisor/show_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/show_upgrade.go index aa37fa36d4e7..b84d3d77ba8e 100644 --- a/tools/cosmovisor/cmd/cosmovisor/show_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/show_upgrade.go @@ -1,41 +1,35 @@ package main import ( + "encoding/json" "fmt" - "os" "github.com/spf13/cobra" - - "cosmossdk.io/tools/cosmovisor" ) -func NewShowUpgradeInfoCmd() *cobra.Command { +func NewShowManualUpgradesCmd() *cobra.Command { return &cobra.Command{ - Use: "show-upgrade-info", - Short: "Display current upgrade-info.json from data directory", + Use: "show-manual-upgrades", + Short: "Display planned manual upgrades", SilenceUsage: false, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig) + cfg, err := getConfigFromCmd(cmd) if err != nil { - return fmt.Errorf("failed to get config flag: %w", err) + return err } - cfg, err := cosmovisor.GetConfigFromFile(configPath) + data, err := cfg.ReadManualUpgrades() if err != nil { - return err + return fmt.Errorf("failed to read upgrade-info.json.batch: %w", err) } - data, err := os.ReadFile(cfg.UpgradeInfoFilePath()) + bz, err := json.MarshalIndent(data, "", " ") if err != nil { - if os.IsNotExist(err) { - cmd.Printf("No upgrade info found at %s\n", cfg.UpgradeInfoFilePath()) - return nil - } - return fmt.Errorf("failed to read upgrade-info.json: %w", err) + return fmt.Errorf("failed to marshal manual upgrade info as json: %w", err) } - cmd.Println(string(data)) + cmd.Println(string(bz)) return nil }, } From e595bb916d210483548dad843214b47503446f03 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 12 Jun 2025 14:14:44 -0400 Subject: [PATCH 068/115] document manual upgrade behavior --- tools/cosmovisor/README.md | 250 ++++++++++++++++++++++++++++--------- 1 file changed, 192 insertions(+), 58 deletions(-) diff --git a/tools/cosmovisor/README.md b/tools/cosmovisor/README.md index 3b5f722c5d1b..225b975b8583 100644 --- a/tools/cosmovisor/README.md +++ b/tools/cosmovisor/README.md @@ -4,8 +4,11 @@ sidebar_position: 1 # Cosmovisor -`cosmovisor` is a process manager for Cosmos SDK application binaries that automates application binary switch at chain upgrades. -It polls the `upgrade-info.json` file that is created by the x/upgrade module at upgrade height, and then can automatically download the new binary, stop the current binary, switch from the old binary to the new one, and finally restart the node with the new binary. +`cosmovisor` is a process manager for Cosmos SDK application binaries that automates application binary switch at chain +upgrades. +It polls the `upgrade-info.json` file that is created by the x/upgrade module at upgrade height, and then can +automatically download the new binary, stop the current binary, switch from the old binary to the new one, and finally +restart the node with the new binary. * [Design](#design) * [Contributing](#contributing) @@ -33,7 +36,9 @@ Cosmovisor is designed to be used as a wrapper for a `Cosmos SDK` app: * it will manage an app by restarting and upgrading if needed; * it is configured using environment variables, not positional arguments. -*Note: If new versions of the application are not set up to run in-place store migrations, migrations will need to be run manually before restarting `cosmovisor` with the new binary. For this reason, we recommend applications adopt in-place store migrations.* +*Note: If new versions of the application are not set up to run in-place store migrations, migrations will need to be +run manually before restarting `cosmovisor` with the new binary. For this reason, we recommend applications adopt +in-place store migrations.* :::tip Only the latest version of cosmovisor is actively developed/maintained. @@ -47,13 +52,15 @@ Versions prior to v1.0.0 have a vulnerability that could lead to a DOS. Please u Cosmovisor is part of the Cosmos SDK monorepo, but it's a separate module with it's own release schedule. -Release branches have the following format `release/cosmovisor/vA.B.x`, where A and B are a number (e.g. `release/cosmovisor/v1.3.x`). Releases are tagged using the following format: `cosmovisor/vA.B.C`. +Release branches have the following format `release/cosmovisor/vA.B.x`, where A and B are a number (e.g. +`release/cosmovisor/v1.3.x`). Releases are tagged using the following format: `cosmovisor/vA.B.C`. ## Setup ### Installation -You can download Cosmovisor from the [GitHub releases](https://github.com/cosmos/cosmos-sdk/releases/tag/cosmovisor%2Fv1.5.0). +You can download Cosmovisor from +the [GitHub releases](https://github.com/cosmos/cosmos-sdk/releases/tag/cosmovisor%2Fv1.5.0). To install the latest version of `cosmovisor`, run the following command: @@ -83,33 +90,69 @@ The first argument passed to `cosmovisor` is the action for `cosmovisor` to take * `help`, `--help`, or `-h` - Output `cosmovisor` help information and check your `cosmovisor` configuration. * `run` - Run the configured binary using the rest of the provided arguments. * `version` - Output the `cosmovisor` version and also run the binary with the `version` argument. -* `config` - Display the current `cosmovisor` configuration, that means displaying the environment variables value that `cosmovisor` is using. -* `add-upgrade` - Add an upgrade manually to `cosmovisor`. This command allow you to easily add the binary corresponding to an upgrade in cosmovisor. - -All arguments passed to `cosmovisor run` will be passed to the application binary (as a subprocess). `cosmovisor` will return `/dev/stdout` and `/dev/stderr` of the subprocess as its own. For this reason, `cosmovisor run` cannot accept any command-line arguments other than those available to the application binary. - -`cosmovisor` reads its configuration from environment variables, or its configuration file (use `--cosmovisor-config `): - -* `DAEMON_HOME` is the location where the `cosmovisor/` directory is kept that contains the genesis binary, the upgrade binaries, and any additional auxiliary files associated with each binary (e.g. `$HOME/.gaiad`, `$HOME/.regend`, `$HOME/.simd`, etc.). +* `config` - Display the current `cosmovisor` configuration, that means displaying the environment variables value that + `cosmovisor` is using. +* `init` - Initialize the `cosmovisor` folder structure and copy the provided executable to the appropriate location. +* `add-upgrade` - Add an upgrade manually to `cosmovisor`. This command allow you to easily add the binary corresponding + to an upgrade in cosmovisor or to schedule a manual upgrade that will set the node's `--halt-height` flag. +* `add-batch-upgrade` - Adds a batch of manually scheduled upgrades that will set the node's `--halt-height` flag. +* `prepare-upgrade` - Prepare for an upgrade by downloading the new binary and placing it in the appropriate directory. +* `show-manual-upgrades` - Show pending manual upgrades that set the `--halt-height` flag. + +All arguments passed to `cosmovisor run` will be passed to the application binary (as a subprocess). `cosmovisor` will +return `/dev/stdout` and `/dev/stderr` of the subprocess as its own. For this reason, `cosmovisor run` cannot accept any +command-line arguments other than those available to the application binary. + +`cosmovisor` reads its configuration from environment variables, or its configuration file (use +`--cosmovisor-config `): + +* `DAEMON_HOME` is the location where the `cosmovisor/` directory is kept that contains the genesis binary, the upgrade + binaries, and any additional auxiliary files associated with each binary (e.g. `$HOME/.gaiad`, `$HOME/.regend`, + `$HOME/.simd`, etc.). * `DAEMON_NAME` is the name of the binary itself (e.g. `gaiad`, `regend`, `simd`, etc.). -* `DAEMON_ALLOW_DOWNLOAD_BINARIES` (*optional*), if set to `true`, will enable auto-downloading of new binaries (for security reasons, this is intended for full nodes rather than validators). By default, `cosmovisor` will not auto-download new binaries. -* `DAEMON_DOWNLOAD_MUST_HAVE_CHECKSUM` (*optional*, default = `false`), if `true` cosmovisor will require that a checksum is provided in the upgrade plan for the binary to be downloaded. If `false`, cosmovisor will not require a checksum to be provided, but still check the checksum if one is provided. -* `DAEMON_RESTART_AFTER_UPGRADE` (*optional*, default = `true`), if `true`, restarts the subprocess with the same command-line arguments and flags (but with the new binary) after a successful upgrade. Otherwise (`false`), `cosmovisor` stops running after an upgrade and requires the system administrator to manually restart it. Note restart is only after the upgrade and does not auto-restart the subprocess after an error occurs. -* `DAEMON_RESTART_DELAY` (*optional*, default none), allow a node operator to define a delay between the node halt (for upgrade) and backup by the specified time. The value must be a duration (e.g. `1s`). -* `DAEMON_SHUTDOWN_GRACE` (*optional*, default none), if set, send interrupt to binary and wait the specified time to allow for cleanup/cache flush to disk before sending the kill signal. The value must be a duration (e.g. `1s`). -* `DAEMON_POLL_INTERVAL` (*optional*, default 300 milliseconds), is the interval length for polling the upgrade plan file. The value must be a duration (e.g. `1s`). +* `DAEMON_ALLOW_DOWNLOAD_BINARIES` (*optional*), if set to `true`, will enable auto-downloading of new binaries (for + security reasons, this is intended for full nodes rather than validators). By default, `cosmovisor` will not + auto-download new binaries. +* `DAEMON_DOWNLOAD_MUST_HAVE_CHECKSUM` (*optional*, default = `false`), if `true` cosmovisor will require that a + checksum is provided in the upgrade plan for the binary to be downloaded. If `false`, cosmovisor will not require a + checksum to be provided, but still check the checksum if one is provided. +* `DAEMON_RESTART_AFTER_UPGRADE` (*optional*, default = `true`), if `true`, restarts the subprocess with the same + command-line arguments and flags (but with the new binary) after a successful upgrade. Otherwise (`false`), + `cosmovisor` stops running after an upgrade and requires the system administrator to manually restart it. Note restart + is only after the upgrade and does not auto-restart the subprocess after an error occurs. +* `DAEMON_RESTART_DELAY` (*optional*, default none), allow a node operator to define a delay between the node halt (for + upgrade) and backup by the specified time. The value must be a duration (e.g. `1s`). +* `DAEMON_SHUTDOWN_GRACE` (*optional*, default none), if set, send interrupt to binary and wait the specified time to + allow for cleanup/cache flush to disk before sending the kill signal. The value must be a duration (e.g. `1s`). +* `DAEMON_POLL_INTERVAL` (*optional*, default 300 milliseconds), is the interval length for polling the upgrade plan + file. The value must be a duration (e.g. `1s`). * `DAEMON_DATA_BACKUP_DIR` option to set a custom backup directory. If not set, `DAEMON_HOME` is used. -* `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `true`, upgrades directly without performing a backup. Otherwise (`false`, default) backs up the data before trying the upgrade. The default value of false is useful and recommended in case of failures and when a backup needed to rollback. We recommend using the default backup option `UNSAFE_SKIP_BACKUP=false`. -* `DAEMON_PREUPGRADE_MAX_RETRIES` (defaults to `0`). The maximum number of times to call [`pre-upgrade`](https://docs.cosmos.network/main/build/building-apps/app-upgrade#pre-upgrade-handling) in the application after exit status of `31`. After the maximum number of retries, Cosmovisor fails the upgrade. -* `COSMOVISOR_DISABLE_LOGS` (defaults to `false`). If set to true, this will disable Cosmovisor logs (but not the underlying process) completely. This may be useful, for example, when a Cosmovisor subcommand you are executing returns a valid JSON you are then parsing, as logs added by Cosmovisor make this output not a valid JSON. -* `COSMOVISOR_COLOR_LOGS` (defaults to `true`). If set to true, this will colorise Cosmovisor logs (but not the underlying process). -* `COSMOVISOR_TIMEFORMAT_LOGS` (defaults to `kitchen`). If set to a value (`layout|ansic|unixdate|rubydate|rfc822|rfc822z|rfc850|rfc1123|rfc1123z|rfc3339|rfc3339nano|kitchen`), this will add timestamp prefix to Cosmovisor logs (but not the underlying process). -* `COSMOVISOR_CUSTOM_PREUPGRADE` (defaults to ``). If set, this will run $DAEMON_HOME/cosmovisor/$COSMOVISOR_CUSTOM_PREUPGRADE prior to upgrade with the arguments [ upgrade.Name, upgrade.Height ]. Executes a custom script (separate and prior to the chain daemon pre-upgrade command) -* `COSMOVISOR_DISABLE_RECASE` (defaults to `false`). If set to true, the upgrade directory will expected to match the upgrade plan name without any case changes +* `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `true`, upgrades directly without performing a backup. + Otherwise (`false`, default) backs up the data before trying the upgrade. The default value of false is useful and + recommended in case of failures and when a backup needed to rollback. We recommend using the default backup option + `UNSAFE_SKIP_BACKUP=false`. +* `DAEMON_PREUPGRADE_MAX_RETRIES` (defaults to `0`). The maximum number of times to call [ + `pre-upgrade`](https://docs.cosmos.network/main/build/building-apps/app-upgrade#pre-upgrade-handling) in the + application after exit status of `31`. After the maximum number of retries, Cosmovisor fails the upgrade. +* `COSMOVISOR_DISABLE_LOGS` (defaults to `false`). If set to true, this will disable Cosmovisor logs (but not the + underlying process) completely. This may be useful, for example, when a Cosmovisor subcommand you are executing + returns a valid JSON you are then parsing, as logs added by Cosmovisor make this output not a valid JSON. +* `COSMOVISOR_COLOR_LOGS` (defaults to `true`). If set to true, this will colorise Cosmovisor logs (but not the + underlying process). +* `COSMOVISOR_TIMEFORMAT_LOGS` (defaults to `kitchen`). If set to a value ( + `layout|ansic|unixdate|rubydate|rfc822|rfc822z|rfc850|rfc1123|rfc1123z|rfc3339|rfc3339nano|kitchen`), this will add + timestamp prefix to Cosmovisor logs (but not the underlying process). +* `COSMOVISOR_CUSTOM_PREUPGRADE` (defaults to ``). If set, this will run + $DAEMON_HOME/cosmovisor/$COSMOVISOR_CUSTOM_PREUPGRADE prior to upgrade with the + arguments [ upgrade.Name, upgrade.Height ]. Executes a custom script (separate and prior to the chain daemon + pre-upgrade command) +* `COSMOVISOR_DISABLE_RECASE` (defaults to `false`). If set to true, the upgrade directory will expected to match the + upgrade plan name without any case changes ### Folder Layout -`$DAEMON_HOME/cosmovisor` is expected to belong completely to `cosmovisor` and the subprocesses that are controlled by it. The folder content is organized as follows: +`$DAEMON_HOME/cosmovisor` is expected to belong completely to `cosmovisor` and the subprocesses that are controlled by +it. The folder content is organized as follows: ```text . @@ -125,9 +168,18 @@ All arguments passed to `cosmovisor run` will be passed to the application binar └── preupgrade.sh (optional) ``` -The `cosmovisor/` directory includes a subdirectory for each version of the application (i.e. `genesis` or `upgrades/`). Within each subdirectory is the application binary (i.e. `bin/$DAEMON_NAME`) and any additional auxiliary files associated with each binary. `current` is a symbolic link to the currently active directory (i.e. `genesis` or `upgrades/`). The `name` variable in `upgrades/` is the lowercased URI-encoded name of the upgrade as specified in the upgrade module plan. Note that the upgrade name path are normalized to be lowercased: for instance, `MyUpgrade` is normalized to `myupgrade`, and its path is `upgrades/myupgrade`. +The `cosmovisor/` directory includes a subdirectory for each version of the application (i.e. `genesis` or +`upgrades/`). Within each subdirectory is the application binary (i.e. `bin/$DAEMON_NAME`) and any additional +auxiliary files associated with each binary. `current` is a symbolic link to the currently active directory (i.e. +`genesis` or `upgrades/`). The `name` variable in `upgrades/` is the lowercased URI-encoded name of the +upgrade as specified in the upgrade module plan. Note that the upgrade name path are normalized to be lowercased: for +instance, `MyUpgrade` is normalized to `myupgrade`, and its path is `upgrades/myupgrade`. -Please note that `$DAEMON_HOME/cosmovisor` only stores the *application binaries*. The `cosmovisor` binary itself can be stored in any typical location (e.g. `/usr/local/bin`). The application will continue to store its data in the default data directory (e.g. `$HOME/.simapp`) or the data directory specified with the `--home` flag. `$DAEMON_HOME` is dependent of the data directory and must be set to the same directory as the data directory, you will end up with a configuration like the following: +Please note that `$DAEMON_HOME/cosmovisor` only stores the *application binaries*. The `cosmovisor` binary itself can be +stored in any typical location (e.g. `/usr/local/bin`). The application will continue to store its data in the default +data directory (e.g. `$HOME/.simapp`) or the data directory specified with the `--home` flag. `$DAEMON_HOME` is +dependent of the data directory and must be set to the same directory as the data directory, you will end up with a +configuration like the following: ```text .simapp @@ -148,11 +200,18 @@ The system administrator is responsible for: * creating the `/cosmovisor/upgrades//bin` folders * placing the different versions of the `` executable in the appropriate `bin` folders. -`cosmovisor` will set the `current` link to point to `genesis` at first start (i.e. when no `current` link exists) and then handle switching binaries at the correct points in time so that the system administrator can prepare days in advance and relax at upgrade time. +`cosmovisor` will set the `current` link to point to `genesis` at first start (i.e. when no `current` link exists) and +then handle switching binaries at the correct points in time so that the system administrator can prepare days in +advance and relax at upgrade time. -In order to support downloadable binaries, a tarball for each upgrade binary will need to be packaged up and made available through a canonical URL. Additionally, a tarball that includes the genesis binary and all available upgrade binaries can be packaged up and made available so that all the necessary binaries required to sync a fullnode from start can be easily downloaded. +In order to support downloadable binaries, a tarball for each upgrade binary will need to be packaged up and made +available through a canonical URL. Additionally, a tarball that includes the genesis binary and all available upgrade +binaries can be packaged up and made available so that all the necessary binaries required to sync a fullnode from start +can be easily downloaded. -The `DAEMON` specific code and operations (e.g. cometBFT config, the application db, syncing blocks, etc.) all work as expected. The application binaries' directives such as command-line flags and environment variables also work as expected. +The `DAEMON` specific code and operations (e.g. cometBFT config, the application db, syncing blocks, etc.) all work as +expected. The application binaries' directives such as command-line flags and environment variables also work as +expected. ### Initialization @@ -167,44 +226,107 @@ It does the following: It uses the `DAEMON_HOME` and `DAEMON_NAME` environment variables for folder location and executable name. -The `cosmovisor init` command is specifically for initializing cosmovisor, and should not be confused with a chain's `init` command (e.g. `cosmovisor run init`). +The `cosmovisor init` command is specifically for initializing cosmovisor, and should not be confused with a chain's +`init` command (e.g. `cosmovisor run init`). ### Detecting Upgrades -`cosmovisor` is polling the `$DAEMON_HOME/data/upgrade-info.json` file for new upgrade instructions. The file is created by the x/upgrade module in `BeginBlocker` when an upgrade is detected and the blockchain reaches the upgrade height. +`cosmovisor` is polling the `$DAEMON_HOME/data/upgrade-info.json` file for new upgrade instructions. The file is created +by the x/upgrade module in `BeginBlocker` when an upgrade is detected and the blockchain reaches the upgrade height. The following heuristic is applied to detect the upgrade: -* When starting, `cosmovisor` doesn't know much about currently running upgrade, except the binary which is `current/bin/`. It tries to read the `current/update-info.json` file to get information about the current upgrade name. -* If neither `cosmovisor/current/upgrade-info.json` nor `data/upgrade-info.json` exist, then `cosmovisor` will wait for `data/upgrade-info.json` file to trigger an upgrade. -* If `cosmovisor/current/upgrade-info.json` doesn't exist but `data/upgrade-info.json` exists, then `cosmovisor` assumes that whatever is in `data/upgrade-info.json` is a valid upgrade request. In this case `cosmovisor` tries immediately to make an upgrade according to the `name` attribute in `data/upgrade-info.json`. -* Otherwise, `cosmovisor` waits for changes in `upgrade-info.json`. As soon as a new upgrade name is recorded in the file, `cosmovisor` will trigger an upgrade mechanism. +* When starting, `cosmovisor` doesn't know much about currently running upgrade, except the binary which is + `current/bin/`. It tries to read the `current/update-info.json` file to get information about the current upgrade + name. +* If neither `cosmovisor/current/upgrade-info.json` nor `data/upgrade-info.json` exist, then `cosmovisor` will wait for + `data/upgrade-info.json` file to trigger an upgrade. +* If `cosmovisor/current/upgrade-info.json` doesn't exist but `data/upgrade-info.json` exists, then `cosmovisor` assumes + that whatever is in `data/upgrade-info.json` is a valid upgrade request. In this case `cosmovisor` tries immediately + to make an upgrade according to the `name` attribute in `data/upgrade-info.json`. +* Otherwise, `cosmovisor` waits for changes in `upgrade-info.json`. As soon as a new upgrade name is recorded in the + file, `cosmovisor` will trigger an upgrade mechanism. When the upgrade mechanism is triggered, `cosmovisor` will: -1. if `DAEMON_ALLOW_DOWNLOAD_BINARIES` is enabled, start by auto-downloading a new binary into `cosmovisor//bin` (where `` is the `upgrade-info.json:name` attribute); -2. update the `current` symbolic link to point to the new directory and save `data/upgrade-info.json` to `cosmovisor/current/upgrade-info.json`. +1. if `DAEMON_ALLOW_DOWNLOAD_BINARIES` is enabled, start by auto-downloading a new binary into `cosmovisor//bin` ( + where `` is the `upgrade-info.json:name` attribute); +2. update the `current` symbolic link to point to the new directory and save `data/upgrade-info.json` to + `cosmovisor/current/upgrade-info.json`. +3. deleted `data/upgrade-info.json` to avoid further triggering of the upgrade mechanism. + +### Scheduling Manual Upgrades + +The `add-upgrade` and `add-upgrade-batch` commands can be used to schedule manual upgrades which will +set the node's `--halt-height` flag. + +Manually scheduled upgrades will be stored in the `data/upgrade-info.json.batch` file as an array of upgrade plans. +A running Cosmovisor process will pick up updates to this file and restart the node with its `--halt-height` flag +set to the height of the earliest scheduled manual upgrade. + +To detect when a node has reached a manual upgrade height, Cosmovisor will continuously poll the node's +`/block` RPC endpoint to determine the current block height. +When Cosmovisor observes that the current height is equal to the desirest halt height, it will shut down +the node and perform the upgrade. +After the upgrade is completed, the manual upgrade will be removed from the `data/upgrade-info.json.batch` file. +If Cosmovisor detects that there is a manual upgrade scheduled for before the chain's current height, it will +assume that this is an error condition requiring manual intervention and it will shut down the node. + +#### Expected `/block` RPC Endpoint + +The RPC endpoint to use for polling the node's current height can be configured using the `DAEMON_RPC_ADDRESS` +environment variable and the `daemon_rpc_address` config variable. +The default address is `http://localhost:26657`. + +Cosmovisor will check for a valid response under either the `/block` or `/v1/block` path and expects that +the response conforms to the following JSON format: + +```json +{ + "result": { + "block": { + "header": { + "height": "" + } + } + } +} +``` ### Adding Upgrade Binary -`cosmovisor` has an `add-upgrade` command that allows to easily link a binary to an upgrade. It creates a new folder in `cosmovisor/upgrades/` and copies the provided executable file to `cosmovisor/upgrades//bin/`. +`cosmovisor` has an `add-upgrade` command that allows to easily link a binary to an upgrade. It creates a new folder in +`cosmovisor/upgrades/` and copies the provided executable file to `cosmovisor/upgrades//bin/`. -Using the `--upgrade-height` flag allows to specify at which height the binary should be switched, without going via a gorvernance proposal. -This enables support for an emergency coordinated upgrades where the binary must be switched at a specific height, but there is no time to go through a governance proposal. +Using the `--upgrade-height` flag allows to specify at which height the binary should be switched, without going via a +gorvernance proposal. +This enables support for an emergency coordinated upgrades where the binary must be switched at a specific height, but +there is no time to go through a governance proposal. :::warning -`--upgrade-height` creates an `upgrade-info.json` file. This means if a chain upgrade via governance proposal is executed before the specified height with `--upgrade-height`, the governance proposal will overwrite the `upgrade-info.json` plan created by `add-upgrade --upgrade-height `. +`--upgrade-height` creates an `upgrade-info.json` file. This means if a chain upgrade via governance proposal is +executed before the specified height with `--upgrade-height`, the governance proposal will overwrite the +`upgrade-info.json` plan created by `add-upgrade --upgrade-height `. Take this into consideration when using `--upgrade-height`. ::: ### Auto-Download -Generally, `cosmovisor` requires that the system administrator place all relevant binaries on disk before the upgrade happens. However, for people who don't need such control and want an automated setup (maybe they are syncing a non-validating fullnode and want to do little maintenance), there is another option. +Generally, `cosmovisor` requires that the system administrator place all relevant binaries on disk before the upgrade +happens. However, for people who don't need such control and want an automated setup (maybe they are syncing a +non-validating fullnode and want to do little maintenance), there is another option. -**NOTE: we don't recommend using auto-download** because it doesn't verify in advance if a binary is available. If there will be any issue with downloading a binary, the cosmovisor will stop and won't restart an App (which could lead to a chain halt). +**NOTE: we don't recommend using auto-download** because it doesn't verify in advance if a binary is available. If there +will be any issue with downloading a binary, the cosmovisor will stop and won't restart an App (which could lead to a +chain halt). -If `DAEMON_ALLOW_DOWNLOAD_BINARIES` is set to `true`, and no local binary can be found when an upgrade is triggered, `cosmovisor` will attempt to download and install the binary itself based on the instructions in the `info` attribute in the `data/upgrade-info.json` file. The files is constructed by the x/upgrade module and contains data from the upgrade `Plan` object. The `Plan` has an info field that is expected to have one of the following two valid formats to specify a download: +If `DAEMON_ALLOW_DOWNLOAD_BINARIES` is set to `true`, and no local binary can be found when an upgrade is triggered, +`cosmovisor` will attempt to download and install the binary itself based on the instructions in the `info` attribute in +the `data/upgrade-info.json` file. The files is constructed by the x/upgrade module and contains data from the upgrade +`Plan` object. The `Plan` has an info field that is expected to have one of the following two valid formats to specify a +download: -1. Store an os/architecture -> binary URI map in the upgrade plan info field as JSON under the `"binaries"` key. For example: +1. Store an os/architecture -> binary URI map in the upgrade plan info field as JSON under the `"binaries"` key. For + example: ```json { @@ -214,7 +336,7 @@ If `DAEMON_ALLOW_DOWNLOAD_BINARIES` is set to `true`, and no local binary can be } ``` - You can include multiple binaries at once to ensure more than one environment will receive the correct binaries: + You can include multiple binaries at once to ensure more than one environment will receive the correct binaries: ```json { @@ -226,7 +348,7 @@ If `DAEMON_ALLOW_DOWNLOAD_BINARIES` is set to `true`, and no local binary can be } ``` - When submitting this as a proposal ensure there are no spaces. An example command using `gaiad` could look like: + When submitting this as a proposal ensure there are no spaces. An example command using `gaiad` could look like: ```shell > gaiad tx upgrade software-upgrade Vega \ @@ -243,15 +365,21 @@ If `DAEMON_ALLOW_DOWNLOAD_BINARIES` is set to `true`, and no local binary can be --yes ``` -2. Store a link to a file that contains all information in the above format (e.g. if you want to specify lots of binaries, changelog info, etc. without filling up the blockchain). For example: +2. Store a link to a file that contains all information in the above format (e.g. if you want to specify lots of + binaries, changelog info, etc. without filling up the blockchain). For example: ```text https://example.com/testnet-1001-info.json?checksum=sha256:deaaa99fda9407c4dbe1d04bd49bab0cc3c1dd76fa392cd55a9425be074af01e ``` -When `cosmovisor` is triggered to download the new binary, `cosmovisor` will parse the `"binaries"` field, download the new binary with [go-getter](https://github.com/hashicorp/go-getter), and unpack the new binary in the `upgrades/` folder so that it can be run as if it was installed manually. +When `cosmovisor` is triggered to download the new binary, `cosmovisor` will parse the `"binaries"` field, download the +new binary with [go-getter](https://github.com/hashicorp/go-getter), and unpack the new binary in the `upgrades/` +folder so that it can be run as if it was installed manually. -Note that for this mechanism to provide strong security guarantees, all URLs should include a SHA 256/512 checksum. This ensures that no false binary is run, even if someone hacks the server or hijacks the DNS. `go-getter` will always ensure the downloaded file matches the checksum if it is provided. `go-getter` will also handle unpacking archives into directories (in this case the download link should point to a `zip` file of all data in the `bin` directory). +Note that for this mechanism to provide strong security guarantees, all URLs should include a SHA 256/512 checksum. This +ensures that no false binary is run, even if someone hacks the server or hijacks the DNS. `go-getter` will always ensure +the downloaded file matches the checksum if it is provided. `go-getter` will also handle unpacking archives into +directories (in this case the download link should point to a `zip` file of all data in the `bin` directory). To properly create a sha256 checksum on linux, you can use the `sha256sum` utility. For example: @@ -261,7 +389,8 @@ sha256sum ./testdata/repo/zip_directory/autod.zip The result will look something like the following: `29139e1381b8177aec909fab9a75d11381cab5adf7d3af0c05ff1c9c117743a7`. -You can also use `sha512sum` if you would prefer to use longer hashes, or `md5sum` if you would prefer to use broken hashes. Whichever you choose, make sure to set the hash algorithm properly in the checksum argument to the URL. +You can also use `sha512sum` if you would prefer to use longer hashes, or `md5sum` if you would prefer to use broken +hashes. Whichever you choose, make sure to set the hash algorithm properly in the checksum argument to the URL. ### Preparing for an Upgrade @@ -297,7 +426,8 @@ INFO Upgrade preparation complete name=v1.0.0 height=1000000 ## Example: SimApp Upgrade -The following instructions provide a demonstration of `cosmovisor` using the simulation application (`simapp`) shipped with the Cosmos SDK's source code. The following commands are to be run from within the `cosmos-sdk` repository. +The following instructions provide a demonstration of `cosmovisor` using the simulation application (`simapp`) shipped +with the Cosmos SDK's source code. The following commands are to be run from within the `cosmos-sdk` repository. ### Chain Setup @@ -376,7 +506,9 @@ Update app to the latest version (e.g. v0.50.0). :::note -Migration plans are defined using the `x/upgrade` module and described in [In-Place Store Migrations](https://github.com/cosmos/cosmos-sdk/blob/main/docs/learn/advanced/15-upgrade.md). Migrations can perform any deterministic state change. +Migration plans are defined using the `x/upgrade` module and described +in [In-Place Store Migrations](https://github.com/cosmos/cosmos-sdk/blob/main/docs/learn/advanced/15-upgrade.md). +Migrations can perform any deterministic state change. The migration plan to upgrade the simapp from v0.47 to v0.50 is defined in `simapp/upgrade.go`. @@ -400,7 +532,8 @@ The migration name must match the one defined in the migration plan. cosmovisor add-upgrade v047-to-v050 ./build/simd ``` -Open a new terminal window and submit an upgrade proposal along with a deposit and a vote (these commands must be run within 20 seconds of each other): +Open a new terminal window and submit an upgrade proposal along with a deposit and a vote (these commands must be run +within 20 seconds of each other): ```shell ./build/simd tx upgrade software-upgrade v047-to-v050 --title upgrade --summary upgrade --upgrade-height 200 --upgrade-info "{}" --no-validate --from validator --yes @@ -408,4 +541,5 @@ Open a new terminal window and submit an upgrade proposal along with a deposit a ./build/simd tx gov vote 1 yes --from validator --yes ``` -The upgrade will occur automatically at height 200. Note: you may need to change the upgrade height in the snippet above if your test play takes more time. +The upgrade will occur automatically at height 200. Note: you may need to change the upgrade height in the snippet above +if your test play takes more time. From 947c5f8fc6e0cfeb1af02dbbc815d1777431f7b0 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 12 Jun 2025 17:12:27 -0400 Subject: [PATCH 069/115] WIP on cosmovisor system tests --- systemtests/system.go | 59 +++++++++++++++++++++++-- tests/systemtests/upgrade_test.go | 2 +- tools/cosmovisor/cmd/cosmovisor/init.go | 4 +- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/systemtests/system.go b/systemtests/system.go index c5e45e607b31..4806f20f35fb 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -174,11 +174,22 @@ func (s *SystemUnderTest) SetupChain() { } func (s *SystemUnderTest) StartChain(t *testing.T, xargs ...string) { + s.doStartChain(t, false, xargs...) +} + +func (s *SystemUnderTest) StartChainWithCosmovisor(t *testing.T, xargs ...string) { + s.doStartChain(t, true, xargs...) +} + +func (s *SystemUnderTest) doStartChain(t *testing.T, useCosmovisor bool, xargs ...string) { t.Helper() + if useCosmovisor { + s.initCosmovisor(t) + } s.Log("Start chain\n") s.ChainStarted = true // HACK: force db_backend - s.startNodesAsync(t, append([]string{"start", "--log_level=info", "--log_no_color", "--db_backend=goleveldb"}, xargs...)...) + s.startNodesAsync(t, useCosmovisor, append([]string{"start", "--log_level=info", "--log_no_color", "--db_backend=goleveldb"}, xargs...)...) s.AwaitNodeUp(t, s.rpcAddr) @@ -195,6 +206,32 @@ func (s *SystemUnderTest) StartChain(t *testing.T, xargs ...string) { s.AwaitNextBlock(t, 10e9) } +func (s *SystemUnderTest) cosmovisorEnv(t *testing.T, home string) []string { + absHome, err := filepath.Abs(home) + require.NoError(t, err) + return []string{ + fmt.Sprintf("DAEMON_HOME=%s", absHome), + fmt.Sprintf("DAEMON_NAME=%s", s.projectName), + } +} + +func (s *SystemUnderTest) initCosmovisor(t *testing.T) { + s.withEachNodeHome(func(i int, home string) { + env := s.cosmovisorEnv(t, home) + binary := locateExecutable(s.execBinary) + t.Logf("Initializing Cosmovisor for node %d with binary %s and env %+v", i, binary, env) + cmd := exec.Command( + "cosmovisor", + "init", + binary, + ) + cmd.Dir = WorkDir + cmd.Env = env + s.watchLogs(i, cmd) + require.NoError(t, cmd.Run(), "cosmovisor init %d", i) + }) +} + // MarkDirty whole chain will be reset when marked dirty func (s *SystemUnderTest) MarkDirty() { s.dirty = true @@ -591,16 +628,30 @@ func RunShellCmd(cmd string, args ...string) (string, error) { } // startNodesAsync runs the given app cli command for all cluster nodes and returns without waiting -func (s *SystemUnderTest) startNodesAsync(t *testing.T, xargs ...string) { +func (s *SystemUnderTest) startNodesAsync(t *testing.T, useCosmovisor bool, xargs ...string) { t.Helper() s.withEachNodeHome(func(i int, home string) { - args := append(xargs, "--home="+home) + absHome, err := filepath.Abs(home) + require.NoError(t, err, "failed to get absolute home path") + args := append(xargs, "--home="+absHome) + var binary string + var env []string + if useCosmovisor { + binary = "cosmovisor" + args = append([]string{"run"}, args...) // cosmovisor run + cfgPath := filepath.Join(absHome, "cosmovisor", "config.toml") + args = append(args, "--cosmovisor-config", cfgPath) + env = s.cosmovisorEnv(t, absHome) + } else { + binary = locateExecutable(s.execBinary) + } s.Logf("Execute `%s %s`\n", s.execBinary, strings.Join(args, " ")) cmd := exec.Command( //nolint:gosec // used by tests only - locateExecutable(s.execBinary), + binary, args..., ) cmd.Dir = WorkDir + cmd.Env = env s.watchLogs(i, cmd) require.NoError(t, cmd.Start(), "node %d", i) s.Logf("Node started: %d\n", cmd.Process.Pid) diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index 59d47e271e41..c9cb09820887 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -82,7 +82,7 @@ func TestChainUpgrade(t *testing.T) { votingPeriod := 5 * time.Second // enough time to vote systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) - systest.Sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) + systest.Sut.StartChainWithCosmovisor(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) govAddr := sdk.AccAddress(address.Module("gov")).String() diff --git a/tools/cosmovisor/cmd/cosmovisor/init.go b/tools/cosmovisor/cmd/cosmovisor/init.go index 98d9fcbc8e73..8a8dfa6ca7bf 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init.go +++ b/tools/cosmovisor/cmd/cosmovisor/init.go @@ -21,7 +21,9 @@ func NewInitCmd() *cobra.Command { Use: "init ", Short: "Initialize a cosmovisor daemon home directory.", Long: `Initialize a cosmovisor daemon home directory with the provided executable. -Configuration file is initialized at the default path (<-home->/cosmovisor/config.toml).`, +Configuration file is initialized at the default path (<-home->/cosmovisor/config.toml). + +The DAEMON_HOME and DAEMON_NAME environment variables must be set for this command to work.`, Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { From e1cceccbfcf40be060ff98c801e2e95b6c7f58b3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 12 Jun 2025 17:40:51 -0400 Subject: [PATCH 070/115] WIP on cosmovisor system tests --- systemtests/system.go | 20 ++++++++++++++------ tests/systemtests/upgrade_test.go | 22 ++++++++++++---------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/systemtests/system.go b/systemtests/system.go index 4806f20f35fb..452b6d8a2cdd 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -215,23 +215,31 @@ func (s *SystemUnderTest) cosmovisorEnv(t *testing.T, home string) []string { } } -func (s *SystemUnderTest) initCosmovisor(t *testing.T) { +func (s *SystemUnderTest) ExecCosmovisor(t *testing.T, async bool, args ...string) { s.withEachNodeHome(func(i int, home string) { env := s.cosmovisorEnv(t, home) - binary := locateExecutable(s.execBinary) - t.Logf("Initializing Cosmovisor for node %d with binary %s and env %+v", i, binary, env) + t.Logf("Calling Cosmovisor with args %+v and env %+v", args, env) cmd := exec.Command( "cosmovisor", - "init", - binary, + args..., ) cmd.Dir = WorkDir cmd.Env = env s.watchLogs(i, cmd) - require.NoError(t, cmd.Run(), "cosmovisor init %d", i) + if async { + require.NoError(t, cmd.Start(), "cosmovisor init %d", i) + s.awaitProcessCleanup(cmd) + } else { + require.NoError(t, cmd.Run(), "cosmovisor init %d", i) + } }) } +func (s *SystemUnderTest) initCosmovisor(t *testing.T) { + binary := locateExecutable(s.execBinary) + s.ExecCosmovisor(t, false, "init", binary) +} + // MarkDirty whole chain will be reset when marked dirty func (s *SystemUnderTest) MarkDirty() { s.dirty = true diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index c9cb09820887..4e9fb7b27d7f 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -72,7 +72,7 @@ func TestChainUpgrade(t *testing.T) { systest.Sut.StopChain() currentBranchBinary := systest.Sut.ExecBinary() - currentInitializer := systest.Sut.TestnetInitializer() + //currentInitializer := systest.Sut.TestnetInitializer() legacyBinary := systest.WorkDir + "/binaries/v0.50/simd" systest.Sut.SetExecBinary(legacyBinary) @@ -84,6 +84,8 @@ func TestChainUpgrade(t *testing.T) { systest.Sut.StartChainWithCosmovisor(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) + systest.Sut.ExecCosmovisor(t, true, "add-upgrade", upgradeName, currentBranchBinary) + cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) govAddr := sdk.AccAddress(address.Module("gov")).String() // submit upgrade proposal @@ -115,16 +117,16 @@ func TestChainUpgrade(t *testing.T) { proposalStatus := gjson.Get(raw, "proposal.status").String() require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) - t.Log("waiting for upgrade info") - systest.Sut.AwaitUpgradeInfo(t) - systest.Sut.StopChain() - - t.Log("Upgrade height was reached. Upgrading chain") - systest.Sut.SetExecBinary(currentBranchBinary) - systest.Sut.SetTestnetInitializer(currentInitializer) - systest.Sut.StartChain(t) + //t.Log("waiting for upgrade info") + //systest.Sut.AwaitUpgradeInfo(t) + //systest.Sut.StopChain() - require.Equal(t, upgradeHeight+1, systest.Sut.CurrentHeight()) + //t.Log("Upgrade height was reached. Upgrading chain") + //systest.Sut.SetExecBinary(currentBranchBinary) + //systest.Sut.SetTestnetInitializer(currentInitializer) + //systest.Sut.StartChain(t) + // + //require.Equal(t, upgradeHeight+1, systest.Sut.CurrentHeight()) regex, err := regexp.Compile("DBG this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") require.NoError(t, err) From 8cbf93b4b01b1d7ee7c345aa069c0fde473bb059 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 13 Jun 2025 09:25:04 -0400 Subject: [PATCH 071/115] fix NPE error --- tools/cosmovisor/cmd/cosmovisor/add_upgrade.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 4e9d98fb1e07..c71df5e7faf3 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -94,6 +94,12 @@ func addUpgradeCmd(cmd *cobra.Command, args []string) error { } plan, err := addUpgrade(cfg, force, upgradeHeight, upgradeName, executablePath) + if err != nil { + return err + } + if plan == nil { + return nil // No plan to add + } return cfg.AddManualUpgrades(force, plan) } From 8ec692500855b7fb86de7280b448b1578f29fd0f Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 13 Jun 2025 09:52:56 -0400 Subject: [PATCH 072/115] create separate cosmovisor systemtest --- tests/systemtests/cosmovisor_test.go | 91 ++++++++++++++++++++++++++++ tests/systemtests/upgrade_test.go | 24 ++++---- tools/cosmovisor/go.mod | 78 ++++++++++++------------ tools/cosmovisor/go.sum | 35 +++++++++++ 4 files changed, 176 insertions(+), 52 deletions(-) create mode 100644 tests/systemtests/cosmovisor_test.go diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go new file mode 100644 index 000000000000..6b8d952efe7a --- /dev/null +++ b/tests/systemtests/cosmovisor_test.go @@ -0,0 +1,91 @@ +//go:build system_test + +package systemtests + +import ( + "fmt" + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + systest "cosmossdk.io/systemtests" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" +) + +func TestCosmovisorUpgrade(t *testing.T) { + // Scenario: + // start a legacy chain with some state + // when a chain upgrade proposal is executed + // then the chain upgrades successfully + systest.Sut.StopChain() + + currentBranchBinary := systest.Sut.ExecBinary() + //currentInitializer := systest.Sut.TestnetInitializer() + + legacyBinary := systest.WorkDir + "/binaries/v0.53/simd" + systest.Sut.SetExecBinary(legacyBinary) + systest.Sut.SetupChain() + + votingPeriod := 5 * time.Second // enough time to vote + systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) + + systest.Sut.StartChainWithCosmovisor(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) + + systest.Sut.ExecCosmovisor(t, true, "add-upgrade", upgradeName, currentBranchBinary) + + cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + govAddr := sdk.AccAddress(address.Module("gov")).String() + // submit upgrade proposal + proposal := fmt.Sprintf(` +{ + "messages": [ + { + "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + "authority": %q, + "plan": { + "name": %q, + "height": "%d" + } + } + ], + "metadata": "ipfs://CID", + "deposit": "100000000stake", + "title": "my upgrade", + "summary": "testing" +}`, govAddr, upgradeName, upgradeHeight) + proposalID := cli.SubmitAndVoteGovProposal(proposal) + t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) + raw := cli.CustomQuery("q", "gov", "proposal", proposalID) + t.Log(raw) + + systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 60*time.Second) + t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) + raw = cli.CustomQuery("q", "gov", "proposal", proposalID) + proposalStatus := gjson.Get(raw, "proposal.status").String() + require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + + //t.Log("waiting for upgrade info") + //systest.Sut.AwaitUpgradeInfo(t) + //systest.Sut.StopChain() + + //t.Log("Upgrade height was reached. Upgrading chain") + //systest.Sut.SetExecBinary(currentBranchBinary) + //systest.Sut.SetTestnetInitializer(currentInitializer) + //systest.Sut.StartChain(t) + // + //require.True(t, upgradeHeight+1 <= systest.Sut.CurrentHeight()) + + regex, err := regexp.Compile("DBG this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") + require.NoError(t, err) + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) + + // smoke test that new version runs + cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") + systest.RequireTxSuccess(t, got) +} diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index 1306a5a64e93..c741df3c1dee 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -31,7 +31,7 @@ func TestChainUpgrade(t *testing.T) { systest.Sut.StopChain() currentBranchBinary := systest.Sut.ExecBinary() - //currentInitializer := systest.Sut.TestnetInitializer() + currentInitializer := systest.Sut.TestnetInitializer() legacyBinary := systest.WorkDir + "/binaries/v0.53/simd" systest.Sut.SetExecBinary(legacyBinary) @@ -40,9 +40,7 @@ func TestChainUpgrade(t *testing.T) { votingPeriod := 5 * time.Second // enough time to vote systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) - systest.Sut.StartChainWithCosmovisor(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) - - systest.Sut.ExecCosmovisor(t, true, "add-upgrade", upgradeName, currentBranchBinary) + systest.Sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) govAddr := sdk.AccAddress(address.Module("gov")).String() @@ -75,16 +73,16 @@ func TestChainUpgrade(t *testing.T) { proposalStatus := gjson.Get(raw, "proposal.status").String() require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) - //t.Log("waiting for upgrade info") - //systest.Sut.AwaitUpgradeInfo(t) - //systest.Sut.StopChain() + t.Log("waiting for upgrade info") + systest.Sut.AwaitUpgradeInfo(t) + systest.Sut.StopChain() + + t.Log("Upgrade height was reached. Upgrading chain") + systest.Sut.SetExecBinary(currentBranchBinary) + systest.Sut.SetTestnetInitializer(currentInitializer) + systest.Sut.StartChain(t) - //t.Log("Upgrade height was reached. Upgrading chain") - //systest.Sut.SetExecBinary(currentBranchBinary) - //systest.Sut.SetTestnetInitializer(currentInitializer) - //systest.Sut.StartChain(t) - // - //require.True(t, upgradeHeight+1 <= systest.Sut.CurrentHeight()) + require.True(t, upgradeHeight+1 <= systest.Sut.CurrentHeight()) regex, err := regexp.Compile("DBG this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") require.NoError(t, err) diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index eaaa1c509d98..90ecc213791e 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -15,11 +15,11 @@ require ( github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.2 - google.golang.org/grpc v1.72.0 + google.golang.org/grpc v1.73.0 ) require ( - cel.dev/expr v0.20.0 // indirect + cel.dev/expr v0.23.0 // indirect cloud.google.com/go v0.118.0 // indirect cloud.google.com/go/auth v0.14.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect @@ -27,21 +27,21 @@ require ( cloud.google.com/go/iam v1.3.1 // indirect cloud.google.com/go/monitoring v1.22.1 // indirect cloud.google.com/go/storage v1.50.0 // indirect - cosmossdk.io/api v0.9.2 // indirect + cosmossdk.io/api v1.0.0-alpha.1 // indirect cosmossdk.io/collections v1.2.1 // indirect - cosmossdk.io/core v0.11.3 // indirect - cosmossdk.io/depinject v1.2.0 // indirect + cosmossdk.io/core v1.1.0-alpha.2 // indirect + cosmossdk.io/depinject v1.2.1 // indirect cosmossdk.io/errors v1.0.2 // indirect cosmossdk.io/math v1.5.3 // indirect cosmossdk.io/schema v1.1.0 // indirect - cosmossdk.io/store v1.1.2 // indirect - cosmossdk.io/x/tx v0.14.0 // indirect + cosmossdk.io/store v1.3.0-alpha.1 // indirect + cosmossdk.io/x/tx v1.2.0-alpha.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.2 // indirect github.com/DataDog/datadog-go v4.8.3+incompatible // indirect github.com/DataDog/zstd v1.5.7 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -54,38 +54,38 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect + github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/pebble v1.1.5 // indirect github.com/cockroachdb/redact v1.1.6 // indirect - github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect github.com/cometbft/cometbft v1.0.1 // indirect github.com/cometbft/cometbft-db v1.0.4 // indirect - github.com/cometbft/cometbft/api v1.0.0 // indirect + github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f // indirect github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-db v1.1.1 // indirect + github.com/cosmos/cosmos-db v1.1.3 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect - github.com/cosmos/iavl v1.2.2 // indirect + github.com/cosmos/iavl v1.2.6 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect github.com/danieljoos/wincred v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect - github.com/dgraph-io/badger/v4 v4.5.1 // indirect + github.com/dgraph-io/badger/v4 v4.6.0 // indirect github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect - github.com/emicklei/dot v1.6.2 // indirect + github.com/emicklei/dot v1.8.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/getsentry/sentry-go v0.32.0 // indirect + github.com/getsentry/sentry-go v0.33.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect @@ -97,14 +97,14 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v25.1.24+incompatible // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/orderedcode v0.0.1 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -135,7 +135,7 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/linxGnu/grocksdb v1.9.8 // indirect + github.com/linxGnu/grocksdb v1.10.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect @@ -151,9 +151,9 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect @@ -162,11 +162,11 @@ require ( github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.8.0 // indirect + github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/supranational/blst v0.3.13 // indirect + github.com/supranational/blst v0.3.14 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect github.com/tidwall/btree v1.7.0 // indirect @@ -178,29 +178,29 @@ require ( go.etcd.io/bbolt v1.4.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.17.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.10.0 // indirect google.golang.org/api v0.222.0 // indirect google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect diff --git a/tools/cosmovisor/go.sum b/tools/cosmovisor/go.sum index f9baef07528c..6085f3e0acc2 100644 --- a/tools/cosmovisor/go.sum +++ b/tools/cosmovisor/go.sum @@ -1,5 +1,6 @@ cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -618,6 +619,7 @@ cosmossdk.io/collections v1.2.1 h1:mAlNMs5vJwkda4TA+k5q/43p24RVAQ/qyDrjANu3BXE= cosmossdk.io/collections v1.2.1/go.mod h1:PSsEJ/fqny0VPsHLFT6gXDj/2C1tBOTS9eByK0+PBFU= cosmossdk.io/depinject v1.2.0 h1:6NW/FSK1IkWTrX7XxUpBmX1QMBozpEI9SsWkKTBc5zw= cosmossdk.io/depinject v1.2.0/go.mod h1:pvitjtUxZZZTQESKNS9KhGjWVslJZxtO9VooRJYyPjk= +cosmossdk.io/depinject v1.2.1/go.mod h1:lqQEycz0H2JXqvOgVwTsjEdMI0plswI7p6KX+MVqFOM= cosmossdk.io/errors v1.0.2 h1:wcYiJz08HThbWxd/L4jObeLaLySopyyuUFB5w4AGpCo= cosmossdk.io/errors v1.0.2/go.mod h1:0rjgiHkftRYPj//3DrD6y8hcm40HcPv/dR4R/4efr0k= cosmossdk.io/log v1.6.0 h1:SJIOmJ059wi1piyRgNRXKXhlDXGqnB5eQwhcZKv2tOk= @@ -646,6 +648,7 @@ github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0 h1:jJKWl98inONJAr/IZrdFQUWcwUO95DLY1XMD1ZIut+g= @@ -761,6 +764,7 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -778,6 +782,7 @@ github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4 github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/cometbft/cometbft v1.0.1 h1:JNVgbpL76sA4kXmBnyZ7iPjFAxi6HVp2l+rdT2RXVUs= github.com/cometbft/cometbft v1.0.1/go.mod h1:r9fEwrbU6Oxs11I2bLsfAiG37OMn0Vip0w9arYU0Nw0= @@ -785,6 +790,7 @@ github.com/cometbft/cometbft-db v1.0.4 h1:cezb8yx/ZWcF124wqUtAFjAuDksS1y1yXedvtp github.com/cometbft/cometbft-db v1.0.4/go.mod h1:M+BtHAGU2XLrpUxo3Nn1nOCcnVCiLM9yx5OuT0u5SCA= github.com/cometbft/cometbft/api v1.0.0 h1:gGBwvsJi/gnHJEtwYfjPIGs2AKg/Vfa1ZuKCPD1/Ko4= github.com/cometbft/cometbft/api v1.0.0/go.mod h1:EkQiqVSu/p2ebrZEnB2z6Re7r8XNe//M7ylR0qEwWm0= +github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f/go.mod h1:Ivh6nSCTJPQOyfQo8dgnyu/T88it092sEqSrZSmTQN8= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -795,6 +801,7 @@ github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/cosmos-db v1.1.3/go.mod h1:kN+wGsnwUJZYn8Sy5Q2O0vCYA99MJllkKASbs6Unb9U= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= @@ -806,6 +813,7 @@ github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fr github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= +github.com/cosmos/iavl v1.2.6/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9Edpel8TYpE= @@ -828,6 +836,7 @@ github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/badger/v4 v4.6.0/go.mod h1:KSJ5VTuZNC3Sd+YhvVjk2nYua9UZnnTr/SkXvdtiPgI= github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -850,6 +859,7 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -895,6 +905,7 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= +github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= @@ -1002,6 +1013,7 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -1009,6 +1021,7 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -1072,6 +1085,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5 github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -1238,6 +1252,7 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linxGnu/grocksdb v1.9.8 h1:vOIKv9/+HKiqJAElJIEYv3ZLcihRxyP7Suu/Mu8Dxjs= github.com/linxGnu/grocksdb v1.9.8/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= +github.com/linxGnu/grocksdb v1.10.1/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -1396,6 +1411,7 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= @@ -1405,6 +1421,7 @@ github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -1414,6 +1431,7 @@ github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/qmuntal/stateless v1.7.2 h1:FqCErOP+Hf+/FByJt/S4UOLHFJeTf8CbMrEE0AkYT8k= github.com/qmuntal/stateless v1.7.2/go.mod h1:n1HjRBM/cq4uCr3rfUjaMkgeGcd+ykAZwkjLje6jGBM= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -1465,6 +1483,7 @@ github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= @@ -1502,6 +1521,7 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= @@ -1554,22 +1574,28 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -1612,6 +1638,7 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1630,6 +1657,7 @@ golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8H golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1751,6 +1779,7 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1782,6 +1811,7 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1804,6 +1834,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1952,6 +1983,7 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2260,8 +2292,10 @@ google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422 h1:6GUHKGv2huWOHKm google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -2311,6 +2345,7 @@ google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3 google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From f9f5c62f376af5a656d769564166078f4dd0974f Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 13 Jun 2025 14:35:39 -0400 Subject: [PATCH 073/115] logging fixes --- systemtests/system.go | 2 +- tools/cosmovisor/go.mod | 11 +- tools/cosmovisor/go.sum | 123 +++++++----------- .../internal/watchers/http_block_checker.go | 8 +- 4 files changed, 54 insertions(+), 90 deletions(-) diff --git a/systemtests/system.go b/systemtests/system.go index 944b2b99180d..ccdbb1a884cd 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -224,8 +224,8 @@ func (s *SystemUnderTest) ExecCosmovisor(t *testing.T, async bool, args ...strin args..., ) cmd.Dir = WorkDir + env = append(env, "COSMOVISOR_COLOR_LOGS=false") cmd.Env = env - s.watchLogs(i, cmd) if async { require.NoError(t, cmd.Start(), "cosmovisor init %d", i) s.awaitProcessCleanup(cmd) diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index 90ecc213791e..70ef07517370 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -10,11 +10,9 @@ require ( github.com/fsnotify/fsnotify v1.9.0 github.com/otiai10/copy v1.14.1 github.com/pelletier/go-toml/v2 v2.2.4 - github.com/qmuntal/stateless v1.7.2 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 - go.uber.org/mock v0.5.2 google.golang.org/grpc v1.73.0 ) @@ -61,9 +59,9 @@ require ( github.com/cockroachdb/pebble v1.1.5 // indirect github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect - github.com/cometbft/cometbft v1.0.1 // indirect github.com/cometbft/cometbft-db v1.0.4 // indirect github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f // indirect + github.com/cometbft/cometbft/v2 v2.0.0-20250611063609-4e308d824f1f // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.3 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect @@ -83,19 +81,17 @@ require ( github.com/emicklei/dot v1.8.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/ethereum/go-ethereum v1.15.5 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/getsentry/sentry-go v0.33.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect @@ -124,6 +120,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect github.com/huandu/skiplist v1.2.1 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect @@ -136,6 +133,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/linxGnu/grocksdb v1.10.1 // indirect + github.com/lmittmann/tint v1.0.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect @@ -176,7 +174,6 @@ require ( github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.4.0 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect diff --git a/tools/cosmovisor/go.sum b/tools/cosmovisor/go.sum index 6085f3e0acc2..1c69ae8d38ae 100644 --- a/tools/cosmovisor/go.sum +++ b/tools/cosmovisor/go.sum @@ -1,5 +1,4 @@ -cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= -cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -617,8 +616,7 @@ cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= cosmossdk.io/collections v1.2.1 h1:mAlNMs5vJwkda4TA+k5q/43p24RVAQ/qyDrjANu3BXE= cosmossdk.io/collections v1.2.1/go.mod h1:PSsEJ/fqny0VPsHLFT6gXDj/2C1tBOTS9eByK0+PBFU= -cosmossdk.io/depinject v1.2.0 h1:6NW/FSK1IkWTrX7XxUpBmX1QMBozpEI9SsWkKTBc5zw= -cosmossdk.io/depinject v1.2.0/go.mod h1:pvitjtUxZZZTQESKNS9KhGjWVslJZxtO9VooRJYyPjk= +cosmossdk.io/depinject v1.2.1 h1:eD6FxkIjlVaNZT+dXTQuwQTKZrFZ4UrfCq1RKgzyhMw= cosmossdk.io/depinject v1.2.1/go.mod h1:lqQEycz0H2JXqvOgVwTsjEdMI0plswI7p6KX+MVqFOM= cosmossdk.io/errors v1.0.2 h1:wcYiJz08HThbWxd/L4jObeLaLySopyyuUFB5w4AGpCo= cosmossdk.io/errors v1.0.2/go.mod h1:0rjgiHkftRYPj//3DrD6y8hcm40HcPv/dR4R/4efr0k= @@ -646,8 +644,7 @@ github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bp github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw= @@ -762,8 +759,7 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= -github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= @@ -780,17 +776,15 @@ github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWL github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g= github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v1.0.1 h1:JNVgbpL76sA4kXmBnyZ7iPjFAxi6HVp2l+rdT2RXVUs= -github.com/cometbft/cometbft v1.0.1/go.mod h1:r9fEwrbU6Oxs11I2bLsfAiG37OMn0Vip0w9arYU0Nw0= github.com/cometbft/cometbft-db v1.0.4 h1:cezb8yx/ZWcF124wqUtAFjAuDksS1y1yXedvtprUFxs= github.com/cometbft/cometbft-db v1.0.4/go.mod h1:M+BtHAGU2XLrpUxo3Nn1nOCcnVCiLM9yx5OuT0u5SCA= -github.com/cometbft/cometbft/api v1.0.0 h1:gGBwvsJi/gnHJEtwYfjPIGs2AKg/Vfa1ZuKCPD1/Ko4= -github.com/cometbft/cometbft/api v1.0.0/go.mod h1:EkQiqVSu/p2ebrZEnB2z6Re7r8XNe//M7ylR0qEwWm0= +github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f h1:xVCNN5smIjz+EoEnBpV1+rlqeHFPoSyonelPevBMGW8= github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f/go.mod h1:Ivh6nSCTJPQOyfQo8dgnyu/T88it092sEqSrZSmTQN8= +github.com/cometbft/cometbft/v2 v2.0.0-20250611063609-4e308d824f1f h1:Gy5kTSwgwCMc5S/xjEc75ogN3worBYkTMscpCmVv3uM= +github.com/cometbft/cometbft/v2 v2.0.0-20250611063609-4e308d824f1f/go.mod h1:/ze08eO171CqUqTqAE7FW7ydUJIVkgp6e2svpYvIR3c= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -799,8 +793,7 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= -github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= -github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/cosmos-db v1.1.3 h1:7QNT77+vkefostcKkhrzDK9uoIEryzFrU9eoMeaQOPY= github.com/cosmos/cosmos-db v1.1.3/go.mod h1:kN+wGsnwUJZYn8Sy5Q2O0vCYA99MJllkKASbs6Unb9U= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= @@ -811,8 +804,7 @@ github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ github.com/cosmos/gogoproto v1.4.2/go.mod h1:cLxOsn1ljAHSV527CHOtaIP91kK6cCrZETRBrkzItWU= github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= -github.com/cosmos/iavl v1.2.2 h1:qHhKW3I70w+04g5KdsdVSHRbFLgt3yY3qTMd4Xa4rC8= -github.com/cosmos/iavl v1.2.2/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= +github.com/cosmos/iavl v1.2.6 h1:Hs3LndJbkIB+rEvToKJFXZvKo6Vy0Ex1SJ54hhtioIs= github.com/cosmos/iavl v1.2.6/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= @@ -834,8 +826,7 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= -github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= -github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/badger/v4 v4.6.0 h1:acOwfOOZ4p1dPRnYzvkVm7rUk2Y21TgPVepCy5dJdFQ= github.com/dgraph-io/badger/v4 v4.6.0/go.mod h1:KSJ5VTuZNC3Sd+YhvVjk2nYua9UZnnTr/SkXvdtiPgI= github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= @@ -857,8 +848,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= -github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -884,6 +874,8 @@ github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0+ github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/ethereum/go-ethereum v1.15.5 h1:Fo2TbBWC61lWVkFw9tsMoHCNX1ndpuaQBRJ8H6xLUPo= +github.com/ethereum/go-ethereum v1.15.5/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -903,8 +895,7 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= -github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= +github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg= github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -925,15 +916,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -1011,16 +998,15 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= -github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -1083,8 +1069,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -1152,8 +1137,9 @@ github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoD github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= @@ -1173,6 +1159,8 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= @@ -1250,9 +1238,10 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/linxGnu/grocksdb v1.9.8 h1:vOIKv9/+HKiqJAElJIEYv3ZLcihRxyP7Suu/Mu8Dxjs= -github.com/linxGnu/grocksdb v1.9.8/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= +github.com/linxGnu/grocksdb v1.10.1 h1:YX6gUcKvSC3d0s9DaqgbU+CRkZHzlELgHu1Z/kmtslg= github.com/linxGnu/grocksdb v1.10.1/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= @@ -1409,8 +1398,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -1419,8 +1407,7 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -1429,11 +1416,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/qmuntal/stateless v1.7.2 h1:FqCErOP+Hf+/FByJt/S4UOLHFJeTf8CbMrEE0AkYT8k= -github.com/qmuntal/stateless v1.7.2/go.mod h1:n1HjRBM/cq4uCr3rfUjaMkgeGcd+ykAZwkjLje6jGBM= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -1481,8 +1465,7 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= -github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= @@ -1519,8 +1502,7 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= -github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= @@ -1572,29 +1554,23 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -1636,8 +1612,7 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1655,8 +1630,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -1777,8 +1751,7 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1809,8 +1782,7 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1832,8 +1804,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1981,8 +1952,7 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2290,11 +2260,9 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422 h1:6GUHKGv2huWOHKmDXLMNE94q3fBDlEHI+oTRIZSebK0= google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg= -google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= -google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -2343,8 +2311,7 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/tools/cosmovisor/internal/watchers/http_block_checker.go b/tools/cosmovisor/internal/watchers/http_block_checker.go index 5bddeb940121..aac83d14eb71 100644 --- a/tools/cosmovisor/internal/watchers/http_block_checker.go +++ b/tools/cosmovisor/internal/watchers/http_block_checker.go @@ -12,7 +12,7 @@ import ( ) func NewHTTPRPCBLockChecker(baseUrl string, logger log.Logger) HeightChecker { - return httpRPCBlockChecker{ + return &httpRPCBlockChecker{ baseUrl: baseUrl, logger: logger, } @@ -24,7 +24,7 @@ type httpRPCBlockChecker struct { logger log.Logger } -func (j httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { +func (j *httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { if j.subUrl != "" { return j.getLatestBlockHeight(j.subUrl) } @@ -48,7 +48,7 @@ func (j httpRPCBlockChecker) GetLatestBlockHeight() (uint64, error) { return 0, fmt.Errorf("failed to get latest block height from both /block and /v1/block RPC endpoints: %w", errors.Join(err1, err2)) } -func (j httpRPCBlockChecker) getLatestBlockHeight(subUrl string) (uint64, error) { +func (j *httpRPCBlockChecker) getLatestBlockHeight(subUrl string) (uint64, error) { url := j.baseUrl + subUrl res, err := http.Get(url) if err != nil { @@ -64,7 +64,7 @@ func (j httpRPCBlockChecker) getLatestBlockHeight(subUrl string) (uint64, error) return getHeightFromRPCBlockResponse(bz) } -var _ HeightChecker = httpRPCBlockChecker{} +var _ HeightChecker = &httpRPCBlockChecker{} type Header struct { Height string `json:"height"` From e4e3feb10d575a358f2f4b019d53e54f469703c5 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 13 Jun 2025 16:10:41 -0400 Subject: [PATCH 074/115] WIP on cosmovisor system tests --- tests/systemtests/cosmovisor_test.go | 115 +++++++++++------- tools/cosmovisor/internal/runner.go | 2 +- .../internal/watchers/data_watcher_test.go | 2 +- .../watchers/file_poll_watcher_test.go | 2 +- tools/cosmovisor/internal/watchers/watcher.go | 17 +-- 5 files changed, 80 insertions(+), 58 deletions(-) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 6b8d952efe7a..73a74440b293 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -4,17 +4,10 @@ package systemtests import ( "fmt" - "regexp" "testing" "time" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - systest "cosmossdk.io/systemtests" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/address" ) func TestCosmovisorUpgrade(t *testing.T) { @@ -34,40 +27,68 @@ func TestCosmovisorUpgrade(t *testing.T) { votingPeriod := 5 * time.Second // enough time to vote systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) - systest.Sut.StartChainWithCosmovisor(t, fmt.Sprintf("--halt-height=%d", upgradeHeight+1)) - - systest.Sut.ExecCosmovisor(t, true, "add-upgrade", upgradeName, currentBranchBinary) - - cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) - govAddr := sdk.AccAddress(address.Module("gov")).String() - // submit upgrade proposal - proposal := fmt.Sprintf(` -{ - "messages": [ - { - "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", - "authority": %q, - "plan": { - "name": %q, - "height": "%d" - } - } - ], - "metadata": "ipfs://CID", - "deposit": "100000000stake", - "title": "my upgrade", - "summary": "testing" -}`, govAddr, upgradeName, upgradeHeight) - proposalID := cli.SubmitAndVoteGovProposal(proposal) - t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) - raw := cli.CustomQuery("q", "gov", "proposal", proposalID) - t.Log(raw) - - systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 60*time.Second) - t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) - raw = cli.CustomQuery("q", "gov", "proposal", proposalID) - proposalStatus := gjson.Get(raw, "proposal.status").String() - require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + systest.Sut.StartChainWithCosmovisor(t) + + systest.Sut.ExecCosmovisor( + t, + true, + "add-upgrade", + "old-to-new", + currentBranchBinary, + fmt.Sprintf("--upgrade-height=%d", 10), + ) + + systest.Sut.AwaitBlockHeight(t, 12) + + // TODO check logs for halt behavior + // TODO check current binary + + systest.Sut.ExecCosmovisor( + t, + true, + "add-upgrade", + "do-nothing-upgrade", + currentBranchBinary, + fmt.Sprintf("--upgrade-height=%d", 20), + ) + + // TODO check logs for halt behavior + // TODO check current binary + + systest.Sut.AwaitBlockHeight(t, 22) + + systest.Sut.StopChain() + + // cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + // govAddr := sdk.AccAddress(address.Module("gov")).String() + // // submit upgrade proposal + // proposal := fmt.Sprintf(` + //{ + // "messages": [ + // { + // "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + // "authority": %q, + // "plan": { + // "name": %q, + // "height": "%d" + // } + // } + // ], + // "metadata": "ipfs://CID", + // "deposit": "100000000stake", + // "title": "my upgrade", + // "summary": "testing" + //}`, govAddr, upgradeName, upgradeHeight) + // proposalID := cli.SubmitAndVoteGovProposal(proposal) + // t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) + // raw := cli.CustomQuery("q", "gov", "proposal", proposalID) + // t.Log(raw) + // + // systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 60*time.Second) + // t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) + // raw = cli.CustomQuery("q", "gov", "proposal", proposalID) + // proposalStatus := gjson.Get(raw, "proposal.status").String() + // require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) //t.Log("waiting for upgrade info") //systest.Sut.AwaitUpgradeInfo(t) @@ -80,12 +101,12 @@ func TestCosmovisorUpgrade(t *testing.T) { // //require.True(t, upgradeHeight+1 <= systest.Sut.CurrentHeight()) - regex, err := regexp.Compile("DBG this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") - require.NoError(t, err) - require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) + //regex, err := regexp.Compile("DBG this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") + //require.NoError(t, err) + //require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) // smoke test that new version runs - cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) - got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") - systest.RequireTxSuccess(t, got) + //cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + //got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") + //systest.RequireTxSuccess(t, got) } diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index ba7883307053..11725498fb63 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -153,7 +153,7 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 defer cancel() // start watchers for upgrade plans, manual upgrades and height updates - eh := watchers.LoggerErrorHandler(r.logger) + eh := watchers.DebugLoggerErrorHandler(r.logger) upgradePlanWatcher := watchers.InitFileWatcher[upgradetypes.Plan](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) manualUpgradesWatcher := watchers.InitFileWatcher[cosmovisor.ManualUpgradeBatch](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker(r.cfg.RPCAddress, r.logger) diff --git a/tools/cosmovisor/internal/watchers/data_watcher_test.go b/tools/cosmovisor/internal/watchers/data_watcher_test.go index d0d565464ea5..0a3a2b993adb 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/data_watcher_test.go @@ -23,7 +23,7 @@ func TestDataWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile.json") ctx, cancel := context.WithCancel(context.Background()) - eh := LoggerErrorHandler(log.NewTestLogger(t)) + eh := DebugLoggerErrorHandler(log.NewTestLogger(t)) pollWatcher := NewFilePollWatcher(ctx, eh, filename, time.Millisecond*100) dataWatcher := NewDataWatcher[TestData](ctx, eh, pollWatcher, func(contents []byte) (TestData, error) { var data TestData diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go index 75ade08bb073..f545e71f3277 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go @@ -17,7 +17,7 @@ func TestPollWatcher(t *testing.T) { filename := filepath.Join(dir, "testfile") ctx, cancel := context.WithCancel(context.Background()) - eh := LoggerErrorHandler(log.NewTestLogger(t)) + eh := DebugLoggerErrorHandler(log.NewTestLogger(t)) watcher := NewFilePollWatcher(ctx, eh, filename, time.Millisecond*100) expectedContent := []byte("test") go func() { diff --git a/tools/cosmovisor/internal/watchers/watcher.go b/tools/cosmovisor/internal/watchers/watcher.go index 26975092d83f..c7f0c1c99e2e 100644 --- a/tools/cosmovisor/internal/watchers/watcher.go +++ b/tools/cosmovisor/internal/watchers/watcher.go @@ -21,21 +21,22 @@ type ErrorHandler interface { Warn(msg string, err error) } -type loggerErrorHandler struct { +type debugLoggerErrorHandler struct { logger log.Logger } -func (h *loggerErrorHandler) Error(msg string, err error) { - h.logger.Error(msg, "error", err) +func (h *debugLoggerErrorHandler) Error(msg string, err error) { + h.logger.Warn(msg, "error", err) } -func (h *loggerErrorHandler) Warn(msg string, err error) { - h.logger.Warn(msg, "error", err) +func (h *debugLoggerErrorHandler) Warn(msg string, err error) { + h.logger.Debug(msg, "error", err) } -// LoggerErrorHandler returns an ErrorHandler that logs errors and warnings using the provided logger. -func LoggerErrorHandler(logger log.Logger) ErrorHandler { - return &loggerErrorHandler{logger: logger} +// DebugLoggerErrorHandler returns an ErrorHandler that logs errors and warnings using the provided logger, +// but downgrades errors to warnings and warnings to debug logs. +func DebugLoggerErrorHandler(logger log.Logger) ErrorHandler { + return &debugLoggerErrorHandler{logger: logger} } // InitFileWatcher initializes a file watcher which uses either both fsnotify and polling (hybrid watcher) or just polling, From 41be9b055f77f9923842d115604333e7a9c62377 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 13 Jun 2025 19:02:02 -0400 Subject: [PATCH 075/115] WIP on cosmovisor system tests --- tests/systemtests/cosmovisor_test.go | 57 +++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 73a74440b293..08802d3b986e 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -7,7 +7,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + systest "cosmossdk.io/systemtests" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" ) func TestCosmovisorUpgrade(t *testing.T) { @@ -29,35 +34,67 @@ func TestCosmovisorUpgrade(t *testing.T) { systest.Sut.StartChainWithCosmovisor(t) + cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + govAddr := sdk.AccAddress(address.Module("gov")).String() + // submit upgrade proposal + proposal := fmt.Sprintf(` + { + "messages": [ + { + "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + "authority": %q, + "plan": { + "name": %q, + "height": "%d" + } + } + ], + "metadata": "ipfs://CID", + "deposit": "100000000stake", + "title": "my upgrade", + "summary": "testing" + }`, govAddr, upgradeName, upgradeHeight) + proposalID := cli.SubmitAndVoteGovProposal(proposal) + + // add manual upgrade systest.Sut.ExecCosmovisor( t, true, "add-upgrade", - "old-to-new", + "manual1", currentBranchBinary, fmt.Sprintf("--upgrade-height=%d", 10), ) - systest.Sut.AwaitBlockHeight(t, 12) - - // TODO check logs for halt behavior - // TODO check current binary - + // add binary for gov upgrade systest.Sut.ExecCosmovisor( t, true, "add-upgrade", - "do-nothing-upgrade", + upgradeName, currentBranchBinary, - fmt.Sprintf("--upgrade-height=%d", 20), ) + systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 60*time.Second) + + t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) + raw := cli.CustomQuery("q", "gov", "proposal", proposalID) + proposalStatus := gjson.Get(raw, "proposal.status").String() + require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + // TODO check logs for halt behavior // TODO check current binary - systest.Sut.AwaitBlockHeight(t, 22) + systest.Sut.AwaitBlockHeight(t, upgradeHeight+1) + // TODO check logs for halt behavior + // TODO check current binary - systest.Sut.StopChain() + // smoke test that new version runs + cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") + systest.RequireTxSuccess(t, got) + + //systest.Sut.StopChain() // cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) // govAddr := sdk.AccAddress(address.Module("gov")).String() From 2ffffdebcb17e9d441378ae80a1d30a5a8f1e753 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 16 Jun 2025 12:41:05 -0400 Subject: [PATCH 076/115] working cosmovisor system tests --- tests/systemtests/cosmovisor_test.go | 98 +++++++++------------------- tests/systemtests/upgrade_test.go | 9 ++- 2 files changed, 36 insertions(+), 71 deletions(-) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 08802d3b986e..19982326a456 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -4,6 +4,7 @@ package systemtests import ( "fmt" + "regexp" "testing" "time" @@ -16,6 +17,12 @@ import ( ) func TestCosmovisorUpgrade(t *testing.T) { + const ( + upgrade1Height = 25 + upgrade1Name = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go + upgrade2Height int64 = 30 + upgrade2Name = "manual1" + ) // Scenario: // start a legacy chain with some state // when a chain upgrade proposal is executed @@ -53,97 +60,52 @@ func TestCosmovisorUpgrade(t *testing.T) { "deposit": "100000000stake", "title": "my upgrade", "summary": "testing" - }`, govAddr, upgradeName, upgradeHeight) + }`, govAddr, upgrade1Name, upgrade1Height) proposalID := cli.SubmitAndVoteGovProposal(proposal) - // add manual upgrade + // add binary for gov upgrade systest.Sut.ExecCosmovisor( t, true, "add-upgrade", - "manual1", + upgrade1Name, currentBranchBinary, - fmt.Sprintf("--upgrade-height=%d", 10), ) - // add binary for gov upgrade + systest.Sut.AwaitBlockHeight(t, 21, 60*time.Second) + + t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) + raw := cli.CustomQuery("q", "gov", "proposal", proposalID) + proposalStatus := gjson.Get(raw, "proposal.status").String() + require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + + // add manual upgrade systest.Sut.ExecCosmovisor( t, true, "add-upgrade", - upgradeName, + upgrade2Name, currentBranchBinary, + fmt.Sprintf("--upgrade-height=%d", upgrade2Height), ) - systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 60*time.Second) + systest.Sut.AwaitBlockHeight(t, upgrade1Height+1) - t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) - raw := cli.CustomQuery("q", "gov", "proposal", proposalID) - proposalStatus := gjson.Get(raw, "proposal.status").String() - require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) - - // TODO check logs for halt behavior + regex, err := regexp.Compile(fmt.Sprintf(`UPGRADE %q NEEDED at height: %d: module=x/upgrade`, + upgrade1Name, upgrade1Height)) + require.NoError(t, err) + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) // TODO check current binary - systest.Sut.AwaitBlockHeight(t, upgradeHeight+1) - // TODO check logs for halt behavior + systest.Sut.AwaitBlockHeight(t, upgrade2Height+1) + regex, err = regexp.Compile(fmt.Sprintf(`halt per configuration height %d`, + upgrade2Height)) + require.NoError(t, err) + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) // TODO check current binary // smoke test that new version runs cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") systest.RequireTxSuccess(t, got) - - //systest.Sut.StopChain() - - // cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) - // govAddr := sdk.AccAddress(address.Module("gov")).String() - // // submit upgrade proposal - // proposal := fmt.Sprintf(` - //{ - // "messages": [ - // { - // "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", - // "authority": %q, - // "plan": { - // "name": %q, - // "height": "%d" - // } - // } - // ], - // "metadata": "ipfs://CID", - // "deposit": "100000000stake", - // "title": "my upgrade", - // "summary": "testing" - //}`, govAddr, upgradeName, upgradeHeight) - // proposalID := cli.SubmitAndVoteGovProposal(proposal) - // t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) - // raw := cli.CustomQuery("q", "gov", "proposal", proposalID) - // t.Log(raw) - // - // systest.Sut.AwaitBlockHeight(t, upgradeHeight-1, 60*time.Second) - // t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) - // raw = cli.CustomQuery("q", "gov", "proposal", proposalID) - // proposalStatus := gjson.Get(raw, "proposal.status").String() - // require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) - - //t.Log("waiting for upgrade info") - //systest.Sut.AwaitUpgradeInfo(t) - //systest.Sut.StopChain() - - //t.Log("Upgrade height was reached. Upgrading chain") - //systest.Sut.SetExecBinary(currentBranchBinary) - //systest.Sut.SetTestnetInitializer(currentInitializer) - //systest.Sut.StartChain(t) - // - //require.True(t, upgradeHeight+1 <= systest.Sut.CurrentHeight()) - - //regex, err := regexp.Compile("DBG this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") - //require.NoError(t, err) - //require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) - - // smoke test that new version runs - //cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) - //got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") - //systest.RequireTxSuccess(t, got) } diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index c741df3c1dee..9978cb010988 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -18,12 +18,15 @@ import ( ) const ( - testSeed = "scene learn remember glide apple expand quality spawn property shoe lamp carry upset blossom draft reject aim file trash miss script joy only measure" - upgradeHeight int64 = 22 - upgradeName = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go + testSeed = "scene learn remember glide apple expand quality spawn property shoe lamp carry upset blossom draft reject aim file trash miss script joy only measure" ) func TestChainUpgrade(t *testing.T) { + const ( + upgradeHeight int64 = 22 + upgradeName = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go + ) + // Scenario: // start a legacy chain with some state // when a chain upgrade proposal is executed From f190de8879ca44c19cf524207746690c539d785d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 24 Jun 2025 12:10:33 +0200 Subject: [PATCH 077/115] only start height watcher if we have a halt height set --- .../cmd/cosmovisor/mockchain_test.go | 2 +- tools/cosmovisor/internal/runner.go | 18 ++++++------ .../internal/watchers/file_poll_watcher.go | 4 ++- .../internal/watchers/height_watcher.go | 8 +++-- .../internal/watchers/poll_watcher.go | 29 ++++++++++++------- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index d6bca8c2541d..2b4486604fc2 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -102,7 +102,7 @@ func TestMockChain(t *testing.T) { }.Setup(t) addManualUpgrade1 := func() { - time.Sleep(pollInterval * 2) // wait for startup + time.Sleep(pollInterval * 3) // wait a bit rootCmd := NewRootCmd() rootCmd.SetArgs([]string{ "add-upgrade", diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 11725498fb63..2caa44b5809f 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -23,15 +23,15 @@ type Runner struct { } // NewRunner creates a new Runner instance with the provided configuration and logger. -func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) Runner { - return Runner{ +func NewRunner(cfg *cosmovisor.Config, runCfg RunConfig, logger log.Logger) *Runner { + return &Runner{ runCfg: runCfg, cfg: cfg, logger: logger, } } -func (r Runner) Start(ctx context.Context, args []string) error { +func (r *Runner) Start(ctx context.Context, args []string) error { retryMgr := NewRetryBackoffManager(r.logger, r.cfg.MaxRestartRetries) for { // First we check if we need to upgrade and if we do we perform the upgrade @@ -106,7 +106,7 @@ var ErrUpgradeNoDaemonRestart = errors.New("upgrade completed, but DAEMON_RESTAR // This is called to determine run arguments first and allows us to observe whether // run arguments have changed or if the process is in a restart loop because of some error, // which is important for the retry backoff manager. -func (r Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, err error) { +func (r *Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, err error) { bin, err := r.cfg.CurrentBin() if err != nil { return nil, 0, fmt.Errorf("error creating symlink to genesis: %w", err) @@ -135,7 +135,7 @@ func (r Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64, } // RunProcess runs the given command until either a upgrade is detected or the process exits. -func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64) error { +func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64) error { // start the fsnotify watcher to watch for changes in the upgrade info directory dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.logger, r.cfg.UpgradeInfoDir(), []string{ r.cfg.UpgradeInfoFilePath(), @@ -157,15 +157,15 @@ func (r Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64 upgradePlanWatcher := watchers.InitFileWatcher[upgradetypes.Plan](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) manualUpgradesWatcher := watchers.InitFileWatcher[cosmovisor.ManualUpgradeBatch](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker(r.cfg.RPCAddress, r.logger) - // TODO should we have a separate poll interval for the height watcher? - heightWatcher := watchers.NewHeightWatcher(ctx, eh, heightChecker, r.cfg.PollInterval, func(height uint64) error { + heightWatcher := watchers.NewHeightWatcher(eh, heightChecker, r.cfg.PollInterval, func(height uint64) error { r.knownHeight = height return r.cfg.WriteLastKnownHeight(height) }) if haltHeight > 0 { - // TODO start height watcher only if we have a halt height set - // currently the height watcher is always checking for the height even when we don't have a halt height set + // only watch for height updates if we have a halt height set + r.logger.Info("Starting height watcher", "halt_height", haltHeight) + heightWatcher.Start(ctx) } r.logger.Info("Starting process", "path", cmd.Path, "args", cmd.Args) diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher.go b/tools/cosmovisor/internal/watchers/file_poll_watcher.go index a35d6627399f..753815811a48 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher.go @@ -33,5 +33,7 @@ func NewFilePollWatcher(ctx context.Context, errorHandler ErrorHandler, filename } return nil, os.ErrNotExist } - return NewPollWatcher[[]byte](ctx, errorHandler, check, pollInterval) + watcher := NewPollWatcher[[]byte](errorHandler, check, pollInterval) + watcher.Start(ctx) + return watcher } diff --git a/tools/cosmovisor/internal/watchers/height_watcher.go b/tools/cosmovisor/internal/watchers/height_watcher.go index d26091b956d1..1dad6c572352 100644 --- a/tools/cosmovisor/internal/watchers/height_watcher.go +++ b/tools/cosmovisor/internal/watchers/height_watcher.go @@ -15,18 +15,22 @@ type HeightWatcher struct { onGetHeight func(uint64) error } -func NewHeightWatcher(ctx context.Context, errorHandler ErrorHandler, checker HeightChecker, pollInterval time.Duration, onGetHeight func(uint64) error) *HeightWatcher { +func NewHeightWatcher(errorHandler ErrorHandler, checker HeightChecker, pollInterval time.Duration, onGetHeight func(uint64) error) *HeightWatcher { watcher := &HeightWatcher{ checker: checker, onGetHeight: onGetHeight, } - watcher.PollWatcher = NewPollWatcher[uint64](ctx, errorHandler, func() (uint64, error) { + watcher.PollWatcher = NewPollWatcher[uint64](errorHandler, func() (uint64, error) { return watcher.ReadNow() }, pollInterval) return watcher } +func (h HeightWatcher) Start(ctx context.Context) { + h.PollWatcher.Start(ctx) +} + func (h HeightWatcher) ReadNow() (uint64, error) { height, err := h.checker.GetLatestBlockHeight() if err != nil { diff --git a/tools/cosmovisor/internal/watchers/poll_watcher.go b/tools/cosmovisor/internal/watchers/poll_watcher.go index c15f3134ef80..8c8ac8905437 100644 --- a/tools/cosmovisor/internal/watchers/poll_watcher.go +++ b/tools/cosmovisor/internal/watchers/poll_watcher.go @@ -8,39 +8,48 @@ import ( ) type PollWatcher[T any] struct { - outChan chan T + outChan chan T + errorHandler ErrorHandler + checker func() (T, error) + pollInterval time.Duration } -func NewPollWatcher[T any](ctx context.Context, errorHandler ErrorHandler, checker func() (T, error), pollInterval time.Duration) *PollWatcher[T] { +func NewPollWatcher[T any](errorHandler ErrorHandler, checker func() (T, error), pollInterval time.Duration) *PollWatcher[T] { outChan := make(chan T, 1) - ticker := time.NewTicker(pollInterval) + return &PollWatcher[T]{ + errorHandler: errorHandler, + checker: checker, + pollInterval: pollInterval, + outChan: outChan, + } +} + +func (w *PollWatcher[T]) Start(ctx context.Context) { + ticker := time.NewTicker(w.pollInterval) go func() { defer ticker.Stop() - defer close(outChan) + defer close(w.outChan) for { select { case <-ctx.Done(): return case <-ticker.C: - x, err := checker() + x, err := w.checker() if err != nil { if !os.IsNotExist(err) { - errorHandler.Error("failed to check for updates", err) + w.errorHandler.Error("failed to check for updates", err) } } else { // to make PollWatcher generic on any type T (including []byte), we use reflect.DeepEqual and the default zero value of T var zero T if !reflect.DeepEqual(x, zero) { - outChan <- x + w.outChan <- x } } } } }() - return &PollWatcher[T]{ - outChan: outChan, - } } func (w *PollWatcher[T]) Updated() <-chan T { From 896202ff89096f9c66489df3f9cc71f54afe46e1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 24 Jun 2025 14:35:20 +0200 Subject: [PATCH 078/115] remove completed TODOs --- tools/cosmovisor/cmd/cosmovisor/add_upgrade.go | 1 - tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 14 +------------- tools/cosmovisor/cmd/mock_node/main.go | 2 -- tools/cosmovisor/internal/runner.go | 3 +-- tools/cosmovisor/internal/upgrader.go | 1 - tools/cosmovisor/manual.go | 1 - 6 files changed, 2 insertions(+), 20 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index c71df5e7faf3..08e985df94a7 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -28,7 +28,6 @@ func NewAddUpgradeCmd() *cobra.Command { } // addUpgrade adds upgrade info to manifest -// TODO batch-upgrade and add-upgrade should write to the same batch file func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath string) (*upgradetypes.Plan, error) { logger := cfg.Logger(os.Stdout) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 2b4486604fc2..33adb5e8dc77 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -16,8 +16,7 @@ import ( ) type MockChainSetup struct { - Genesis string - // TODO test setup should be similar to process_test.go + Genesis string GovUpgrades map[string]string ManualUpgrades map[string]string // to be added with the add-upgrade command Config *cosmovisor.Config @@ -199,15 +198,4 @@ func TestMockChain(t *testing.T) { wg.Wait() require.Equal(t, 8, callbackCount) - - // TODO: - // - [x] add callback on restart for checking state - // - [ ] add manual upgrade (manual20) at height 20 - // - [ ] then add other manual upgrades manual10 at height 10 and manual30 at height 30 as a batch - // - [ ] manual20 should get picked up and the process should restart with halt-height 20 - // - [ ] then manual10 should get picked up and the process should restart with halt-height 10 - // - [ ] when manual10 gets applied, it should restart with halt-height 20 - // - [ ] when manual20 gets applied, it should restart with no halt-height - // - [ ] and then manual20 should trigger gov2 upgrade at height 40 - // - [ ] } diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 87e0d99476e8..7d58e0eb9ba5 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -50,8 +50,6 @@ x/upgrade upgrade-info.json behavior.`, cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", 0, "Duration to wait before shutting down the node upon receiving a shutdown signal. Defaults to 0 (no delay).") cmd.Flags().BoolVar(&shutdownOnUpgrade, "shutdown-on-upgrade", false, "If true, the node will shutdown immediately after reaching the upgrade height. If false, it will continue running until a shutdown signal is received. Defaults to false.") cmd.Flags().BoolVar(&upgradeInfoEncodingJson, "upgrade-info-encoding-json", false, "If true, the upgrade-info.json will be encoded using encoding/json instead of jsonpb. This is useful for testing compatibility with different JSON decoders. Defaults to false (uses jsonpb).") - // TODO add flag to use either jsonpb or encoding/json - // TODO shutdown at upgrade height cmd.RunE = func(cmd *cobra.Command, args []string) error { if upgradePlan == "" && haltHeight == 0 { return fmt.Errorf("must specify either --upgrade-plan or --halt-height") diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 2caa44b5809f..62a430cf077a 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -171,7 +171,7 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 r.logger.Info("Starting process", "path", cmd.Path, "args", cmd.Args) processRunner := RunProcess(cmd) defer func() { - // TODO always check for the latest block height before shutting down so that we have it in the last known height file + // always check for the latest block height before shutting down so that we have it in the last known height file _, _ = heightChecker.GetLatestBlockHeight() _ = processRunner.Shutdown(r.cfg.ShutdownGrace) }() @@ -185,7 +185,6 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 return errDone case _, ok := <-upgradePlanWatcher.Updated(): // TODO check skip upgrade heights?? (although not sure why we need this as the node should not emit an upgrade plan if skip heights is enabled) - // TODO should we double check we're at the right height in case operators manually create this file? if !ok { return nil } diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index 4f65583acd3a..02756aad62a8 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -22,7 +22,6 @@ type UpgradeCheckResult struct { func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint64) (upgraded bool, err error) { // if we see upgrade-info.json, assume we are at the right height and upgrade - // TODO should we check the height when we have upgrade-info.json? logger.Info("Checking for upgrade-info.json") if upgradePlan, err := cfg.UpgradeInfo(); err == nil { err := DoUpgrade(cfg, logger, upgradePlan) diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index 21a6a65fa0e0..db4ffee05391 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -96,7 +96,6 @@ func (cfg *Config) RemoveManualUpgrade(height int64) error { func (cfg *Config) saveManualUpgrades(manualUpgrades ManualUpgradeBatch) error { sortUpgrades(manualUpgrades) - // TODO we should not write the file every time we add an upgrade, but only once per command otherwise we can trigger spurious manualUpgradesData, err := json.MarshalIndent(manualUpgrades, "", " ") if err != nil { return err From 1e7cc7279c4e6200f11ffc68485a6d6f9a867831 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 25 Jun 2025 19:35:02 +0200 Subject: [PATCH 079/115] update system test make task to include cosmovisor --- Makefile | 2 +- systemtests/system.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ec36a316f827..cddfd69a9438 100644 --- a/Makefile +++ b/Makefile @@ -505,7 +505,7 @@ localnet-debug: localnet-stop localnet-build-dlv localnet-build-nodes .PHONY: localnet-start localnet-stop localnet-debug localnet-build-env localnet-build-dlv localnet-build-nodes -test-system: build-v53 build +test-system: build-v53 build cosmovisor mkdir -p ./tests/systemtests/binaries/ cp $(BUILDDIR)/simd ./tests/systemtests/binaries/ mkdir -p ./tests/systemtests/binaries/v0.53 diff --git a/systemtests/system.go b/systemtests/system.go index ccdbb1a884cd..c6902bb0e3f4 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -220,7 +220,7 @@ func (s *SystemUnderTest) ExecCosmovisor(t *testing.T, async bool, args ...strin env := s.cosmovisorEnv(t, home) t.Logf("Calling Cosmovisor with args %+v and env %+v", args, env) cmd := exec.Command( - "cosmovisor", + "../../tools/cosmovisor/cosmovisor", args..., ) cmd.Dir = WorkDir From 2577a326af9c5f6d77d16e3b30e647967a422450 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 25 Jun 2025 20:56:07 +0200 Subject: [PATCH 080/115] WIP on adding some non-determinism during manual upgrade --- Makefile | 12 ++++++++++-- simapp/app_di.go | 24 ++++++++++++++++++++++++ systemtests/system.go | 7 +++++-- tests/systemtests/cosmovisor_test.go | 14 ++++++++++++-- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index cddfd69a9438..a7183db7f724 100644 --- a/Makefile +++ b/Makefile @@ -505,13 +505,21 @@ localnet-debug: localnet-stop localnet-build-dlv localnet-build-nodes .PHONY: localnet-start localnet-stop localnet-debug localnet-build-env localnet-build-dlv localnet-build-nodes -test-system: build-v53 build cosmovisor +# build-system-test-current builds the binaries necessary for running system tests, but only those on the current branch +# this is useful if you are iterating on tests which rely on changes to the current branch only (which is most common in development) +build-system-test-current: build cosmovisor mkdir -p ./tests/systemtests/binaries/ cp $(BUILDDIR)/simd ./tests/systemtests/binaries/ + cp tools/cosmovisor/cosmovisor ./tests/systemtests/binaries/ + +# build-system-test builds the binaries necessary for runnings system tests and places them in the correct locations +build-system-test: build-v53 mkdir -p ./tests/systemtests/binaries/v0.53 mv $(BUILDDIR)/simdv53 ./tests/systemtests/binaries/v0.53/simd + +test-system: build-system-test $(MAKE) -C tests/systemtests test -.PHONY: test-system +.PHONY: build-system-test-current build-system-test test-system # build-v53 checks out the v0.53.x branch, builds the binary, and renames it to simdv53. build-v53: diff --git a/simapp/app_di.go b/simapp/app_di.go index 974bdf9adff6..2c8db48eff0d 100644 --- a/simapp/app_di.go +++ b/simapp/app_di.go @@ -3,8 +3,12 @@ package simapp import ( + "fmt" "io" + "os" + "strconv" + abci "github.com/cometbft/cometbft/v2/abci/types" dbm "github.com/cosmos/cosmos-db" clienthelpers "cosmossdk.io/client/v2/helpers" @@ -22,6 +26,7 @@ import ( "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" testdata_pulsar "github.com/cosmos/cosmos-sdk/testutil/testdata/testpb" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/ante" @@ -268,6 +273,25 @@ func NewSimApp( return app } +// PreBlocker application updates every pre block +func (app *SimApp) PreBlocker(ctx sdk.Context, req *abci.FinalizeBlockRequest) (*sdk.ResponsePreBlock, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + curHeight := sdkCtx.BlockHeight() + testManualUpgradeHeight, haveManualUpgrade := os.LookupEnv("TEST_MANUAL_UPGRADE_HEIGHT") + + if haveManualUpgrade { + upgradeHeight, err := strconv.ParseInt(testManualUpgradeHeight, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid TEST_MANUAL_UPGRADE_HEIGHT: %w", err) + } + app.Logger().Info("TEST_MANUAL_UPGRADE_HEIGHT is set", "upgrade_height", upgradeHeight, "current_height", curHeight) + if curHeight == upgradeHeight { + app.Logger().Info("TEST_MANUAL_UPGRADE_HEIGHT is set, running manual upgrade logic", "height", upgradeHeight) + } + } + return app.App.PreBlocker(ctx, req) +} + // setAnteHandler sets custom ante handlers. // "x/auth/tx" pre-defined ante handler have been disabled in app_config. func (app *SimApp) setAnteHandler(txConfig client.TxConfig) { diff --git a/systemtests/system.go b/systemtests/system.go index c6902bb0e3f4..e8415b631bb2 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -215,12 +215,15 @@ func (s *SystemUnderTest) cosmovisorEnv(t *testing.T, home string) []string { } } +func (s *SystemUnderTest) cosmovisorPath() string { + return filepath.Join(WorkDir, "binaries", "cosmovisor") +} func (s *SystemUnderTest) ExecCosmovisor(t *testing.T, async bool, args ...string) { s.withEachNodeHome(func(i int, home string) { env := s.cosmovisorEnv(t, home) t.Logf("Calling Cosmovisor with args %+v and env %+v", args, env) cmd := exec.Command( - "../../tools/cosmovisor/cosmovisor", + s.cosmovisorPath(), args..., ) cmd.Dir = WorkDir @@ -645,7 +648,7 @@ func (s *SystemUnderTest) startNodesAsync(t *testing.T, useCosmovisor bool, xarg var binary string var env []string if useCosmovisor { - binary = "cosmovisor" + binary = s.cosmovisorPath() args = append([]string{"run"}, args...) // cosmovisor run cfgPath := filepath.Join(absHome, "cosmovisor", "config.toml") args = append(args, "--cosmovisor-config", cfgPath) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 19982326a456..44a7b2bca641 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -4,6 +4,8 @@ package systemtests import ( "fmt" + "os" + "path/filepath" "regexp" "testing" "time" @@ -30,7 +32,6 @@ func TestCosmovisorUpgrade(t *testing.T) { systest.Sut.StopChain() currentBranchBinary := systest.Sut.ExecBinary() - //currentInitializer := systest.Sut.TestnetInitializer() legacyBinary := systest.WorkDir + "/binaries/v0.53/simd" systest.Sut.SetExecBinary(legacyBinary) @@ -79,13 +80,22 @@ func TestCosmovisorUpgrade(t *testing.T) { proposalStatus := gjson.Get(raw, "proposal.status").String() require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + wrapperTxt := fmt.Sprintf(`#!/usr/bin/env bash +set -e +TEST_MANUAL_UPGRADE_HEIGHT="%d" exec %s "$@"`, upgrade2Height, currentBranchBinary) + wrapperPath := filepath.Join(systest.WorkDir, "testnet", fmt.Sprintf("%s.sh", upgrade2Name)) + wrapperPath, err := filepath.Abs(wrapperPath) + require.NoError(t, err, "failed to get absolute path for manual upgrade script") + err = os.WriteFile(wrapperPath, []byte(wrapperTxt), 0o755) + require.NoError(t, err, "failed to write manual upgrade script") + // add manual upgrade systest.Sut.ExecCosmovisor( t, true, "add-upgrade", upgrade2Name, - currentBranchBinary, + wrapperPath, fmt.Sprintf("--upgrade-height=%d", upgrade2Height), ) From 3175579fdf69398e3f10c049dab28126b9b158e7 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 26 Jun 2025 18:27:21 +0200 Subject: [PATCH 081/115] add a proper manual upgrade test --- simapp/app_di.go | 24 --- simapp/upgrades.go | 18 +++ tests/systemtests/cosmovisor_test.go | 206 ++++++++++++++++---------- tools/cosmovisor/internal/upgrader.go | 1 + x/upgrade/abci.go | 22 +++ x/upgrade/keeper/keeper.go | 22 +++ 6 files changed, 187 insertions(+), 106 deletions(-) diff --git a/simapp/app_di.go b/simapp/app_di.go index 2c8db48eff0d..974bdf9adff6 100644 --- a/simapp/app_di.go +++ b/simapp/app_di.go @@ -3,12 +3,8 @@ package simapp import ( - "fmt" "io" - "os" - "strconv" - abci "github.com/cometbft/cometbft/v2/abci/types" dbm "github.com/cosmos/cosmos-db" clienthelpers "cosmossdk.io/client/v2/helpers" @@ -26,7 +22,6 @@ import ( "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" testdata_pulsar "github.com/cosmos/cosmos-sdk/testutil/testdata/testpb" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/ante" @@ -273,25 +268,6 @@ func NewSimApp( return app } -// PreBlocker application updates every pre block -func (app *SimApp) PreBlocker(ctx sdk.Context, req *abci.FinalizeBlockRequest) (*sdk.ResponsePreBlock, error) { - sdkCtx := sdk.UnwrapSDKContext(ctx) - curHeight := sdkCtx.BlockHeight() - testManualUpgradeHeight, haveManualUpgrade := os.LookupEnv("TEST_MANUAL_UPGRADE_HEIGHT") - - if haveManualUpgrade { - upgradeHeight, err := strconv.ParseInt(testManualUpgradeHeight, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid TEST_MANUAL_UPGRADE_HEIGHT: %w", err) - } - app.Logger().Info("TEST_MANUAL_UPGRADE_HEIGHT is set", "upgrade_height", upgradeHeight, "current_height", curHeight) - if curHeight == upgradeHeight { - app.Logger().Info("TEST_MANUAL_UPGRADE_HEIGHT is set, running manual upgrade logic", "height", upgradeHeight) - } - } - return app.App.PreBlocker(ctx, req) -} - // setAnteHandler sets custom ante handlers. // "x/auth/tx" pre-defined ante handler have been disabled in app_config. func (app *SimApp) setAnteHandler(txConfig client.TxConfig) { diff --git a/simapp/upgrades.go b/simapp/upgrades.go index 1c8e2c9d8146..69e0c57e10ce 100644 --- a/simapp/upgrades.go +++ b/simapp/upgrades.go @@ -2,6 +2,8 @@ package simapp import ( "context" + "os" + "strconv" storetypes "cosmossdk.io/store/types" @@ -32,6 +34,22 @@ func (app SimApp) RegisterUpgradeHandlers() { panic(err) } + // this allows us to check migration to v0.54.x in the system tests via a manual (non-governance upgrade) + if manualUpgrade, ok := os.LookupEnv("SIMAPP_MANUAL_UPGRADE_HEIGHT"); ok { + height, err := strconv.ParseUint(manualUpgrade, 10, 64) + if err != nil { + panic("invalid SIMAPP_MANUAL_UPGRADE_HEIGHT height: " + err.Error()) + } + upgradeInfo = upgradetypes.Plan{ + Name: UpgradeName, + Height: int64(height), + } + err = app.UpgradeKeeper.SetManualUpgrade(&upgradeInfo) + if err != nil { + panic("failed to set manual upgrade: " + err.Error()) + } + } + if upgradeInfo.Name == UpgradeName && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { storeUpgrades := storetypes.StoreUpgrades{ Added: []string{}, diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 44a7b2bca641..85d29eaf8064 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -19,33 +19,35 @@ import ( ) func TestCosmovisorUpgrade(t *testing.T) { - const ( - upgrade1Height = 25 - upgrade1Name = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go - upgrade2Height int64 = 30 - upgrade2Name = "manual1" - ) - // Scenario: - // start a legacy chain with some state - // when a chain upgrade proposal is executed - // then the chain upgrades successfully - systest.Sut.StopChain() - - currentBranchBinary := systest.Sut.ExecBinary() - - legacyBinary := systest.WorkDir + "/binaries/v0.53/simd" - systest.Sut.SetExecBinary(legacyBinary) - systest.Sut.SetupChain() - - votingPeriod := 5 * time.Second // enough time to vote - systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) - - systest.Sut.StartChainWithCosmovisor(t) - - cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) - govAddr := sdk.AccAddress(address.Module("gov")).String() - // submit upgrade proposal - proposal := fmt.Sprintf(` + t.Run("gov upgrade, then manual upgrade", func(t *testing.T) { + const ( + upgrade1Height = 25 + upgrade1Name = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go + upgrade2Height int64 = 30 + upgrade2Name = "manual1" + ) + + // Scenario: + // start a legacy chain with some state + // when a chain upgrade proposal is executed + // then the chain upgrades successfully + systest.Sut.StopChain() + + currentBranchBinary := systest.Sut.ExecBinary() + + legacyBinary := systest.WorkDir + "/binaries/v0.53/simd" + systest.Sut.SetExecBinary(legacyBinary) + systest.Sut.SetupChain() + + votingPeriod := 5 * time.Second // enough time to vote + systest.Sut.ModifyGenesisJSON(t, systest.SetGovVotingPeriod(t, votingPeriod)) + + systest.Sut.StartChainWithCosmovisor(t) + + cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + govAddr := sdk.AccAddress(address.Module("gov")).String() + // submit upgrade proposal + proposal := fmt.Sprintf(` { "messages": [ { @@ -62,60 +64,100 @@ func TestCosmovisorUpgrade(t *testing.T) { "title": "my upgrade", "summary": "testing" }`, govAddr, upgrade1Name, upgrade1Height) - proposalID := cli.SubmitAndVoteGovProposal(proposal) - - // add binary for gov upgrade - systest.Sut.ExecCosmovisor( - t, - true, - "add-upgrade", - upgrade1Name, - currentBranchBinary, - ) - - systest.Sut.AwaitBlockHeight(t, 21, 60*time.Second) - - t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) - raw := cli.CustomQuery("q", "gov", "proposal", proposalID) - proposalStatus := gjson.Get(raw, "proposal.status").String() - require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) - - wrapperTxt := fmt.Sprintf(`#!/usr/bin/env bash + proposalID := cli.SubmitAndVoteGovProposal(proposal) + + // add binary for gov upgrade + systest.Sut.ExecCosmovisor( + t, + true, + "add-upgrade", + upgrade1Name, + currentBranchBinary, + ) + + systest.Sut.AwaitBlockHeight(t, 21, 60*time.Second) + + t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) + raw := cli.CustomQuery("q", "gov", "proposal", proposalID) + proposalStatus := gjson.Get(raw, "proposal.status").String() + require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + + // add manual upgrade + systest.Sut.ExecCosmovisor( + t, + true, + "add-upgrade", + upgrade2Name, + currentBranchBinary, + fmt.Sprintf("--upgrade-height=%d", upgrade2Height), + ) + + systest.Sut.AwaitBlockHeight(t, upgrade1Height+1) + + regex, err := regexp.Compile(fmt.Sprintf(`UPGRADE %q NEEDED at height: %d: module=x/upgrade`, + upgrade1Name, upgrade1Height)) + require.NoError(t, err) + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) + // TODO check current binary + + systest.Sut.AwaitBlockHeight(t, upgrade2Height+1) + regex, err = regexp.Compile(fmt.Sprintf(`halt per configuration height %d`, + upgrade2Height)) + require.NoError(t, err) + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) + // TODO check current binary + + // smoke test that new version runs + cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") + systest.RequireTxSuccess(t, got) + }) + + t.Run("manual upgrade", func(t *testing.T) { + const ( + upgradeHeight = 10 + upgradeName = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go + ) + // Scenario: + // start a legacy chain with some state + // when a chain upgrade proposal is executed + // then the chain upgrades successfully + systest.Sut.StopChain() + + currentBranchBinary := systest.Sut.ExecBinary() + + legacyBinary := systest.WorkDir + "/binaries/v0.53/simd" + systest.Sut.SetExecBinary(legacyBinary) + systest.Sut.SetupChain() + + systest.Sut.StartChainWithCosmovisor(t) + + // we create a wrapper for the current branch binary which sets the + // SIMAPP_MANUAL_UPGRADE_HEIGHT which will cause the upgrade to be applied manually + wrapperTxt := fmt.Sprintf(`#!/usr/bin/env bash set -e -TEST_MANUAL_UPGRADE_HEIGHT="%d" exec %s "$@"`, upgrade2Height, currentBranchBinary) - wrapperPath := filepath.Join(systest.WorkDir, "testnet", fmt.Sprintf("%s.sh", upgrade2Name)) - wrapperPath, err := filepath.Abs(wrapperPath) - require.NoError(t, err, "failed to get absolute path for manual upgrade script") - err = os.WriteFile(wrapperPath, []byte(wrapperTxt), 0o755) - require.NoError(t, err, "failed to write manual upgrade script") - - // add manual upgrade - systest.Sut.ExecCosmovisor( - t, - true, - "add-upgrade", - upgrade2Name, - wrapperPath, - fmt.Sprintf("--upgrade-height=%d", upgrade2Height), - ) - - systest.Sut.AwaitBlockHeight(t, upgrade1Height+1) - - regex, err := regexp.Compile(fmt.Sprintf(`UPGRADE %q NEEDED at height: %d: module=x/upgrade`, - upgrade1Name, upgrade1Height)) - require.NoError(t, err) - require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) - // TODO check current binary - - systest.Sut.AwaitBlockHeight(t, upgrade2Height+1) - regex, err = regexp.Compile(fmt.Sprintf(`halt per configuration height %d`, - upgrade2Height)) - require.NoError(t, err) - require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) - // TODO check current binary - - // smoke test that new version runs - cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) - got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") - systest.RequireTxSuccess(t, got) +SIMAPP_MANUAL_UPGRADE_HEIGHT="%d" exec %s "$@"`, upgradeHeight, currentBranchBinary) + wrapperPath := filepath.Join(systest.WorkDir, "testnet", fmt.Sprintf("%s.sh", upgradeName)) + wrapperPath, err := filepath.Abs(wrapperPath) + require.NoError(t, err, "failed to get absolute path for manual upgrade script") + err = os.WriteFile(wrapperPath, []byte(wrapperTxt), 0o755) + require.NoError(t, err, "failed to write manual upgrade script") + + // schedule manual upgrade to latest version + systest.Sut.ExecCosmovisor( + t, + true, + "add-upgrade", + upgradeName, + wrapperPath, + fmt.Sprintf("--upgrade-height=%d", upgradeHeight), + ) + + systest.Sut.AwaitBlockHeight(t, upgradeHeight+1, 60*time.Second) + + // smoke test that new version runs + cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) + got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") + systest.RequireTxSuccess(t, got) + }) } diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index 02756aad62a8..9dfd1e3506ea 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -29,6 +29,7 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint return false, err } // remove the upgrade-info.json file after a successful upgrade, otherwise we will keep trying to upgrade + // TODO is this actually safe to do, it seems like some logic in simapp actually depends on the file being present, so maybe x/upgrade should clear it?? logger.Info("Removing completed upgrade plan", "height", upgradePlan.Height, "name", upgradePlan.Name) file := cfg.UpgradeInfoFilePath() err = os.Remove(file) diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index e4cec38b0c22..bacc5d912e5e 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -62,6 +62,28 @@ func PreBlocker(ctx context.Context, k *keeper.Keeper) (appmodule.ResponsePreBlo } if !found { + // check for manual upgrade + manualPlan := k.GetManualUpgrade() + if manualPlan != nil && manualPlan.Height == blockHeight { + // if we have a manual upgrade, we execute it + logger := k.Logger(ctx) + logger.Info(fmt.Sprintf("applying manual upgrade \"%s\" at %d", manualPlan.Name, blockHeight)) + sdkCtx = sdkCtx.WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) + if err := k.ApplyUpgrade(sdkCtx, *manualPlan); err != nil { + return nil, err + } + + // clear the manual upgrade plan + err = k.SetManualUpgrade(nil) + if err != nil { + return nil, fmt.Errorf("failed to clear manual upgrade plan: %w", err) + } + + return &sdk.ResponsePreBlock{ + ConsensusParamsChanged: true, + }, nil + } + return &sdk.ResponsePreBlock{ ConsensusParamsChanged: false, }, nil diff --git a/x/upgrade/keeper/keeper.go b/x/upgrade/keeper/keeper.go index b8b424aba1d5..8e9aa77bc7ce 100644 --- a/x/upgrade/keeper/keeper.go +++ b/x/upgrade/keeper/keeper.go @@ -47,6 +47,7 @@ type Keeper struct { downgradeVerified bool // tells if we've already sanity checked that this binary version isn't being used against an old state. authority string // the address capable of executing and canceling an upgrade. Usually the gov module account initVersionMap module.VersionMap // the module version map at init genesis + manualUpgradeInfo *types.Plan } // NewKeeper constructs an upgrade Keeper which requires the following arguments: @@ -600,3 +601,24 @@ func (k *Keeper) SetDowngradeVerified(v bool) { func (k Keeper) DowngradeVerified() bool { return k.downgradeVerified } + +// SetManualUpgrade sets the manual upgrade plan. +// Currently, this e +// If the plan is nil, it clears the existing manual upgrade info. +func (k Keeper) SetManualUpgrade(plan *types.Plan) error { + if plan == nil { + k.manualUpgradeInfo = nil + return nil + } + + if err := plan.ValidateBasic(); err != nil { + return err + } + + k.manualUpgradeInfo = plan + return nil +} + +func (k Keeper) GetManualUpgrade() *types.Plan { + return k.manualUpgradeInfo +} From b8a4948213d293252557cdd85312a08e68ff85fc Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 26 Jun 2025 19:42:48 +0200 Subject: [PATCH 082/115] fix test --- tests/systemtests/cosmovisor_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 85d29eaf8064..b1d9e3370508 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -101,10 +101,6 @@ func TestCosmovisorUpgrade(t *testing.T) { // TODO check current binary systest.Sut.AwaitBlockHeight(t, upgrade2Height+1) - regex, err = regexp.Compile(fmt.Sprintf(`halt per configuration height %d`, - upgrade2Height)) - require.NoError(t, err) - require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) // TODO check current binary // smoke test that new version runs From 36fe230e8de6ac0683a720a123c7394b07a3b838 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 26 Jun 2025 20:43:13 +0200 Subject: [PATCH 083/115] don't delete upgrade-info.json file, instead check that the upgrade name has changed --- simapp/upgrades.go | 1 + tools/cosmovisor/README.md | 4 +-- tools/cosmovisor/args.go | 42 ++++++++++++++++++++++----- tools/cosmovisor/internal/runner.go | 10 +++++-- tools/cosmovisor/internal/upgrader.go | 23 +++++++-------- 5 files changed, 53 insertions(+), 27 deletions(-) diff --git a/simapp/upgrades.go b/simapp/upgrades.go index 69e0c57e10ce..0c2328c97678 100644 --- a/simapp/upgrades.go +++ b/simapp/upgrades.go @@ -33,6 +33,7 @@ func (app SimApp) RegisterUpgradeHandlers() { if err != nil { panic(err) } + app.Logger().Debug("read upgrade info from disk", "upgrade_info", upgradeInfo) // this allows us to check migration to v0.54.x in the system tests via a manual (non-governance upgrade) if manualUpgrade, ok := os.LookupEnv("SIMAPP_MANUAL_UPGRADE_HEIGHT"); ok { diff --git a/tools/cosmovisor/README.md b/tools/cosmovisor/README.md index 225b975b8583..11ba40eb8141 100644 --- a/tools/cosmovisor/README.md +++ b/tools/cosmovisor/README.md @@ -243,8 +243,7 @@ The following heuristic is applied to detect the upgrade: * If `cosmovisor/current/upgrade-info.json` doesn't exist but `data/upgrade-info.json` exists, then `cosmovisor` assumes that whatever is in `data/upgrade-info.json` is a valid upgrade request. In this case `cosmovisor` tries immediately to make an upgrade according to the `name` attribute in `data/upgrade-info.json`. -* Otherwise, `cosmovisor` waits for changes in `upgrade-info.json`. As soon as a new upgrade name is recorded in the - file, `cosmovisor` will trigger an upgrade mechanism. +* If `cosmovisor/current/upgrade-info.json` exists, `cosmovisor` waits for changes in `upgrade-info.json`. As soon as a new upgrade name different from the current one is recorded in the file, `cosmovisor` will trigger an upgrade mechanism. When the upgrade mechanism is triggered, `cosmovisor` will: @@ -252,7 +251,6 @@ When the upgrade mechanism is triggered, `cosmovisor` will: where `` is the `upgrade-info.json:name` attribute); 2. update the `current` symbolic link to point to the new directory and save `data/upgrade-info.json` to `cosmovisor/current/upgrade-info.json`. -3. deleted `data/upgrade-info.json` to avoid further triggering of the upgrade mechanism. ### Scheduling Manual Upgrades diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index ef39c87dd740..01ccc634531c 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -442,32 +442,58 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) { return err } -// UpgradeInfo returns the current upgrade info -func (cfg *Config) UpgradeInfo() (upgradetypes.Plan, error) { +// PendingUpgradeInfo returns pending upgrade info written by x/upgrade. +func (cfg *Config) PendingUpgradeInfo() (*upgradetypes.Plan, error) { filename := cfg.UpgradeInfoFilePath() _, err := os.Lstat(filename) var bz []byte if err != nil { // no current directory - return upgradetypes.Plan{}, fmt.Errorf("failed to read %q: %w", filename, err) + return nil, fmt.Errorf("failed to read %q: %w", filename, err) } if bz, err = os.ReadFile(filename); err != nil { - return upgradetypes.Plan{}, fmt.Errorf("failed to read %q: %w", filename, err) + return nil, fmt.Errorf("failed to read %q: %w", filename, err) } return cfg.ParseUpgradeInfo(bz) } -func (cfg *Config) ParseUpgradeInfo(bz []byte) (upgradetypes.Plan, error) { +// CurrentBinaryUpgradeInfo returns the upgrade info for the current active binary, if any. +func (cfg *Config) CurrentBinaryUpgradeInfo() (*upgradetypes.Plan, error) { + filename := filepath.Join(cfg.Root(), currentLink, upgradetypes.UpgradeInfoFilename) + bz, err := os.ReadFile(filename) + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to read %q: %w", filename, err) + } + return cfg.ParseUpgradeInfo(bz) +} + +// CurrentBinaryUpgradeName returns the upgrade info for the current active binary, if any. +func (cfg *Config) CurrentBinaryUpgradeName() string { + upgradeInfo, err := cfg.CurrentBinaryUpgradeInfo() + if err != nil { + return "" + } + if upgradeInfo == nil { + return "" + } + return upgradeInfo.Name +} + +// ParseUpgradeInfo parses the upgrade info from the given byte slice. +func (cfg *Config) ParseUpgradeInfo(bz []byte) (*upgradetypes.Plan, error) { var upgradePlan upgradetypes.Plan if err := jsonpb.Unmarshal(bytes.NewReader(bz), &upgradePlan); err != nil { - return upgradetypes.Plan{}, fmt.Errorf("error unmarshalling upgrade info: %w", err) + return nil, fmt.Errorf("error unmarshalling upgrade info: %w", err) } if err := upgradePlan.ValidateBasic(); err != nil { - return upgradetypes.Plan{}, fmt.Errorf("upgrade info failed validation upgrade inof: %w", err) + return nil, fmt.Errorf("upgrade info failed validation upgrade inof: %w", err) } if !cfg.DisableRecase { upgradePlan.Name = strings.ToLower(upgradePlan.Name) } - return upgradePlan, nil + return &upgradePlan, nil } const LastKnownHeightFile = ".last_known_height" diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 62a430cf077a..3450179df568 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -136,6 +136,7 @@ func (r *Runner) ComputeRunPlan(args []string) (cmd *exec.Cmd, haltHeight uint64 // RunProcess runs the given command until either a upgrade is detected or the process exits. func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint64) error { + currentBinaryUpgradeName := r.cfg.CurrentBinaryUpgradeName() // start the fsnotify watcher to watch for changes in the upgrade info directory dirWatcher, err := watchers.NewFSNotifyWatcher(ctx, r.logger, r.cfg.UpgradeInfoDir(), []string{ r.cfg.UpgradeInfoFilePath(), @@ -154,7 +155,7 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 // start watchers for upgrade plans, manual upgrades and height updates eh := watchers.DebugLoggerErrorHandler(r.logger) - upgradePlanWatcher := watchers.InitFileWatcher[upgradetypes.Plan](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) + upgradePlanWatcher := watchers.InitFileWatcher[*upgradetypes.Plan](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoFilePath(), r.cfg.ParseUpgradeInfo) manualUpgradesWatcher := watchers.InitFileWatcher[cosmovisor.ManualUpgradeBatch](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker(r.cfg.RPCAddress, r.logger) heightWatcher := watchers.NewHeightWatcher(eh, heightChecker, r.cfg.PollInterval, func(height uint64) error { @@ -183,13 +184,16 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 case <-parentCtx.Done(): r.logger.Info("Parent context cancelled, shutting down") return errDone - case _, ok := <-upgradePlanWatcher.Updated(): + case upgradePlan, ok := <-upgradePlanWatcher.Updated(): // TODO check skip upgrade heights?? (although not sure why we need this as the node should not emit an upgrade plan if skip heights is enabled) if !ok { return nil } r.logger.Info("Received upgrade-info.json") - return ErrRestartNeeded{} + if upgradePlan.Name != currentBinaryUpgradeName { + // only restart if we have a different upgrade name than the current binary's upgrade name + return ErrRestartNeeded{} + } case manualUpgrades, ok := <-manualUpgradesWatcher.Updated(): if !ok { return nil diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index 9dfd1e3506ea..481f22811b44 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -23,19 +23,16 @@ type UpgradeCheckResult struct { func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint64) (upgraded bool, err error) { // if we see upgrade-info.json, assume we are at the right height and upgrade logger.Info("Checking for upgrade-info.json") - if upgradePlan, err := cfg.UpgradeInfo(); err == nil { + currentBinaryUpgradeName := cfg.CurrentBinaryUpgradeName() + logger.Debug("read current binary's upgrade info", "name", currentBinaryUpgradeName) + // only upgrade if we have a pending upgrade plan with a different name from the current binary's upgrade plan + if upgradePlan, err := cfg.PendingUpgradeInfo(); err == nil && + upgradePlan != nil && + upgradePlan.Name != currentBinaryUpgradeName { err := DoUpgrade(cfg, logger, upgradePlan) if err != nil { return false, err } - // remove the upgrade-info.json file after a successful upgrade, otherwise we will keep trying to upgrade - // TODO is this actually safe to do, it seems like some logic in simapp actually depends on the file being present, so maybe x/upgrade should clear it?? - logger.Info("Removing completed upgrade plan", "height", upgradePlan.Height, "name", upgradePlan.Name) - file := cfg.UpgradeInfoFilePath() - err = os.Remove(file) - if err != nil { - return true, fmt.Errorf("failed to remove upgrade-info.json: %w", err) - } return true, nil } logger.Info("Checking for upgrade-info.json.batch") @@ -52,7 +49,7 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint haltHeight := uint64(manualUpgrade.Height) if lastKnownHeight == haltHeight { logger.Info("At manual upgrade", "upgrade", manualUpgrade, "halt_height", haltHeight) - err := DoUpgrade(cfg, logger, *manualUpgrade) + err := DoUpgrade(cfg, logger, manualUpgrade) if err != nil { return false, err } @@ -74,10 +71,10 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint type Upgrader struct { cfg *cosmovisor.Config logger log.Logger - upgradePlan upgradetypes.Plan + upgradePlan *upgradetypes.Plan } -func DoUpgrade(cfg *cosmovisor.Config, logger log.Logger, upgradePlan upgradetypes.Plan) error { +func DoUpgrade(cfg *cosmovisor.Config, logger log.Logger, upgradePlan *upgradetypes.Plan) error { upgrader := &Upgrader{ cfg: cfg, logger: logger, @@ -108,7 +105,7 @@ func (u *Upgrader) DoUpgrade() error { return err } - if err := UpgradeBinary(u.logger, u.cfg, u.upgradePlan); err != nil { + if err := UpgradeBinary(u.logger, u.cfg, *u.upgradePlan); err != nil { return err } From 2b82656430931e4557604682f84a2e61e322aa0c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 10:17:42 +0200 Subject: [PATCH 084/115] rename --- tools/cosmovisor/internal/runner.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 3450179df568..275bce354618 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -16,10 +16,10 @@ import ( ) type Runner struct { - runCfg RunConfig - cfg *cosmovisor.Config - logger log.Logger - knownHeight uint64 + runCfg RunConfig + cfg *cosmovisor.Config + logger log.Logger + lastSeenHeight uint64 } // NewRunner creates a new Runner instance with the provided configuration and logger. @@ -35,7 +35,7 @@ func (r *Runner) Start(ctx context.Context, args []string) error { retryMgr := NewRetryBackoffManager(r.logger, r.cfg.MaxRestartRetries) for { // First we check if we need to upgrade and if we do we perform the upgrade - upgraded, err := UpgradeIfNeeded(r.cfg, r.logger, r.knownHeight) + upgraded, err := UpgradeIfNeeded(r.cfg, r.logger, r.lastSeenHeight) if err != nil { return err } @@ -159,7 +159,7 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 manualUpgradesWatcher := watchers.InitFileWatcher[cosmovisor.ManualUpgradeBatch](ctx, eh, r.cfg.PollInterval, dirWatcher, r.cfg.UpgradeInfoBatchFilePath(), r.cfg.ParseManualUpgrades) heightChecker := watchers.NewHTTPRPCBLockChecker(r.cfg.RPCAddress, r.logger) heightWatcher := watchers.NewHeightWatcher(eh, heightChecker, r.cfg.PollInterval, func(height uint64) error { - r.knownHeight = height + r.lastSeenHeight = height return r.cfg.WriteLastKnownHeight(height) }) From 0c5ad5394c53250d9bfc5986ef57c9ff02fb6ae4 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 10:36:25 +0200 Subject: [PATCH 085/115] move shutdown go routine to run because that's where it's really relevant --- tools/cosmovisor/cmd/cosmovisor/main.go | 19 +------------------ tools/cosmovisor/cmd/cosmovisor/run.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/main.go b/tools/cosmovisor/cmd/cosmovisor/main.go index 7fcb8ecb552c..d5d54a7c7203 100644 --- a/tools/cosmovisor/cmd/cosmovisor/main.go +++ b/tools/cosmovisor/cmd/cosmovisor/main.go @@ -1,28 +1,11 @@ package main import ( - "context" - "fmt" "os" - "os/signal" - "syscall" - "time" ) func main() { - ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - shutdownChan := make(chan os.Signal, 1) - signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM) - // ensure we shutdown if the process is killed and context cancellation doesn't cause an exit - go func() { - <-shutdownChan - fmt.Println("Received shutdown signal, exiting gracefully...") - // TODO configure this timeout - time.Sleep(10 * time.Second) - fmt.Println("Forcing process shutdown") - os.Exit(0) - }() - if err := NewRootCmd().ExecuteContext(ctx); err != nil { + if err := NewRootCmd().Execute(); err != nil { os.Exit(1) } } diff --git a/tools/cosmovisor/cmd/cosmovisor/run.go b/tools/cosmovisor/cmd/cosmovisor/run.go index ffc3ca9b410e..07effff4ca47 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run.go +++ b/tools/cosmovisor/cmd/cosmovisor/run.go @@ -4,7 +4,10 @@ import ( "context" "fmt" "os" + "os/signal" "strings" + "syscall" + "time" "github.com/spf13/cobra" @@ -37,6 +40,18 @@ func run(ctx context.Context, cfgPath string, args []string, options ...RunOptio return err } + ctx, _ = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + shutdownChan := make(chan os.Signal, 1) + signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM) + // ensure we shutdown if the process is killed and context cancellation doesn't cause an exit on its own + go func() { + <-shutdownChan + fmt.Println("Received shutdown signal, exiting gracefully...") + time.Sleep(cfg.ShutdownGrace) + fmt.Println("Forcing process shutdown") + os.Exit(0) + }() + runCfg := DefaultRunConfig for _, opt := range options { opt(&runCfg) From 0d07bcec13586071764e341d3baf03d7bfd89c8b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 10:43:54 +0200 Subject: [PATCH 086/115] address TODOs --- tools/cosmovisor/internal/backoff.go | 1 - tools/cosmovisor/internal/process_test.go | 4 +--- tools/cosmovisor/internal/runner.go | 4 ---- tools/cosmovisor/internal/upgrader.go | 5 +++-- tools/cosmovisor/manual.go | 1 - 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/tools/cosmovisor/internal/backoff.go b/tools/cosmovisor/internal/backoff.go index 9e2e1c599189..a563ba80dc14 100644 --- a/tools/cosmovisor/internal/backoff.go +++ b/tools/cosmovisor/internal/backoff.go @@ -50,7 +50,6 @@ func (r *RetryBackoffManager) BeforeRun(cmd string, args []string) error { } else { r.retryCount++ if r.maxRestarts > 0 && r.retryCount >= r.maxRestarts { - // TODO we should return the last error that the process returned here return backoff.Permanent(fmt.Errorf("maximum number of restarts reached: %d", r.maxRestarts)) } // if the command and arguments are the same, we wait for the next backoff interval diff --git a/tools/cosmovisor/internal/process_test.go b/tools/cosmovisor/internal/process_test.go index f340b0420994..99bcdbcf409c 100644 --- a/tools/cosmovisor/internal/process_test.go +++ b/tools/cosmovisor/internal/process_test.go @@ -30,15 +30,13 @@ func init() { workDir = filepath.Join(dir, "..") } -// TODO all these tests share the same setup so we can extract it to a common function - type launchProcessFixture struct { cfg *cosmovisor.Config stdin *os.File stdout *buffer stderr *buffer logger log.Logger - runner Runner + runner *Runner } // TestLaunchProcess will try running the script a few times and watch upgrades work properly diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 275bce354618..2fc2b4f45ff1 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -62,8 +62,6 @@ func (r *Runner) Start(ctx context.Context, args []string) error { // We pass the current command and args to the retry manager so it can check whether // the command or its arguments have changed (e.g. if the binary was updated or the halt height changed), // or if we're just in some sort of error restart loop. - // TODO add tests for this behavior - // TODO should DAEMON_RESTART_AFTER_UPGRADE apply here? it is explicitly about upgrades, but is the user's intention to prevent all restarts? if err := retryMgr.BeforeRun(cmd.Path, cmd.Args); err != nil { return err } @@ -75,8 +73,6 @@ func (r *Runner) Start(ctx context.Context, args []string) error { testCallback() } - // TODO should restart delay go here? or should it be handled when upgrading - // Now we actually run the process err = r.RunProcess(ctx, cmd, haltHeight) // There are three types of cases we're checking for here: diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index 481f22811b44..9dc0610c39ff 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -61,8 +61,9 @@ func UpgradeIfNeeded(cfg *cosmovisor.Config, logger log.Logger, knownHeight uint } return true, err } else if lastKnownHeight > haltHeight { - // TODO should we just warn here? or actually return an error? - return false, fmt.Errorf("missed manual upgrade %s at height %d, last known height is %d", manualUpgrade.Name, manualUpgrade.Height, lastKnownHeight) + // if the last known height is past the halt height, we assume that we are in an error condition and have missed the halt height! + return false, fmt.Errorf("last known height is %d, but we have manual upgrade %s scheduled for height %d which is before the current height! For safety, Cosmovisor assumes that this is an error condition that requires operator intervention. If the manual upgrade was scheduled by error, please remove it from upgrade-info.json.batch and restart Cosmovisor", + lastKnownHeight, manualUpgrade.Name, manualUpgrade.Height) } } return false, nil diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index db4ffee05391..f6f68a18f446 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -43,7 +43,6 @@ func (cfg *Config) AddManualUpgrades(forceOverwrite bool, plans ...*upgradetypes if len(plans) == 0 { return nil } - // TODO only allow plans that are AFTER the last known height existing, err := cfg.ReadManualUpgrades() if err != nil { return err From 9cbc2603f729a1b73c83f1f115cd283f51cddcbd Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 11:02:43 +0200 Subject: [PATCH 087/115] remove TODO --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 33adb5e8dc77..a60f6a587729 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -84,7 +84,7 @@ func TestMockChain(t *testing.T) { cfg := &cosmovisor.Config{ PollInterval: pollInterval, RestartAfterUpgrade: true, - RPCAddress: "http://localhost:26657", // TODO this should be the default! + RPCAddress: "http://localhost:26657", } mockchainDir, cfgFile := MockChainSetup{ Genesis: "--block-time 1s --upgrade-plan '{\"name\":\"gov1\",\"height\":30}'", From f93a7cb04092c6c2137a7ae1882a8dce955b757b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 11:15:27 +0200 Subject: [PATCH 088/115] fix scanner_test.go --- tools/cosmovisor/scanner_test.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/cosmovisor/scanner_test.go b/tools/cosmovisor/scanner_test.go index 38b6ad2ccebe..f725270e072f 100644 --- a/tools/cosmovisor/scanner_test.go +++ b/tools/cosmovisor/scanner_test.go @@ -13,65 +13,65 @@ import ( func TestParseUpgradeInfoFile(t *testing.T) { cases := []struct { filename string - expectUpgrade upgradetypes.Plan + expectUpgrade *upgradetypes.Plan disableRecase bool expectErr string }{ { filename: "f1-good.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{Name: "upgrade1", Info: "some info", Height: 123}, + expectUpgrade: &upgradetypes.Plan{Name: "upgrade1", Info: "some info", Height: 123}, }, { filename: "f2-normalized-name.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{Name: "upgrade2", Info: "some info", Height: 125}, + expectUpgrade: &upgradetypes.Plan{Name: "upgrade2", Info: "some info", Height: 125}, }, { filename: "f2-normalized-name.json", disableRecase: true, - expectUpgrade: upgradetypes.Plan{Name: "Upgrade2", Info: "some info", Height: 125}, + expectUpgrade: &upgradetypes.Plan{Name: "Upgrade2", Info: "some info", Height: 125}, }, { filename: "f2-bad-type.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{}, + expectUpgrade: nil, expectErr: "cannot unmarshal number into Go value", }, { filename: "f2-bad-type-2.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{}, + expectUpgrade: nil, expectErr: `unknown field "heigh"`, }, { filename: "f3-empty.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{}, + expectUpgrade: nil, expectErr: "EOF", }, { filename: "f4-empty-obj.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{}, + expectUpgrade: nil, expectErr: "name cannot be empty", }, { filename: "f5-partial-obj-1.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{}, + expectUpgrade: nil, expectErr: "height must be greater than 0", }, { filename: "f5-partial-obj-2.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{}, + expectUpgrade: nil, expectErr: "name cannot be empty: invalid request", }, { filename: "non-existent.json", disableRecase: false, - expectUpgrade: upgradetypes.Plan{}, + expectUpgrade: nil, expectErr: "no such file or directory", }, } @@ -92,13 +92,13 @@ func TestParseUpgradeInfoFile(t *testing.T) { } } -func parseUpgradeInfoFile(filename string, disableRecase bool) (upgradetypes.Plan, error) { +func parseUpgradeInfoFile(filename string, disableRecase bool) (*upgradetypes.Plan, error) { cfg := &Config{ DisableRecase: disableRecase, } bz, err := os.ReadFile(filename) if err != nil { - return upgradetypes.Plan{}, err + return nil, err } return cfg.ParseUpgradeInfo(bz) } From c9fb18f91bb029fd1713621c4ed62d7b0dafee44 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 12:01:40 +0200 Subject: [PATCH 089/115] fix bug --- tools/cosmovisor/cmd/cosmovisor/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/run.go b/tools/cosmovisor/cmd/cosmovisor/run.go index 07effff4ca47..b62d3300da94 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run.go +++ b/tools/cosmovisor/cmd/cosmovisor/run.go @@ -40,7 +40,7 @@ func run(ctx context.Context, cfgPath string, args []string, options ...RunOptio return err } - ctx, _ = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + ctx, _ = signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) shutdownChan := make(chan os.Signal, 1) signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM) // ensure we shutdown if the process is killed and context cancellation doesn't cause an exit on its own From 1aac5d1525cfbe25fd4b5aedf81d0b1e772ecbab Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 12:03:48 +0200 Subject: [PATCH 090/115] refactor process runner to better handle cases where process could have already exited --- tools/cosmovisor/internal/process.go | 70 ++++++++++++++++------------ tools/cosmovisor/internal/runner.go | 5 +- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/tools/cosmovisor/internal/process.go b/tools/cosmovisor/internal/process.go index af923d4a4539..687dfbe88561 100644 --- a/tools/cosmovisor/internal/process.go +++ b/tools/cosmovisor/internal/process.go @@ -2,59 +2,67 @@ package internal import ( "os/exec" - "sync" "syscall" "time" ) type ProcessRunner struct { cmd *exec.Cmd - done chan error - wg *sync.WaitGroup + done chan error // closed exactly once, after Wait returns } -func RunProcess(cmd *exec.Cmd) *ProcessRunner { +func RunProcess(cmd *exec.Cmd) (*ProcessRunner, error) { + // start the process before returning a ProcessRunner + if err := cmd.Start(); err != nil { + return nil, err + } + done := make(chan error, 1) - wg := &sync.WaitGroup{} - wg.Add(1) go func() { - defer wg.Done() - err := cmd.Run() - done <- err + // wait on the process to complete in a separate go routine + done <- cmd.Wait() close(done) }() - return &ProcessRunner{ - cmd: cmd, - done: done, - wg: wg, - } + return &ProcessRunner{cmd: cmd, done: done}, nil } -func (pr *ProcessRunner) Done() chan error { +// Done returns the error that the process returned when it exited. +func (pr *ProcessRunner) Done() <-chan error { return pr.done } -func (pr *ProcessRunner) Shutdown(shutdownGrace time.Duration) error { +// Shutdown attempts to gracefully shut down the process by sending a SIGTERM signal. +// If the process does not exit within the specified grace period, it will be forcefully killed. +// An error will only be returned if there was an error shutting down the process. +// To get the error that the process itself returned, use Done(). +func (pr *ProcessRunner) Shutdown(grace time.Duration) error { + // check if already finished + select { + case <-pr.done: + // already finished, nothing to do + return nil + default: + // not finished yet, proceed with shutdown + } + proc := pr.cmd.Process if proc == nil { - return nil // process already exited + // this should only be true if the process has already exited + <-pr.done // make sure Wait() has returned + return nil } - // TODO make sure we don't kill a process that is not running + + // signal shutdown if err := proc.Signal(syscall.SIGTERM); err != nil { return err } - done := make(chan struct{}) - go func() { - pr.wg.Wait() - close(done) - }() - go func() { - select { - case <-done: - case <-time.After(shutdownGrace): - // TODO handle the error - proc.Kill() - } - }() + + // wait for graceful exit or force-kill after timeout + select { + case <-pr.done: + case <-time.After(grace): + _ = proc.Kill() + <-pr.done + } return nil } diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 2fc2b4f45ff1..09cb228afe26 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -166,7 +166,10 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 } r.logger.Info("Starting process", "path", cmd.Path, "args", cmd.Args) - processRunner := RunProcess(cmd) + processRunner, err := RunProcess(cmd) + if err != nil { + return fmt.Errorf("failed to start process: %w", err) + } defer func() { // always check for the latest block height before shutting down so that we have it in the last known height file _, _ = heightChecker.GetLatestBlockHeight() From 03e7c724f408887ad56cda47ef4a46ca0e8b8498 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 12:21:15 +0200 Subject: [PATCH 091/115] add TODOs --- tests/systemtests/cosmovisor_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index b1d9e3370508..9d276a28d2fb 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -99,6 +99,7 @@ func TestCosmovisorUpgrade(t *testing.T) { require.NoError(t, err) require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) // TODO check current binary + // TODO check logs to make sure upgrade-info.json was readable by the node for store upgrades systest.Sut.AwaitBlockHeight(t, upgrade2Height+1) // TODO check current binary From 7d2d1c642839ac2ae9a6a1fd3f681cb1c8b3cd75 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 12:40:22 +0200 Subject: [PATCH 092/115] move everything to v2 --- tools/cosmovisor/cmd/cosmovisor/add_upgrade.go | 2 +- tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go | 2 +- tools/cosmovisor/cmd/cosmovisor/config.go | 2 +- tools/cosmovisor/cmd/cosmovisor/help.go | 2 +- tools/cosmovisor/cmd/cosmovisor/help_test.go | 2 +- tools/cosmovisor/cmd/cosmovisor/init.go | 2 +- tools/cosmovisor/cmd/cosmovisor/init_test.go | 2 +- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 4 ++-- tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go | 2 +- tools/cosmovisor/cmd/cosmovisor/root.go | 2 +- tools/cosmovisor/cmd/cosmovisor/run.go | 4 ++-- tools/cosmovisor/cmd/cosmovisor/run_config.go | 2 +- tools/cosmovisor/cmd/cosmovisor/version.go | 2 +- tools/cosmovisor/cmd/mock_node/main.go | 2 +- tools/cosmovisor/go.mod | 2 +- tools/cosmovisor/internal/process_test.go | 2 +- tools/cosmovisor/internal/runner.go | 4 ++-- tools/cosmovisor/internal/skip.go | 4 +++- tools/cosmovisor/internal/upgrade.go | 2 +- tools/cosmovisor/internal/upgrade_test.go | 2 +- tools/cosmovisor/internal/upgrader.go | 2 +- 21 files changed, 26 insertions(+), 24 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 08e985df94a7..9ff532104dd7 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go index acb4151e9504..e370eb9a8b47 100644 --- a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/cmd/cosmovisor/config.go b/tools/cosmovisor/cmd/cosmovisor/config.go index c7e6107471a2..cbdeaa2b661f 100644 --- a/tools/cosmovisor/cmd/cosmovisor/config.go +++ b/tools/cosmovisor/cmd/cosmovisor/config.go @@ -5,7 +5,7 @@ import ( "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" ) var configCmd = &cobra.Command{ diff --git a/tools/cosmovisor/cmd/cosmovisor/help.go b/tools/cosmovisor/cmd/cosmovisor/help.go index 6d6df59cef7b..10d8f29b65f9 100644 --- a/tools/cosmovisor/cmd/cosmovisor/help.go +++ b/tools/cosmovisor/cmd/cosmovisor/help.go @@ -3,7 +3,7 @@ package main import ( "fmt" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" ) // GetHelpText creates the help text multi-line string. diff --git a/tools/cosmovisor/cmd/cosmovisor/help_test.go b/tools/cosmovisor/cmd/cosmovisor/help_test.go index 48d7aa148436..b73b48f04c19 100644 --- a/tools/cosmovisor/cmd/cosmovisor/help_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/help_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" ) func TestGetHelpText(t *testing.T) { diff --git a/tools/cosmovisor/cmd/cosmovisor/init.go b/tools/cosmovisor/cmd/cosmovisor/init.go index 8a8dfa6ca7bf..b54ea22aa764 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init.go +++ b/tools/cosmovisor/cmd/cosmovisor/init.go @@ -11,7 +11,7 @@ import ( "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" ) diff --git a/tools/cosmovisor/cmd/cosmovisor/init_test.go b/tools/cosmovisor/cmd/cosmovisor/init_test.go index 35a73348cc39..6862bc076fe4 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/init_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/suite" "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" ) const ( diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index a60f6a587729..38fe3e3aa720 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" - "cosmossdk.io/tools/cosmovisor" - "cosmossdk.io/tools/cosmovisor/internal" + "cosmossdk.io/tools/cosmovisor/v2" + "cosmossdk.io/tools/cosmovisor/v2/internal" ) type MockChainSetup struct { diff --git a/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go index fc6589b77664..00f85f06a387 100644 --- a/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go @@ -12,7 +12,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" - "cosmossdk.io/tools/cosmovisor/internal" + "cosmossdk.io/tools/cosmovisor/v2/internal" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" diff --git a/tools/cosmovisor/cmd/cosmovisor/root.go b/tools/cosmovisor/cmd/cosmovisor/root.go index 59d51b68bbc8..3481af5ce139 100644 --- a/tools/cosmovisor/cmd/cosmovisor/root.go +++ b/tools/cosmovisor/cmd/cosmovisor/root.go @@ -3,7 +3,7 @@ package main import ( "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" ) func NewRootCmd() *cobra.Command { diff --git a/tools/cosmovisor/cmd/cosmovisor/run.go b/tools/cosmovisor/cmd/cosmovisor/run.go index b62d3300da94..a7850881539a 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run.go +++ b/tools/cosmovisor/cmd/cosmovisor/run.go @@ -11,8 +11,8 @@ import ( "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor" - "cosmossdk.io/tools/cosmovisor/internal" + "cosmossdk.io/tools/cosmovisor/v2" + "cosmossdk.io/tools/cosmovisor/v2/internal" ) var runCmd = &cobra.Command{ diff --git a/tools/cosmovisor/cmd/cosmovisor/run_config.go b/tools/cosmovisor/cmd/cosmovisor/run_config.go index 5a0982de87cd..91a1f60bfaf6 100644 --- a/tools/cosmovisor/cmd/cosmovisor/run_config.go +++ b/tools/cosmovisor/cmd/cosmovisor/run_config.go @@ -4,7 +4,7 @@ import ( "io" "os" - "cosmossdk.io/tools/cosmovisor/internal" + "cosmossdk.io/tools/cosmovisor/v2/internal" ) // DefaultRunConfig defintes a default RunConfig that writes to os.Stdout and os.Stderr diff --git a/tools/cosmovisor/cmd/cosmovisor/version.go b/tools/cosmovisor/cmd/cosmovisor/version.go index 021f8ab4832b..501aeb469562 100644 --- a/tools/cosmovisor/cmd/cosmovisor/version.go +++ b/tools/cosmovisor/cmd/cosmovisor/version.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" ) func NewVersionCmd() *cobra.Command { diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index 7d58e0eb9ba5..b50528100bd6 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -18,7 +18,7 @@ import ( "github.com/cosmos/gogoproto/jsonpb" "github.com/spf13/cobra" - "cosmossdk.io/tools/cosmovisor/internal/watchers" + "cosmossdk.io/tools/cosmovisor/v2/internal/watchers" "github.com/cosmos/cosmos-sdk/server" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index 70ef07517370..208c2ed6da39 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -1,4 +1,4 @@ -module cosmossdk.io/tools/cosmovisor +module cosmossdk.io/tools/cosmovisor/v2 go 1.23.5 diff --git a/tools/cosmovisor/internal/process_test.go b/tools/cosmovisor/internal/process_test.go index 99bcdbcf409c..b623dd77c41a 100644 --- a/tools/cosmovisor/internal/process_test.go +++ b/tools/cosmovisor/internal/process_test.go @@ -16,7 +16,7 @@ import ( "cosmossdk.io/log" "github.com/stretchr/testify/require" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 09cb228afe26..217bdd9c68b7 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -9,8 +9,8 @@ import ( "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor" - "cosmossdk.io/tools/cosmovisor/internal/watchers" + "cosmossdk.io/tools/cosmovisor/v2" + "cosmossdk.io/tools/cosmovisor/v2/internal/watchers" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/skip.go b/tools/cosmovisor/internal/skip.go index 881da30f2aa2..5e56728fcf51 100644 --- a/tools/cosmovisor/internal/skip.go +++ b/tools/cosmovisor/internal/skip.go @@ -5,10 +5,12 @@ import ( "strconv" "strings" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) +// TODO do we need this functionality or should it be deleted? + // IsSkipUpgradeHeight checks if pre-upgrade script must be run. // If the height in the upgrade plan matches any of the heights provided in --unsafe-skip-upgrades, the script is not run. func IsSkipUpgradeHeight(args []string, upgradeInfo upgradetypes.Plan) bool { diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrade.go index d9cc30825285..8f0006fc87e1 100644 --- a/tools/cosmovisor/internal/upgrade.go +++ b/tools/cosmovisor/internal/upgrade.go @@ -8,7 +8,7 @@ import ( "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/upgrade_test.go b/tools/cosmovisor/internal/upgrade_test.go index f1b442d315a1..21d5f6e3c30b 100644 --- a/tools/cosmovisor/internal/upgrade_test.go +++ b/tools/cosmovisor/internal/upgrade_test.go @@ -16,7 +16,7 @@ import ( "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index 9dc0610c39ff..f67d2cd394c9 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -11,7 +11,7 @@ import ( "cosmossdk.io/log" "github.com/otiai10/copy" - "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/tools/cosmovisor/v2" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) From 726288ec3b6ef5e7f802354c37fa116ac5d6ef6c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 12:40:44 +0200 Subject: [PATCH 093/115] update CHANGELOG.md --- tools/cosmovisor/CHANGELOG.md | 96 ++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/tools/cosmovisor/CHANGELOG.md b/tools/cosmovisor/CHANGELOG.md index b2c1d038723a..2a8846d5f2cf 100644 --- a/tools/cosmovisor/CHANGELOG.md +++ b/tools/cosmovisor/CHANGELOG.md @@ -36,20 +36,19 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] -### Improvements - -* [#23720](https://github.com/cosmos/cosmos-sdk/pull/23720) Get block height from db after node execution fails +### Breaking Changes -### Bug Fixes - -* [#23683](https://github.com/cosmos/cosmos-sdk/pull/23683) Replace `SigInt` with `SigTerm` to gracefully shutdown the process. +* [#24821](https://github.com/cosmos/cosmos-sdk/pull/24821) Reimplement core Cosmovisor logic for managing and observing + governance and manual upgrades. ## v1.7.1 - 2025-01-12 ### Bug Fixes -* [#23652](https://github.com/cosmos/cosmos-sdk/pull/23652) Fix issue with wrong directory placement when using `prepare-upgrade` for non archive. -* [#23653](https://github.com/cosmos/cosmos-sdk/pull/23653) Remove duplicate binary downloads during auto-download process +* [#23652](https://github.com/cosmos/cosmos-sdk/pull/23652) Fix issue with wrong directory placement when using + `prepare-upgrade` for non archive. +* [#23653](https://github.com/cosmos/cosmos-sdk/pull/23653) Remove duplicate binary downloads during auto-download + process ## v1.7.0 - 2024-11-18 @@ -57,13 +56,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#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 -* [#21932](https://github.com/cosmos/cosmos-sdk/pull/21932) Add `cosmovisor show-upgrade-info` command to display the upgrade-info.json into stdout. +* [#21932](https://github.com/cosmos/cosmos-sdk/pull/21932) Add `cosmovisor show-upgrade-info` command to display the + upgrade-info.json into stdout. ### Improvements * [#21891](https://github.com/cosmos/cosmos-sdk/pull/21891) create `current` symlink as relative * [#21462](https://github.com/cosmos/cosmos-sdk/pull/21462) Pass `stdin` to binary. - + ### Bug Fixes * [#22528](https://github.com/cosmos/cosmos-sdk/pull/22528) Fix premature upgrades on restarting cosmovisor. @@ -74,40 +74,53 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Bump `cosmossdk.io/x/upgrade` to v0.1.4 (including go-getter vulnerability fix) * [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995): - * `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`. - * Provide `--cosmovisor-config` flag with value as args to provide the path to the configuration file in the `run` command. `run --cosmovisor-config (other cmds with flags) ...`. - * Add `--cosmovisor-config` flag to provide `config.toml` path to the configuration file in root command used by `add-upgrade` and `config` subcommands. - * `config command` now displays the configuration from the config file if it is provided. If the config file is not provided, it will display the configuration from the environment variables. + * `init command` writes the configuration to the config file only at the default path + `DAEMON_HOME/cosmovisor/config.toml`. + * Provide `--cosmovisor-config` flag with value as args to provide the path to the configuration file in the `run` + command. `run --cosmovisor-config (other cmds with flags) ...`. + * Add `--cosmovisor-config` flag to provide `config.toml` path to the configuration file in root command used by + `add-upgrade` and `config` subcommands. + * `config command` now displays the configuration from the config file if it is provided. If the config file is not + provided, it will display the configuration from the environment variables. ## Bug Fixes * [#20062](https://github.com/cosmos/cosmos-sdk/pull/20062) Fixed cosmovisor add-upgrade permissions * [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Always parse stdout and stderr * [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Pass right home to command `status` -* [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Fix upgrades applied automatically (check two casing of sync_info) +* [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Fix upgrades applied automatically (check two casing of + sync_info) ## v1.5.0 - 2023-07-17 ## Features -* [#16413](https://github.com/cosmos/cosmos-sdk/issues/16413) Add `cosmovisor add-upgrade` command to manually add an upgrade to cosmovisor. -* [#16573](https://github.com/cosmos/cosmos-sdk/pull/16573) Extend `cosmovisor` configuration with new log format options. -* [#16550](https://github.com/cosmos/cosmos-sdk/pull/16550) Add COSMOVISOR_CUSTOM_PREUPGRADE to cosmovisor to execute custom pre-upgrade scripts (separate from daemon `pre-upgrade` command). -* [#16963](https://github.com/cosmos/cosmos-sdk/pull/16963) Add DAEMON_SHUTDOWN_GRACE to send interrupt and wait before sending kill. -* [#15361](https://github.com/cosmos/cosmos-sdk/pull/15361) Add `cosmovisor config` command to display the configuration used by cosmovisor. +* [#16413](https://github.com/cosmos/cosmos-sdk/issues/16413) Add `cosmovisor add-upgrade` command to manually add an + upgrade to cosmovisor. +* [#16573](https://github.com/cosmos/cosmos-sdk/pull/16573) Extend `cosmovisor` configuration with new log format + options. +* [#16550](https://github.com/cosmos/cosmos-sdk/pull/16550) Add COSMOVISOR_CUSTOM_PREUPGRADE to cosmovisor to execute + custom pre-upgrade scripts (separate from daemon `pre-upgrade` command). +* [#16963](https://github.com/cosmos/cosmos-sdk/pull/16963) Add DAEMON_SHUTDOWN_GRACE to send interrupt and wait before + sending kill. +* [#15361](https://github.com/cosmos/cosmos-sdk/pull/15361) Add `cosmovisor config` command to display the configuration + used by cosmovisor. ## Improvements -* [#16919](https://github.com/cosmos/cosmos-sdk/pull/16919) Add COSMOVISOR_DISABLE_RECASE to cosmovisor to disable automatic case change for plan name. +* [#16919](https://github.com/cosmos/cosmos-sdk/pull/16919) Add COSMOVISOR_DISABLE_RECASE to cosmovisor to disable + automatic case change for plan name. * [#14881](https://github.com/cosmos/cosmos-sdk/pull/14881) Refactor Cosmovisor to use `x/upgrade` validation logic. -* [#14881](https://github.com/cosmos/cosmos-sdk/pull/14881) Refactor Cosmovisor to depend only on the `x/upgrade` module. +* [#14881](https://github.com/cosmos/cosmos-sdk/pull/14881) Refactor Cosmovisor to depend only on the `x/upgrade` + module. * [#15362](https://github.com/cosmos/cosmos-sdk/pull/15362) Allow disabling Cosmovisor logs. ## v1.4.0 - 2022-10-23 ### API Breaking Changes -* [#13603](https://github.com/cosmos/cosmos-sdk/pull/13603) Rename cosmovisor package to `cosmossdk.io/tools/cosmovisor`. +* [#13603](https://github.com/cosmos/cosmos-sdk/pull/13603) Rename cosmovisor package to + `cosmossdk.io/tools/cosmovisor`. ## v1.3.0 - 2022-09-11 @@ -125,9 +138,11 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * [\#12464](https://github.com/cosmos/cosmos-sdk/pull/12464) Create the `cosmovisor init` command. -* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Add a `DAEMON_RESTART_DELAY` for allowing a node operator to define a delay between the node halt (for upgrade) and backup. +* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Add a `DAEMON_RESTART_DELAY` for allowing a node operator + to define a delay between the node halt (for upgrade) and backup. * [\#11823](https://github.com/cosmos/cosmos-sdk/pull/11823) Refactor `cosmovisor` CLI to use `cobra`. -* [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version -o json` returns the cosmovisor version and the result of `simd --output json --long` in one JSON object. +* [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version -o json` returns the cosmovisor version + and the result of `simd --output json --long` in one JSON object. ### Bug Fixes @@ -135,14 +150,17 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### CLI Breaking Changes -* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Remove the possibility to set a time with only a number. `DAEMON_POLL_INTERVAL` env variable now only supports a duration (e.g. `100ms`, `30s`, `20m`). +* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Remove the possibility to set a time with only a number. + `DAEMON_POLL_INTERVAL` env variable now only supports a duration (e.g. `100ms`, `30s`, `20m`). ## v1.1.0 - 2022-02-10 ### Features * [\#10285](https://github.com/cosmos/cosmos-sdk/pull/10316) Added `run` command to run the associated app. -* [\#10649](https://github.com/cosmos/cosmos-sdk/pull/10649) Customize backup directory. Added new env variable: `DAEMON_DATA_BACKUP_DIR`. If it is set, cosmovisor will backup the app data in `DAEMON_DATA_BACKUP_DIR` before running the update. +* [\#10649](https://github.com/cosmos/cosmos-sdk/pull/10649) Customize backup directory. Added new env variable: + `DAEMON_DATA_BACKUP_DIR`. If it is set, cosmovisor will backup the app data in `DAEMON_DATA_BACKUP_DIR` before running + the update. ### Deprecated @@ -150,30 +168,40 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes -* [\#10458](https://github.com/cosmos/cosmos-sdk/pull/10458) Fix version when using 'go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v1.0.0' to install cosmovisor. +* [\#10458](https://github.com/cosmos/cosmos-sdk/pull/10458) Fix version when using 'go install + github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v1.0.0' to install cosmovisor. ## v1.0.0 - 2021-09-30 ### Features -* [\#8590](https://github.com/cosmos/cosmos-sdk/pull/8590) File watcher for cosmovisor. Instead of parsing logs from stdin and stderr, we watch the `/data/upgrade-info.json` file updates using polling mechanism. -* [\#9999](https://github.com/cosmos/cosmos-sdk/pull/10103) Added `version` command that returns the cosmovisor version and the application version. -* [\#9973](https://github.com/cosmos/cosmos-sdk/pull/10056) Added support for pre-upgrade command in Cosmovisor to be called before the binary is upgraded. Added new environmental variable `DAEMON_PREUPGRADE_MAX_RETRIES` that holds the maximum number of times to reattempt pre-upgrade before failing. +* [\#8590](https://github.com/cosmos/cosmos-sdk/pull/8590) File watcher for cosmovisor. Instead of parsing logs from + stdin and stderr, we watch the `/data/upgrade-info.json` file updates using polling mechanism. +* [\#9999](https://github.com/cosmos/cosmos-sdk/pull/10103) Added `version` command that returns the cosmovisor version + and the application version. +* [\#9973](https://github.com/cosmos/cosmos-sdk/pull/10056) Added support for pre-upgrade command in Cosmovisor to be + called before the binary is upgraded. Added new environmental variable `DAEMON_PREUPGRADE_MAX_RETRIES` that holds the + maximum number of times to reattempt pre-upgrade before failing. * [\#10126](https://github.com/cosmos/cosmos-sdk/pull/10229) Added `help`. ### Improvements -* [\#10018](https://github.com/cosmos/cosmos-sdk/pull/10018) Strict boolean argument parsing: cosmovisor will fail if user will not set correctly a boolean variable. Correct values are: "true", "false", "" (not setting) - all case not sensitive. +* [\#10018](https://github.com/cosmos/cosmos-sdk/pull/10018) Strict boolean argument parsing: cosmovisor will fail if + user will not set correctly a boolean variable. Correct values are: "true", "false", "" (not setting) - all case not + sensitive. * [\#10036](https://github.com/cosmos/cosmos-sdk/pull/10036) Improve logs when downloading the binary. * [\#10217](https://github.com/cosmos/cosmos-sdk/pull/10217) Replacing logging to use zerolog. ### CLI Breaking -* [\#10128](https://github.com/cosmos/cosmos-sdk/pull/10128) Change default value of `DAEMON_RESTART_AFTER_UPGRADE` to `true`. +* [\#10128](https://github.com/cosmos/cosmos-sdk/pull/10128) Change default value of `DAEMON_RESTART_AFTER_UPGRADE` to + `true`. ## v0.1.0 - 2021-08-06 -This is the first release and we started this changelog on 2021-07-01. See the [README](https://github.com/cosmos/cosmos-sdk/blob/release/cosmovisor/v0.1.x/cosmovisor/CHANGELOG.md) file for the full list of features. +This is the first release and we started this changelog on 2021-07-01. See +the [README](https://github.com/cosmos/cosmos-sdk/blob/release/cosmovisor/v0.1.x/cosmovisor/CHANGELOG.md) file for the +full list of features. ## Features From b7721c8b689059ac8c4f9aacad175c9de8fe7151 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 12:44:52 +0200 Subject: [PATCH 094/115] go mod tidy --- tools/cosmovisor/go.mod | 16 ++++++++-------- tools/cosmovisor/go.sum | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index 208c2ed6da39..524fe8bd3e67 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -25,15 +25,15 @@ require ( cloud.google.com/go/iam v1.3.1 // indirect cloud.google.com/go/monitoring v1.22.1 // indirect cloud.google.com/go/storage v1.50.0 // indirect - cosmossdk.io/api v1.0.0-alpha.1 // indirect - cosmossdk.io/collections v1.2.1 // indirect - cosmossdk.io/core v1.1.0-alpha.2 // indirect + cosmossdk.io/api v1.0.0-rc.1 // indirect + cosmossdk.io/collections v1.3.1 // indirect + cosmossdk.io/core v1.1.0-rc.1 // indirect cosmossdk.io/depinject v1.2.1 // indirect cosmossdk.io/errors v1.0.2 // indirect cosmossdk.io/math v1.5.3 // indirect cosmossdk.io/schema v1.1.0 // indirect - cosmossdk.io/store v1.3.0-alpha.1 // indirect - cosmossdk.io/x/tx v1.2.0-alpha.1 // indirect + cosmossdk.io/store v1.10.0-rc.2 // indirect + cosmossdk.io/x/tx v1.2.0-rc.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.2 // indirect @@ -60,8 +60,8 @@ require ( github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect github.com/cometbft/cometbft-db v1.0.4 // indirect - github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f // indirect - github.com/cometbft/cometbft/v2 v2.0.0-20250611063609-4e308d824f1f // indirect + github.com/cometbft/cometbft/api v1.1.0-rc1 // indirect + github.com/cometbft/cometbft/v2 v2.0.0-rc1 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.3 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect @@ -150,7 +150,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/tools/cosmovisor/go.sum b/tools/cosmovisor/go.sum index 1c69ae8d38ae..774f28f5b277 100644 --- a/tools/cosmovisor/go.sum +++ b/tools/cosmovisor/go.sum @@ -614,8 +614,8 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= -cosmossdk.io/collections v1.2.1 h1:mAlNMs5vJwkda4TA+k5q/43p24RVAQ/qyDrjANu3BXE= -cosmossdk.io/collections v1.2.1/go.mod h1:PSsEJ/fqny0VPsHLFT6gXDj/2C1tBOTS9eByK0+PBFU= +cosmossdk.io/collections v1.3.1 h1:09e+DUId2brWsNOQ4nrk+bprVmMUaDH9xvtZkeqIjVw= +cosmossdk.io/collections v1.3.1/go.mod h1:ynvkP0r5ruAjbmedE+vQ07MT6OtJ0ZIDKrtJHK7Q/4c= cosmossdk.io/depinject v1.2.1 h1:eD6FxkIjlVaNZT+dXTQuwQTKZrFZ4UrfCq1RKgzyhMw= cosmossdk.io/depinject v1.2.1/go.mod h1:lqQEycz0H2JXqvOgVwTsjEdMI0plswI7p6KX+MVqFOM= cosmossdk.io/errors v1.0.2 h1:wcYiJz08HThbWxd/L4jObeLaLySopyyuUFB5w4AGpCo= @@ -781,10 +781,10 @@ github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1: github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/cometbft/cometbft-db v1.0.4 h1:cezb8yx/ZWcF124wqUtAFjAuDksS1y1yXedvtprUFxs= github.com/cometbft/cometbft-db v1.0.4/go.mod h1:M+BtHAGU2XLrpUxo3Nn1nOCcnVCiLM9yx5OuT0u5SCA= -github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f h1:xVCNN5smIjz+EoEnBpV1+rlqeHFPoSyonelPevBMGW8= -github.com/cometbft/cometbft/api v1.1.0-alpha.1.0.20250611063609-4e308d824f1f/go.mod h1:Ivh6nSCTJPQOyfQo8dgnyu/T88it092sEqSrZSmTQN8= -github.com/cometbft/cometbft/v2 v2.0.0-20250611063609-4e308d824f1f h1:Gy5kTSwgwCMc5S/xjEc75ogN3worBYkTMscpCmVv3uM= -github.com/cometbft/cometbft/v2 v2.0.0-20250611063609-4e308d824f1f/go.mod h1:/ze08eO171CqUqTqAE7FW7ydUJIVkgp6e2svpYvIR3c= +github.com/cometbft/cometbft/api v1.1.0-rc1 h1:NdlXfp4wialMwJ+1ds1DBtfysdxErUxg8/AaqgT0ifQ= +github.com/cometbft/cometbft/api v1.1.0-rc1/go.mod h1:Ivh6nSCTJPQOyfQo8dgnyu/T88it092sEqSrZSmTQN8= +github.com/cometbft/cometbft/v2 v2.0.0-rc1 h1:3QyDHTFzH3a1N6c2jt03kFDCxM/hgUvhzDYBVnPVXY8= +github.com/cometbft/cometbft/v2 v2.0.0-rc1/go.mod h1:/ze08eO171CqUqTqAE7FW7ydUJIVkgp6e2svpYvIR3c= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -1407,8 +1407,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= From 28e2e6851831054d3795855af786ff750f421bf9 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 12:51:20 +0200 Subject: [PATCH 095/115] update CHANGELOG.md's --- tools/cosmovisor/CHANGELOG.md | 3 +-- x/upgrade/CHANGELOG.md | 4 ++++ x/upgrade/keeper/keeper.go | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/cosmovisor/CHANGELOG.md b/tools/cosmovisor/CHANGELOG.md index 2a8846d5f2cf..72feee0eab63 100644 --- a/tools/cosmovisor/CHANGELOG.md +++ b/tools/cosmovisor/CHANGELOG.md @@ -38,8 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Breaking Changes -* [#24821](https://github.com/cosmos/cosmos-sdk/pull/24821) Reimplement core Cosmovisor logic for managing and observing - governance and manual upgrades. +* [#24821](https://github.com/cosmos/cosmos-sdk/pull/24821) Reimplement core Cosmovisor logic for managing and observing governance and manual upgrades. ## v1.7.1 - 2025-01-12 diff --git a/x/upgrade/CHANGELOG.md b/x/upgrade/CHANGELOG.md index 094f8cd2b546..4b47d65b868b 100644 --- a/x/upgrade/CHANGELOG.md +++ b/x/upgrade/CHANGELOG.md @@ -29,6 +29,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#24543](https://github.com/cosmos/cosmos-sdk/issues/24543) Use `telemetry.MetricKeyPreBlocker` metric key instead of `telemetry.MetricKeyBeginBlocker` in `PreBlocker`. * [#24720](https://github.com/cosmos/cosmos-sdk/pull/24720) switch to verbose mode logging when calling upgrading handlers. +* [#24821](https://github.com/cosmos/cosmos-sdk/pull/24821) Add support for executing upgrade logic for manual upgrades via the `SetManualUpgrade` and `GetManualUpgrade` methods in the `Keeper`. + +### Breaking Changes +* [#24821](https://github.com/cosmos/cosmos-sdk/pull/24821) Upgrade plans are now saved to disk using the app's JSON codec rather than encoding/json which is the correct behavior for emitting JSON for protobuf generated types. This will likely result in the height field being rendered as a JSON string rather than an integer. ## [v0.2.0](https://github.com/cosmos/cosmos-sdk/releases/tag/x/upgrade/v0.2.0) - 2025-04-24 diff --git a/x/upgrade/keeper/keeper.go b/x/upgrade/keeper/keeper.go index 8e9aa77bc7ce..1869a40397a4 100644 --- a/x/upgrade/keeper/keeper.go +++ b/x/upgrade/keeper/keeper.go @@ -603,8 +603,10 @@ func (k Keeper) DowngradeVerified() bool { } // SetManualUpgrade sets the manual upgrade plan. -// Currently, this e // If the plan is nil, it clears the existing manual upgrade info. +// This allows manual upgrades to be executed using handlers registered with SetUpgradeHandler. +// Currently, only when manual upgrade can be set. +// It will be applied and cleared when the specified upgrade height has been reached. func (k Keeper) SetManualUpgrade(plan *types.Plan) error { if plan == nil { k.manualUpgradeInfo = nil From 9cd07c092c50ed8e0127d3291aa90137608f265f Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 13:28:32 +0200 Subject: [PATCH 096/115] check cosmovisor symlinks, confirm upgrade info readable --- simapp/upgrades.go | 4 +++- tests/systemtests/cosmovisor_test.go | 26 +++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/simapp/upgrades.go b/simapp/upgrades.go index 0c2328c97678..ab5ff2a09a72 100644 --- a/simapp/upgrades.go +++ b/simapp/upgrades.go @@ -33,7 +33,9 @@ func (app SimApp) RegisterUpgradeHandlers() { if err != nil { panic(err) } - app.Logger().Debug("read upgrade info from disk", "upgrade_info", upgradeInfo) + if upgradeInfo.Name != "" { + app.Logger().Info("read upgrade info from disk", "upgrade_info", upgradeInfo) + } // this allows us to check migration to v0.54.x in the system tests via a manual (non-governance upgrade) if manualUpgrade, ok := os.LookupEnv("SIMAPP_MANUAL_UPGRADE_HEIGHT"); ok { diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 9d276a28d2fb..ed9969406055 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -75,6 +75,8 @@ func TestCosmovisorUpgrade(t *testing.T) { currentBranchBinary, ) + requireCurrentPointsTo(t, "genesis") + systest.Sut.AwaitBlockHeight(t, 21, 60*time.Second) t.Logf("current_height: %d\n", systest.Sut.CurrentHeight()) @@ -94,15 +96,20 @@ func TestCosmovisorUpgrade(t *testing.T) { systest.Sut.AwaitBlockHeight(t, upgrade1Height+1) + requireCurrentPointsTo(t, fmt.Sprintf("upgrades/%s", upgrade1Name)) + // make sure a gov upgrade was triggered regex, err := regexp.Compile(fmt.Sprintf(`UPGRADE %q NEEDED at height: %d: module=x/upgrade`, upgrade1Name, upgrade1Height)) require.NoError(t, err) require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) - // TODO check current binary - // TODO check logs to make sure upgrade-info.json was readable by the node for store upgrades + // make sure the upgrade-info.json was readable by nodes when they restarted + regex, err = regexp.Compile("read upgrade info from disk") + require.NoError(t, err) + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) systest.Sut.AwaitBlockHeight(t, upgrade2Height+1) - // TODO check current binary + + requireCurrentPointsTo(t, fmt.Sprintf("upgrades/%s", upgrade2Name)) // smoke test that new version runs cli = systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) @@ -128,6 +135,7 @@ func TestCosmovisorUpgrade(t *testing.T) { systest.Sut.SetupChain() systest.Sut.StartChainWithCosmovisor(t) + requireCurrentPointsTo(t, "genesis") // we create a wrapper for the current branch binary which sets the // SIMAPP_MANUAL_UPGRADE_HEIGHT which will cause the upgrade to be applied manually @@ -152,9 +160,21 @@ SIMAPP_MANUAL_UPGRADE_HEIGHT="%d" exec %s "$@"`, upgradeHeight, currentBranchBin systest.Sut.AwaitBlockHeight(t, upgradeHeight+1, 60*time.Second) + requireCurrentPointsTo(t, fmt.Sprintf("upgrades/%s", upgradeName)) + // smoke test that new version runs cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") systest.RequireTxSuccess(t, got) }) } + +func requireCurrentPointsTo(t *testing.T, expected string) { + t.Helper() + for i := 0; i < systest.Sut.NodesCount(); i++ { + curSymLink := filepath.Join(systest.Sut.NodeDir(i), "cosmovisor", "current") + resolved, err := os.Readlink(curSymLink) + require.NoError(t, err, "failed to read current symlink for node %d", i) + require.Equal(t, expected, resolved, "current symlink for node %d does not point to expected directory", i) + } +} From 3122f9ec458d53b4dbd49b2b09ed8c31913adb96 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 13:35:17 +0200 Subject: [PATCH 097/115] switch to pointer --- x/upgrade/keeper/keeper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/upgrade/keeper/keeper.go b/x/upgrade/keeper/keeper.go index 1869a40397a4..3a1b20ba4572 100644 --- a/x/upgrade/keeper/keeper.go +++ b/x/upgrade/keeper/keeper.go @@ -607,7 +607,7 @@ func (k Keeper) DowngradeVerified() bool { // This allows manual upgrades to be executed using handlers registered with SetUpgradeHandler. // Currently, only when manual upgrade can be set. // It will be applied and cleared when the specified upgrade height has been reached. -func (k Keeper) SetManualUpgrade(plan *types.Plan) error { +func (k *Keeper) SetManualUpgrade(plan *types.Plan) error { if plan == nil { k.manualUpgradeInfo = nil return nil @@ -621,6 +621,6 @@ func (k Keeper) SetManualUpgrade(plan *types.Plan) error { return nil } -func (k Keeper) GetManualUpgrade() *types.Plan { +func (k *Keeper) GetManualUpgrade() *types.Plan { return k.manualUpgradeInfo } From c0efa1dc0de3c4905f88017d088ea9143471d1f3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 13:44:42 +0200 Subject: [PATCH 098/115] add comments --- simapp/upgrades.go | 1 + systemtests/system.go | 4 ++++ tests/systemtests/cosmovisor_test.go | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/simapp/upgrades.go b/simapp/upgrades.go index ab5ff2a09a72..bbe4f580d9b7 100644 --- a/simapp/upgrades.go +++ b/simapp/upgrades.go @@ -29,6 +29,7 @@ func (app SimApp) RegisterUpgradeHandlers() { }, ) + // we check that we can read the upgrade info from disk, which is necessary for setting store key upgrades upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() if err != nil { panic(err) diff --git a/systemtests/system.go b/systemtests/system.go index e8415b631bb2..25bee3ffe867 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -177,6 +177,7 @@ func (s *SystemUnderTest) StartChain(t *testing.T, xargs ...string) { s.doStartChain(t, false, xargs...) } +// StartChainWithCosmovisor starts the chain wrapping its execution with Cosmovisor. func (s *SystemUnderTest) StartChainWithCosmovisor(t *testing.T, xargs ...string) { s.doStartChain(t, true, xargs...) } @@ -218,6 +219,9 @@ func (s *SystemUnderTest) cosmovisorEnv(t *testing.T, home string) []string { func (s *SystemUnderTest) cosmovisorPath() string { return filepath.Join(WorkDir, "binaries", "cosmovisor") } + +// ExecCosmovisor executes the Cosmovisor binary with the given arguments +// for each node in the network with the home directory set properly for each node. func (s *SystemUnderTest) ExecCosmovisor(t *testing.T, async bool, args ...string) { s.withEachNodeHome(func(i int, home string) { env := s.cosmovisorEnv(t, home) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index ed9969406055..a658c3ce3dbb 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -20,6 +20,14 @@ import ( func TestCosmovisorUpgrade(t *testing.T) { t.Run("gov upgrade, then manual upgrade", func(t *testing.T) { + // this test + // 1. starts a legacy v0.53 chain with Cosmovisor + // 2. submits a gov upgrade proposal to switch to v0.54 + // 3. adds a binary for the gov upgrade + // 4. waits for the upgrade to be applied and checks the symlink + // 5. adds a manual upgrade which doesn't really do anything, + // but we check that the cosmovisor symlink has been updated + // 6. waits for the manual upgrade to be applied and checks the symlink const ( upgrade1Height = 25 upgrade1Name = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go @@ -27,10 +35,6 @@ func TestCosmovisorUpgrade(t *testing.T) { upgrade2Name = "manual1" ) - // Scenario: - // start a legacy chain with some state - // when a chain upgrade proposal is executed - // then the chain upgrades successfully systest.Sut.StopChain() currentBranchBinary := systest.Sut.ExecBinary() @@ -118,6 +122,10 @@ func TestCosmovisorUpgrade(t *testing.T) { }) t.Run("manual upgrade", func(t *testing.T) { + // this test: + // 1. starts a legacy v0.53 chain with Cosmovisor + // 2. adds a manual upgrade to v0.54 which has an environment variable set to manually perform the migration + // 3. waits for the manual upgrade to be applied and checks the symlink const ( upgradeHeight = 10 upgradeName = "v053-to-v054" // must match UpgradeName in simapp/upgrades.go From 53032d4ace5717091544a015541c0967cddb3e31 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 13:46:14 +0200 Subject: [PATCH 099/115] revert CHANGELOG.md reformatting, remove old RELEASE_NOTES.md --- tools/cosmovisor/CHANGELOG.md | 87 ++++++++++--------------------- tools/cosmovisor/RELEASE_NOTES.md | 9 ---- 2 files changed, 28 insertions(+), 68 deletions(-) delete mode 100644 tools/cosmovisor/RELEASE_NOTES.md diff --git a/tools/cosmovisor/CHANGELOG.md b/tools/cosmovisor/CHANGELOG.md index 72feee0eab63..2180a31370fc 100644 --- a/tools/cosmovisor/CHANGELOG.md +++ b/tools/cosmovisor/CHANGELOG.md @@ -44,10 +44,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes -* [#23652](https://github.com/cosmos/cosmos-sdk/pull/23652) Fix issue with wrong directory placement when using - `prepare-upgrade` for non archive. -* [#23653](https://github.com/cosmos/cosmos-sdk/pull/23653) Remove duplicate binary downloads during auto-download - process +* [#23652](https://github.com/cosmos/cosmos-sdk/pull/23652) Fix issue with wrong directory placement when using `prepare-upgrade` for non archive. +* [#23653](https://github.com/cosmos/cosmos-sdk/pull/23653) Remove duplicate binary downloads during auto-download process ## v1.7.0 - 2024-11-18 @@ -55,14 +53,13 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#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 -* [#21932](https://github.com/cosmos/cosmos-sdk/pull/21932) Add `cosmovisor show-upgrade-info` command to display the - upgrade-info.json into stdout. +* [#21932](https://github.com/cosmos/cosmos-sdk/pull/21932) Add `cosmovisor show-upgrade-info` command to display the upgrade-info.json into stdout. ### Improvements * [#21891](https://github.com/cosmos/cosmos-sdk/pull/21891) create `current` symlink as relative * [#21462](https://github.com/cosmos/cosmos-sdk/pull/21462) Pass `stdin` to binary. - + ### Bug Fixes * [#22528](https://github.com/cosmos/cosmos-sdk/pull/22528) Fix premature upgrades on restarting cosmovisor. @@ -73,53 +70,40 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Bump `cosmossdk.io/x/upgrade` to v0.1.4 (including go-getter vulnerability fix) * [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995): - * `init command` writes the configuration to the config file only at the default path - `DAEMON_HOME/cosmovisor/config.toml`. - * Provide `--cosmovisor-config` flag with value as args to provide the path to the configuration file in the `run` - command. `run --cosmovisor-config (other cmds with flags) ...`. - * Add `--cosmovisor-config` flag to provide `config.toml` path to the configuration file in root command used by - `add-upgrade` and `config` subcommands. - * `config command` now displays the configuration from the config file if it is provided. If the config file is not - provided, it will display the configuration from the environment variables. + * `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`. + * Provide `--cosmovisor-config` flag with value as args to provide the path to the configuration file in the `run` command. `run --cosmovisor-config (other cmds with flags) ...`. + * Add `--cosmovisor-config` flag to provide `config.toml` path to the configuration file in root command used by `add-upgrade` and `config` subcommands. + * `config command` now displays the configuration from the config file if it is provided. If the config file is not provided, it will display the configuration from the environment variables. ## Bug Fixes * [#20062](https://github.com/cosmos/cosmos-sdk/pull/20062) Fixed cosmovisor add-upgrade permissions * [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Always parse stdout and stderr * [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Pass right home to command `status` -* [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Fix upgrades applied automatically (check two casing of - sync_info) +* [#20585](https://github.com/cosmos/cosmos-sdk/pull/20585) Fix upgrades applied automatically (check two casing of sync_info) ## v1.5.0 - 2023-07-17 ## Features -* [#16413](https://github.com/cosmos/cosmos-sdk/issues/16413) Add `cosmovisor add-upgrade` command to manually add an - upgrade to cosmovisor. -* [#16573](https://github.com/cosmos/cosmos-sdk/pull/16573) Extend `cosmovisor` configuration with new log format - options. -* [#16550](https://github.com/cosmos/cosmos-sdk/pull/16550) Add COSMOVISOR_CUSTOM_PREUPGRADE to cosmovisor to execute - custom pre-upgrade scripts (separate from daemon `pre-upgrade` command). -* [#16963](https://github.com/cosmos/cosmos-sdk/pull/16963) Add DAEMON_SHUTDOWN_GRACE to send interrupt and wait before - sending kill. -* [#15361](https://github.com/cosmos/cosmos-sdk/pull/15361) Add `cosmovisor config` command to display the configuration - used by cosmovisor. +* [#16413](https://github.com/cosmos/cosmos-sdk/issues/16413) Add `cosmovisor add-upgrade` command to manually add an upgrade to cosmovisor. +* [#16573](https://github.com/cosmos/cosmos-sdk/pull/16573) Extend `cosmovisor` configuration with new log format options. +* [#16550](https://github.com/cosmos/cosmos-sdk/pull/16550) Add COSMOVISOR_CUSTOM_PREUPGRADE to cosmovisor to execute custom pre-upgrade scripts (separate from daemon `pre-upgrade` command). +* [#16963](https://github.com/cosmos/cosmos-sdk/pull/16963) Add DAEMON_SHUTDOWN_GRACE to send interrupt and wait before sending kill. +* [#15361](https://github.com/cosmos/cosmos-sdk/pull/15361) Add `cosmovisor config` command to display the configuration used by cosmovisor. ## Improvements -* [#16919](https://github.com/cosmos/cosmos-sdk/pull/16919) Add COSMOVISOR_DISABLE_RECASE to cosmovisor to disable - automatic case change for plan name. +* [#16919](https://github.com/cosmos/cosmos-sdk/pull/16919) Add COSMOVISOR_DISABLE_RECASE to cosmovisor to disable automatic case change for plan name. * [#14881](https://github.com/cosmos/cosmos-sdk/pull/14881) Refactor Cosmovisor to use `x/upgrade` validation logic. -* [#14881](https://github.com/cosmos/cosmos-sdk/pull/14881) Refactor Cosmovisor to depend only on the `x/upgrade` - module. +* [#14881](https://github.com/cosmos/cosmos-sdk/pull/14881) Refactor Cosmovisor to depend only on the `x/upgrade` module. * [#15362](https://github.com/cosmos/cosmos-sdk/pull/15362) Allow disabling Cosmovisor logs. ## v1.4.0 - 2022-10-23 ### API Breaking Changes -* [#13603](https://github.com/cosmos/cosmos-sdk/pull/13603) Rename cosmovisor package to - `cosmossdk.io/tools/cosmovisor`. +* [#13603](https://github.com/cosmos/cosmos-sdk/pull/13603) Rename cosmovisor package to `cosmossdk.io/tools/cosmovisor`. ## v1.3.0 - 2022-09-11 @@ -137,11 +121,9 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * [\#12464](https://github.com/cosmos/cosmos-sdk/pull/12464) Create the `cosmovisor init` command. -* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Add a `DAEMON_RESTART_DELAY` for allowing a node operator - to define a delay between the node halt (for upgrade) and backup. +* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Add a `DAEMON_RESTART_DELAY` for allowing a node operator to define a delay between the node halt (for upgrade) and backup. * [\#11823](https://github.com/cosmos/cosmos-sdk/pull/11823) Refactor `cosmovisor` CLI to use `cobra`. -* [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version -o json` returns the cosmovisor version - and the result of `simd --output json --long` in one JSON object. +* [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version -o json` returns the cosmovisor version and the result of `simd --output json --long` in one JSON object. ### Bug Fixes @@ -149,17 +131,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### CLI Breaking Changes -* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Remove the possibility to set a time with only a number. - `DAEMON_POLL_INTERVAL` env variable now only supports a duration (e.g. `100ms`, `30s`, `20m`). +* [\#12188](https://github.com/cosmos/cosmos-sdk/pull/12188) Remove the possibility to set a time with only a number. `DAEMON_POLL_INTERVAL` env variable now only supports a duration (e.g. `100ms`, `30s`, `20m`). ## v1.1.0 - 2022-02-10 ### Features * [\#10285](https://github.com/cosmos/cosmos-sdk/pull/10316) Added `run` command to run the associated app. -* [\#10649](https://github.com/cosmos/cosmos-sdk/pull/10649) Customize backup directory. Added new env variable: - `DAEMON_DATA_BACKUP_DIR`. If it is set, cosmovisor will backup the app data in `DAEMON_DATA_BACKUP_DIR` before running - the update. +* [\#10649](https://github.com/cosmos/cosmos-sdk/pull/10649) Customize backup directory. Added new env variable: `DAEMON_DATA_BACKUP_DIR`. If it is set, cosmovisor will backup the app data in `DAEMON_DATA_BACKUP_DIR` before running the update. ### Deprecated @@ -167,40 +146,30 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes -* [\#10458](https://github.com/cosmos/cosmos-sdk/pull/10458) Fix version when using 'go install - github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v1.0.0' to install cosmovisor. +* [\#10458](https://github.com/cosmos/cosmos-sdk/pull/10458) Fix version when using 'go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v1.0.0' to install cosmovisor. ## v1.0.0 - 2021-09-30 ### Features -* [\#8590](https://github.com/cosmos/cosmos-sdk/pull/8590) File watcher for cosmovisor. Instead of parsing logs from - stdin and stderr, we watch the `/data/upgrade-info.json` file updates using polling mechanism. -* [\#9999](https://github.com/cosmos/cosmos-sdk/pull/10103) Added `version` command that returns the cosmovisor version - and the application version. -* [\#9973](https://github.com/cosmos/cosmos-sdk/pull/10056) Added support for pre-upgrade command in Cosmovisor to be - called before the binary is upgraded. Added new environmental variable `DAEMON_PREUPGRADE_MAX_RETRIES` that holds the - maximum number of times to reattempt pre-upgrade before failing. +* [\#8590](https://github.com/cosmos/cosmos-sdk/pull/8590) File watcher for cosmovisor. Instead of parsing logs from stdin and stderr, we watch the `/data/upgrade-info.json` file updates using polling mechanism. +* [\#9999](https://github.com/cosmos/cosmos-sdk/pull/10103) Added `version` command that returns the cosmovisor version and the application version. +* [\#9973](https://github.com/cosmos/cosmos-sdk/pull/10056) Added support for pre-upgrade command in Cosmovisor to be called before the binary is upgraded. Added new environmental variable `DAEMON_PREUPGRADE_MAX_RETRIES` that holds the maximum number of times to reattempt pre-upgrade before failing. * [\#10126](https://github.com/cosmos/cosmos-sdk/pull/10229) Added `help`. ### Improvements -* [\#10018](https://github.com/cosmos/cosmos-sdk/pull/10018) Strict boolean argument parsing: cosmovisor will fail if - user will not set correctly a boolean variable. Correct values are: "true", "false", "" (not setting) - all case not - sensitive. +* [\#10018](https://github.com/cosmos/cosmos-sdk/pull/10018) Strict boolean argument parsing: cosmovisor will fail if user will not set correctly a boolean variable. Correct values are: "true", "false", "" (not setting) - all case not sensitive. * [\#10036](https://github.com/cosmos/cosmos-sdk/pull/10036) Improve logs when downloading the binary. * [\#10217](https://github.com/cosmos/cosmos-sdk/pull/10217) Replacing logging to use zerolog. ### CLI Breaking -* [\#10128](https://github.com/cosmos/cosmos-sdk/pull/10128) Change default value of `DAEMON_RESTART_AFTER_UPGRADE` to - `true`. +* [\#10128](https://github.com/cosmos/cosmos-sdk/pull/10128) Change default value of `DAEMON_RESTART_AFTER_UPGRADE` to `true`. ## v0.1.0 - 2021-08-06 -This is the first release and we started this changelog on 2021-07-01. See -the [README](https://github.com/cosmos/cosmos-sdk/blob/release/cosmovisor/v0.1.x/cosmovisor/CHANGELOG.md) file for the -full list of features. +This is the first release and we started this changelog on 2021-07-01. See the [README](https://github.com/cosmos/cosmos-sdk/blob/release/cosmovisor/v0.1.x/cosmovisor/CHANGELOG.md) file for the full list of features. ## Features diff --git a/tools/cosmovisor/RELEASE_NOTES.md b/tools/cosmovisor/RELEASE_NOTES.md deleted file mode 100644 index e840af68b7cb..000000000000 --- a/tools/cosmovisor/RELEASE_NOTES.md +++ /dev/null @@ -1,9 +0,0 @@ -# Cosmovisor v1.5.0 Release Notes - -See the [CHANGELOG](https://github.com/cosmos/cosmos-sdk/blob/tools/cosmovisor/v1.5.0/tools/cosmovisor/CHANGELOG.md) for details on the changes in v1.5.0. - -## Installation instructions - -```go -go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest -``` From 2b3a5d7576214c325c5dca00280bfdf9958b7c41 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 14:25:36 +0200 Subject: [PATCH 100/115] add more state breakage to manual upgrade --- simapp/upgrades.go | 30 +++++++++++++------- tests/systemtests/cosmovisor_test.go | 42 +++++++++++++++++++--------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/simapp/upgrades.go b/simapp/upgrades.go index bbe4f580d9b7..362a61b076a3 100644 --- a/simapp/upgrades.go +++ b/simapp/upgrades.go @@ -1,11 +1,13 @@ package simapp import ( + "bytes" "context" + "fmt" "os" - "strconv" storetypes "cosmossdk.io/store/types" + "github.com/cosmos/gogoproto/jsonpb" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" @@ -19,6 +21,7 @@ import ( // could look like when an application is migrating from Cosmos SDK version // v0.53.x to v0.54.x. const UpgradeName = "v053-to-v054" +const ManualUpgradeName = "manual1" func (app SimApp) RegisterUpgradeHandlers() { app.UpgradeKeeper.SetUpgradeHandler( @@ -28,6 +31,16 @@ func (app SimApp) RegisterUpgradeHandlers() { return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM) }, ) + // we add another upgrade, to be performed manually which does some small state breakage + app.UpgradeKeeper.SetUpgradeHandler( + ManualUpgradeName, + func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + // do some minimal state breaking update + err := app.GovKeeper.Constitution.Set(ctx, + fmt.Sprintf("we have expected upgrade %q and that's now our constitution", plan.Name)) + return fromVM, err + }, + ) // we check that we can read the upgrade info from disk, which is necessary for setting store key upgrades upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() @@ -38,17 +51,14 @@ func (app SimApp) RegisterUpgradeHandlers() { app.Logger().Info("read upgrade info from disk", "upgrade_info", upgradeInfo) } - // this allows us to check migration to v0.54.x in the system tests via a manual (non-governance upgrade) - if manualUpgrade, ok := os.LookupEnv("SIMAPP_MANUAL_UPGRADE_HEIGHT"); ok { - height, err := strconv.ParseUint(manualUpgrade, 10, 64) + // this allows to test stateful manual upgrades with Cosmovisor + if manualUpgradeVar, ok := os.LookupEnv("SIMAPP_MANUAL_UPGRADE"); ok { + var manualUpgrade upgradetypes.Plan + err := (&jsonpb.Unmarshaler{}).Unmarshal(bytes.NewBufferString(manualUpgradeVar), &manualUpgrade) if err != nil { - panic("invalid SIMAPP_MANUAL_UPGRADE_HEIGHT height: " + err.Error()) - } - upgradeInfo = upgradetypes.Plan{ - Name: UpgradeName, - Height: int64(height), + panic("invalid SIMAPP_MANUAL_UPGRADE: " + err.Error()) } - err = app.UpgradeKeeper.SetManualUpgrade(&upgradeInfo) + err = app.UpgradeKeeper.SetManualUpgrade(&manualUpgrade) if err != nil { panic("failed to set manual upgrade: " + err.Error()) } diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index a658c3ce3dbb..5ec869a3e116 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -10,12 +10,14 @@ import ( "testing" "time" + "github.com/cosmos/gogoproto/jsonpb" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" systest "cosmossdk.io/systemtests" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/address" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) func TestCosmovisorUpgrade(t *testing.T) { @@ -25,8 +27,7 @@ func TestCosmovisorUpgrade(t *testing.T) { // 2. submits a gov upgrade proposal to switch to v0.54 // 3. adds a binary for the gov upgrade // 4. waits for the upgrade to be applied and checks the symlink - // 5. adds a manual upgrade which doesn't really do anything, - // but we check that the cosmovisor symlink has been updated + // 5. adds a manual upgrade which simapp has configured to make a small state breaking update // 6. waits for the manual upgrade to be applied and checks the symlink const ( upgrade1Height = 25 @@ -88,13 +89,16 @@ func TestCosmovisorUpgrade(t *testing.T) { proposalStatus := gjson.Get(raw, "proposal.status").String() require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw) + // we create a wrapper for the current branch binary which sets up the manual upgrade + wrapperPath := createWrapper(t, upgrade2Name, upgrade2Height, currentBranchBinary) + // add manual upgrade systest.Sut.ExecCosmovisor( t, true, "add-upgrade", upgrade2Name, - currentBranchBinary, + wrapperPath, fmt.Sprintf("--upgrade-height=%d", upgrade2Height), ) @@ -145,16 +149,8 @@ func TestCosmovisorUpgrade(t *testing.T) { systest.Sut.StartChainWithCosmovisor(t) requireCurrentPointsTo(t, "genesis") - // we create a wrapper for the current branch binary which sets the - // SIMAPP_MANUAL_UPGRADE_HEIGHT which will cause the upgrade to be applied manually - wrapperTxt := fmt.Sprintf(`#!/usr/bin/env bash -set -e -SIMAPP_MANUAL_UPGRADE_HEIGHT="%d" exec %s "$@"`, upgradeHeight, currentBranchBinary) - wrapperPath := filepath.Join(systest.WorkDir, "testnet", fmt.Sprintf("%s.sh", upgradeName)) - wrapperPath, err := filepath.Abs(wrapperPath) - require.NoError(t, err, "failed to get absolute path for manual upgrade script") - err = os.WriteFile(wrapperPath, []byte(wrapperTxt), 0o755) - require.NoError(t, err, "failed to write manual upgrade script") + // we create a wrapper for the current branch binary which sets up the manual upgrade + wrapperPath := createWrapper(t, upgradeName, upgradeHeight, currentBranchBinary) // schedule manual upgrade to latest version systest.Sut.ExecCosmovisor( @@ -186,3 +182,23 @@ func requireCurrentPointsTo(t *testing.T, expected string) { require.Equal(t, expected, resolved, "current symlink for node %d does not point to expected directory", i) } } + +func createWrapper(t *testing.T, upgradeName string, upgradeHeight int64, binary string) string { + t.Helper() + plan := upgradetypes.Plan{ + Name: upgradeName, + Height: upgradeHeight, + } + str, err := (&jsonpb.Marshaler{}).MarshalToString(&plan) + require.NoError(t, err, "failed to marshal upgrade plan to JSON") + + wrapperTxt := fmt.Sprintf(`#!/usr/bin/env bash +set -e +SIMAPP_MANUAL_UPGRADE='%s' exec %s "$@"`, str, binary) + wrapperPath := filepath.Join(systest.WorkDir, "testnet", fmt.Sprintf("%s.sh", upgradeName)) + wrapperPath, err = filepath.Abs(wrapperPath) + require.NoError(t, err, "failed to get absolute path for manual upgrade script") + err = os.WriteFile(wrapperPath, []byte(wrapperTxt), 0o755) + require.NoError(t, err, "failed to write manual upgrade script") + return wrapperPath +} From 42fb734d6c167a1e67b5cc22eb40d686fe89d260 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 14:39:52 +0200 Subject: [PATCH 101/115] fix x/upgrade tests --- x/upgrade/keeper/keeper.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x/upgrade/keeper/keeper.go b/x/upgrade/keeper/keeper.go index 3a1b20ba4572..9acf209b7142 100644 --- a/x/upgrade/keeper/keeper.go +++ b/x/upgrade/keeper/keeper.go @@ -3,7 +3,6 @@ package keeper import ( "context" "encoding/binary" - "encoding/json" "errors" "fmt" "os" @@ -577,7 +576,7 @@ func (k Keeper) ReadUpgradeInfoFromDisk() (types.Plan, error) { return upgradeInfo, err } - if err := json.Unmarshal(data, &upgradeInfo); err != nil { + if err := k.cdc.UnmarshalJSON(data, &upgradeInfo); err != nil { return upgradeInfo, err } From 1ffada581f1e7065dd48505b6ea84f7361166971 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 14:40:00 +0200 Subject: [PATCH 102/115] fix systemtests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a7183db7f724..a25686247a18 100644 --- a/Makefile +++ b/Makefile @@ -513,7 +513,7 @@ build-system-test-current: build cosmovisor cp tools/cosmovisor/cosmovisor ./tests/systemtests/binaries/ # build-system-test builds the binaries necessary for runnings system tests and places them in the correct locations -build-system-test: build-v53 +build-system-test: build-system-test-current build-v53 mkdir -p ./tests/systemtests/binaries/v0.53 mv $(BUILDDIR)/simdv53 ./tests/systemtests/binaries/v0.53/simd From 1ea885bcbfc4b6ba970bf4bf7461f3a3d517de5d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 15:04:36 +0200 Subject: [PATCH 103/115] lint-fix, go mod tidy, cleanup --- simapp/upgrades.go | 9 ++- systemtests/system.go | 6 +- tests/systemtests/cosmovisor_test.go | 1 + tests/systemtests/go.mod | 2 +- tests/systemtests/upgrade_test.go | 4 -- tools/cosmovisor/args.go | 4 +- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 1 + .../cmd/cosmovisor/batch_upgrade.go | 1 + tools/cosmovisor/cmd/cosmovisor/init.go | 1 - .../cmd/cosmovisor/mockchain_test.go | 1 + tools/cosmovisor/cmd/mock_node/main.go | 3 +- tools/cosmovisor/internal/backoff.go | 3 +- tools/cosmovisor/internal/errors.go | 13 ---- tools/cosmovisor/internal/process_test.go | 5 +- tools/cosmovisor/internal/runner.go | 26 +++---- tools/cosmovisor/internal/skip.go | 1 + tools/cosmovisor/internal/upgrade.go | 2 +- tools/cosmovisor/internal/upgrade_test.go | 1 - tools/cosmovisor/internal/upgrader.go | 3 +- .../internal/watchers/data_watcher.go | 3 +- .../internal/watchers/data_watcher_test.go | 7 +- .../watchers/file_poll_watcher_test.go | 8 +-- .../internal/watchers/fsnotify_watcher.go | 3 +- .../internal/watchers/height_watcher.go | 1 - tools/cosmovisor/manual.go | 2 +- tools/cosmovisor/state_machine.puml | 67 ------------------- x/upgrade/keeper/keeper.go | 3 +- 27 files changed, 57 insertions(+), 124 deletions(-) delete mode 100644 tools/cosmovisor/internal/errors.go delete mode 100644 tools/cosmovisor/state_machine.puml diff --git a/simapp/upgrades.go b/simapp/upgrades.go index 362a61b076a3..b9a733827e1c 100644 --- a/simapp/upgrades.go +++ b/simapp/upgrades.go @@ -6,9 +6,10 @@ import ( "fmt" "os" - storetypes "cosmossdk.io/store/types" "github.com/cosmos/gogoproto/jsonpb" + storetypes "cosmossdk.io/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" @@ -20,8 +21,10 @@ import ( // NOTE: This upgrade defines a reference implementation of what an upgrade // could look like when an application is migrating from Cosmos SDK version // v0.53.x to v0.54.x. -const UpgradeName = "v053-to-v054" -const ManualUpgradeName = "manual1" +const ( + UpgradeName = "v053-to-v054" + ManualUpgradeName = "manual1" +) func (app SimApp) RegisterUpgradeHandlers() { app.UpgradeKeeper.SetUpgradeHandler( diff --git a/systemtests/system.go b/systemtests/system.go index 25bee3ffe867..3ab23f982029 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -174,11 +174,13 @@ func (s *SystemUnderTest) SetupChain() { } func (s *SystemUnderTest) StartChain(t *testing.T, xargs ...string) { + t.Helper() s.doStartChain(t, false, xargs...) } // StartChainWithCosmovisor starts the chain wrapping its execution with Cosmovisor. func (s *SystemUnderTest) StartChainWithCosmovisor(t *testing.T, xargs ...string) { + t.Helper() s.doStartChain(t, true, xargs...) } @@ -208,6 +210,7 @@ func (s *SystemUnderTest) doStartChain(t *testing.T, useCosmovisor bool, xargs . } func (s *SystemUnderTest) cosmovisorEnv(t *testing.T, home string) []string { + t.Helper() absHome, err := filepath.Abs(home) require.NoError(t, err) return []string{ @@ -243,6 +246,7 @@ func (s *SystemUnderTest) ExecCosmovisor(t *testing.T, async bool, args ...strin } func (s *SystemUnderTest) initCosmovisor(t *testing.T) { + t.Helper() binary := locateExecutable(s.execBinary) s.ExecCosmovisor(t, false, "init", binary) } @@ -661,7 +665,7 @@ func (s *SystemUnderTest) startNodesAsync(t *testing.T, useCosmovisor bool, xarg binary = locateExecutable(s.execBinary) } s.Logf("Execute `%s %s`\n", s.execBinary, strings.Join(args, " ")) - cmd := exec.Command( //nolint:gosec // used by tests only + cmd := exec.Command( binary, args..., ) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index 5ec869a3e116..e8aa45b9eaaa 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -15,6 +15,7 @@ import ( "github.com/tidwall/gjson" systest "cosmossdk.io/systemtests" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/address" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" diff --git a/tests/systemtests/go.mod b/tests/systemtests/go.mod index 3a8d0f0b66ae..c6e596f5cb15 100644 --- a/tests/systemtests/go.mod +++ b/tests/systemtests/go.mod @@ -12,6 +12,7 @@ require ( cosmossdk.io/math v1.5.3 cosmossdk.io/systemtests v1.2.1 github.com/cosmos/cosmos-sdk v0.54.0-rc.1 + github.com/cosmos/gogoproto v1.7.0 github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 @@ -53,7 +54,6 @@ require ( github.com/cosmos/cosmos-proto v1.0.0-beta.5 // 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 github.com/cosmos/iavl v1.2.6 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index 9978cb010988..c72c5b00f026 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -17,10 +17,6 @@ import ( "github.com/cosmos/cosmos-sdk/types/address" ) -const ( - testSeed = "scene learn remember glide apple expand quality spawn property shoe lamp carry upset blossom draft reject aim file trash miss script joy only measure" -) - func TestChainUpgrade(t *testing.T) { const ( upgradeHeight int64 = 22 diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index 01ccc634531c..3292a85d413d 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -488,7 +488,7 @@ func (cfg *Config) ParseUpgradeInfo(bz []byte) (*upgradetypes.Plan, error) { return nil, fmt.Errorf("error unmarshalling upgrade info: %w", err) } if err := upgradePlan.ValidateBasic(); err != nil { - return nil, fmt.Errorf("upgrade info failed validation upgrade inof: %w", err) + return nil, fmt.Errorf("upgrade info failed validation upgrade info: %w", err) } if !cfg.DisableRecase { upgradePlan.Name = strings.ToLower(upgradePlan.Name) @@ -515,7 +515,7 @@ func (cfg Config) ReadLastKnownHeight() uint64 { func (cfg Config) WriteLastKnownHeight(height uint64) error { filename := filepath.Join(cfg.UpgradeInfoDir(), LastKnownHeightFile) - return os.WriteFile(filename, []byte(strconv.FormatUint(height, 10)), 0644) + return os.WriteFile(filename, []byte(strconv.FormatUint(height, 10)), 0o644) } // BooleanOption checks and validate env option diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 9ff532104dd7..350d5eb17b48 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor/v2" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go index e370eb9a8b47..99dd6ffa8c15 100644 --- a/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor/v2" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/cmd/cosmovisor/init.go b/tools/cosmovisor/cmd/cosmovisor/init.go index b54ea22aa764..06f6408aa7a7 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init.go +++ b/tools/cosmovisor/cmd/cosmovisor/init.go @@ -10,7 +10,6 @@ import ( "github.com/spf13/cobra" "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor/v2" "github.com/cosmos/cosmos-sdk/x/upgrade/plan" diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 38fe3e3aa720..bce48ca5adae 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -33,6 +33,7 @@ exec mock_node %s "$@" } func (m MockChainSetup) Setup(t *testing.T) (string, string) { + t.Helper() dir, err := os.MkdirTemp("", "mockchain") require.NoError(t, err) t.Cleanup(func() { diff --git a/tools/cosmovisor/cmd/mock_node/main.go b/tools/cosmovisor/cmd/mock_node/main.go index b50528100bd6..b183e2e98076 100644 --- a/tools/cosmovisor/cmd/mock_node/main.go +++ b/tools/cosmovisor/cmd/mock_node/main.go @@ -14,11 +14,12 @@ import ( "syscall" "time" - "cosmossdk.io/log" "github.com/cosmos/gogoproto/jsonpb" "github.com/spf13/cobra" + "cosmossdk.io/log" "cosmossdk.io/tools/cosmovisor/v2/internal/watchers" + "github.com/cosmos/cosmos-sdk/server" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/backoff.go b/tools/cosmovisor/internal/backoff.go index a563ba80dc14..8490023b9f34 100644 --- a/tools/cosmovisor/internal/backoff.go +++ b/tools/cosmovisor/internal/backoff.go @@ -4,8 +4,9 @@ import ( "fmt" "time" - "cosmossdk.io/log" "github.com/cenkalti/backoff/v5" + + "cosmossdk.io/log" ) type RetryBackoffManager struct { diff --git a/tools/cosmovisor/internal/errors.go b/tools/cosmovisor/internal/errors.go deleted file mode 100644 index fd03e41f80a6..000000000000 --- a/tools/cosmovisor/internal/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package internal - -import ( - "fmt" -) - -type ErrRestartNeeded struct{} - -func (e ErrRestartNeeded) Error() string { - return fmt.Sprintf("upgrade needed") -} - -var _ error = ErrRestartNeeded{} diff --git a/tools/cosmovisor/internal/process_test.go b/tools/cosmovisor/internal/process_test.go index b623dd77c41a..a03d771bef58 100644 --- a/tools/cosmovisor/internal/process_test.go +++ b/tools/cosmovisor/internal/process_test.go @@ -13,10 +13,12 @@ import ( "testing" "time" - "cosmossdk.io/log" "github.com/stretchr/testify/require" + "cosmossdk.io/log" + "cosmossdk.io/tools/cosmovisor/v2" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -473,6 +475,7 @@ type buffer struct { } func setupTestLaunchProcessFixture(t *testing.T, testdataDir string, cfg cosmovisor.Config) *launchProcessFixture { + t.Helper() // binaries from testdata/validate directory preppedCfg := prepareConfig( t, diff --git a/tools/cosmovisor/internal/runner.go b/tools/cosmovisor/internal/runner.go index 217bdd9c68b7..8f6305652112 100644 --- a/tools/cosmovisor/internal/runner.go +++ b/tools/cosmovisor/internal/runner.go @@ -11,6 +11,7 @@ import ( "cosmossdk.io/tools/cosmovisor/v2" "cosmossdk.io/tools/cosmovisor/v2/internal/watchers" + "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) @@ -76,11 +77,10 @@ func (r *Runner) Start(ctx context.Context, args []string) error { // Now we actually run the process err = r.RunProcess(ctx, cmd, haltHeight) // There are three types of cases we're checking for here: - // 1. ErrRestartNeeded: this is a custom error that is returned whenever the run loop detects that a restart is needed. + // 1. errRestartNeeded: this is a custom error that is returned whenever the run loop detects that a restart is needed. // 2. errDone: this is a sentinel error that indicates that the cosmovisor process itself should be stopped gracefully. // 3. Any other error or the: this is an unexpected error that should trigger a restart of the process with a backoff strategy. - var restartNeeded ErrRestartNeeded - if ok := errors.As(err, &restartNeeded); ok { + if ok := errors.Is(err, errRestartNeeded); ok { r.logger.Info("Process shutdown complete, restart needed") } else if errors.Is(err, errDone) { r.logger.Info("Shutting down Cosmovisor process gracefully") @@ -140,7 +140,7 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 }) if err != nil { // if fsnotify is not available, we fall back to polling so we don't return an error here - r.logger.Warn("failed to intialize fsnotify, it's probably not available on this platform, using polling only", "error", err) + r.logger.Warn("failed to initialize fsnotify, it's probably not available on this platform, using polling only", "error", err) } // keep the original context for cancellation detection @@ -181,7 +181,7 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 select { // listen to the parent context's cancellation case <-parentCtx.Done(): - r.logger.Info("Parent context cancelled, shutting down") + r.logger.Info("Parent context canceled, shutting down") return errDone case upgradePlan, ok := <-upgradePlanWatcher.Updated(): // TODO check skip upgrade heights?? (although not sure why we need this as the node should not emit an upgrade plan if skip heights is enabled) @@ -191,7 +191,7 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 r.logger.Info("Received upgrade-info.json") if upgradePlan.Name != currentBinaryUpgradeName { // only restart if we have a different upgrade name than the current binary's upgrade name - return ErrRestartNeeded{} + return errRestartNeeded } case manualUpgrades, ok := <-manualUpgradesWatcher.Updated(): if !ok { @@ -201,19 +201,19 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 if haltHeight == 0 && len(manualUpgrades) > 0 { // shutdown, no halt height set r.logger.Info("No halt height set, but manual upgrades found, restarting process") - return ErrRestartNeeded{} + return errRestartNeeded } else { // restart if we need to change the halt height based on the upgrade firstUpgrade := manualUpgrades.FirstUpgrade() if firstUpgrade == nil { // if we have no longer have an upgrade then we need to remove halt height r.logger.Info("No upgrade found, removing halt height") - return ErrRestartNeeded{} + return errRestartNeeded } if uint64(firstUpgrade.Height) < haltHeight { // if we have an earlier halt height then we need to change the halt height r.logger.Info("Earlier manual upgrade found, changing halt height", "current_halt_height", haltHeight, "needed_halt_height", firstUpgrade.Height) - return ErrRestartNeeded{} + return errRestartNeeded } } case err := <-processRunner.Done(): @@ -236,20 +236,20 @@ func (r *Runner) RunProcess(ctx context.Context, cmd *exec.Cmd, haltHeight uint6 if firstUpgrade == nil { // no upgrade found, so we shouldn't have a halt height r.logger.Warn("No upgrade found, but halt height is set, removing halt height. This is unexpected because we didn't receive an update to upgrade-info.json.batch") - return ErrRestartNeeded{} + return errRestartNeeded } if uint64(firstUpgrade.Height) == haltHeight { correctHeightConfirmed = true } else { // we're at the wrong halt height so we need to restart r.logger.Info("We're at a different height expected, so we need to set a different halt height", "current_halt_height", haltHeight, "needed_halt_height", firstUpgrade.Height) - return ErrRestartNeeded{} + return errRestartNeeded } } // signal a restart if we're at or past the halt height if actualHeight >= haltHeight { r.logger.Info("Reached halt height, restarting process for upgrade") - return ErrRestartNeeded{} + return errRestartNeeded } } } @@ -262,3 +262,5 @@ type RunConfig struct { StdOut io.Writer StdErr io.Writer } + +var errRestartNeeded = errors.New("restart needed") diff --git a/tools/cosmovisor/internal/skip.go b/tools/cosmovisor/internal/skip.go index 5e56728fcf51..fe8cc3e203d9 100644 --- a/tools/cosmovisor/internal/skip.go +++ b/tools/cosmovisor/internal/skip.go @@ -6,6 +6,7 @@ import ( "strings" "cosmossdk.io/tools/cosmovisor/v2" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/upgrade.go b/tools/cosmovisor/internal/upgrade.go index 8f0006fc87e1..8318a421187e 100644 --- a/tools/cosmovisor/internal/upgrade.go +++ b/tools/cosmovisor/internal/upgrade.go @@ -7,8 +7,8 @@ import ( "runtime" "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor/v2" + "github.com/cosmos/cosmos-sdk/x/upgrade/plan" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/upgrade_test.go b/tools/cosmovisor/internal/upgrade_test.go index 21d5f6e3c30b..c9f2404ba744 100644 --- a/tools/cosmovisor/internal/upgrade_test.go +++ b/tools/cosmovisor/internal/upgrade_test.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/suite" "cosmossdk.io/log" - "cosmossdk.io/tools/cosmovisor/v2" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" diff --git a/tools/cosmovisor/internal/upgrader.go b/tools/cosmovisor/internal/upgrader.go index f67d2cd394c9..778b120527ba 100644 --- a/tools/cosmovisor/internal/upgrader.go +++ b/tools/cosmovisor/internal/upgrader.go @@ -8,10 +8,11 @@ import ( "path/filepath" "time" - "cosmossdk.io/log" "github.com/otiai10/copy" + "cosmossdk.io/log" "cosmossdk.io/tools/cosmovisor/v2" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) diff --git a/tools/cosmovisor/internal/watchers/data_watcher.go b/tools/cosmovisor/internal/watchers/data_watcher.go index bbfdce9b2675..17be5b4512e1 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher.go +++ b/tools/cosmovisor/internal/watchers/data_watcher.go @@ -8,7 +8,7 @@ type DataWatcher[T any] struct { outChan chan T } -func NewDataWatcher[T any, I any](ctx context.Context, errorHandler ErrorHandler, watcher Watcher[I], unmarshal func(I) (T, error)) *DataWatcher[T] { +func NewDataWatcher[T, I any](ctx context.Context, errorHandler ErrorHandler, watcher Watcher[I], unmarshal func(I) (T, error)) *DataWatcher[T] { outChan := make(chan T, 1) go func() { defer close(outChan) @@ -37,5 +37,4 @@ func NewDataWatcher[T any, I any](ctx context.Context, errorHandler ErrorHandler func (d DataWatcher[T]) Updated() <-chan T { return d.outChan - } diff --git a/tools/cosmovisor/internal/watchers/data_watcher_test.go b/tools/cosmovisor/internal/watchers/data_watcher_test.go index 0a3a2b993adb..addb8f4ec67d 100644 --- a/tools/cosmovisor/internal/watchers/data_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/data_watcher_test.go @@ -8,8 +8,9 @@ import ( "testing" "time" - "cosmossdk.io/log" "github.com/stretchr/testify/require" + + "cosmossdk.io/log" ) type TestData struct { @@ -38,14 +39,14 @@ func TestDataWatcher(t *testing.T) { go func() { // write some dummy data to the file time.Sleep(time.Second) - err = os.WriteFile(filename, []byte("unexpected content - should be ignored"), 0644) + err = os.WriteFile(filename, []byte("unexpected content - should be ignored"), 0o644) require.NoError(t, err) // write the expected content to the file time.Sleep(time.Second) bz, err := json.Marshal(expectedContent) require.NoError(t, err) - err = os.WriteFile(filename, bz, 0644) + err = os.WriteFile(filename, bz, 0o644) require.NoError(t, err) // wait a bit to ensure the watcher has time to pick up the change diff --git a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go index f545e71f3277..a27822b53463 100644 --- a/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go +++ b/tools/cosmovisor/internal/watchers/file_poll_watcher_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - "cosmossdk.io/log" "github.com/stretchr/testify/require" + + "cosmossdk.io/log" ) func TestPollWatcher(t *testing.T) { @@ -23,12 +24,12 @@ func TestPollWatcher(t *testing.T) { go func() { // write some dummy data to the file time.Sleep(time.Second) - err = os.WriteFile(filename, []byte("unexpected content - should be updated later"), 0644) + err = os.WriteFile(filename, []byte("unexpected content - should be updated later"), 0o644) require.NoError(t, err) // write the expected content to the file time.Sleep(time.Second) - err := os.WriteFile(filename, expectedContent, 0644) + err := os.WriteFile(filename, expectedContent, 0o644) require.NoError(t, err) // wait a bit to ensure the watcher has time to pick up the change @@ -60,5 +61,4 @@ func TestPollWatcher(t *testing.T) { // check that all the channels are closed _, open := <-watcher.Updated() require.False(t, open) - } diff --git a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go index 7690d16aab40..cfa10025c680 100644 --- a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go +++ b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go @@ -5,8 +5,9 @@ import ( "fmt" "os" - "cosmossdk.io/log" "github.com/fsnotify/fsnotify" + + "cosmossdk.io/log" ) type FSNotifyWatcher struct { diff --git a/tools/cosmovisor/internal/watchers/height_watcher.go b/tools/cosmovisor/internal/watchers/height_watcher.go index 1dad6c572352..8af8769e8803 100644 --- a/tools/cosmovisor/internal/watchers/height_watcher.go +++ b/tools/cosmovisor/internal/watchers/height_watcher.go @@ -22,7 +22,6 @@ func NewHeightWatcher(errorHandler ErrorHandler, checker HeightChecker, pollInte } watcher.PollWatcher = NewPollWatcher[uint64](errorHandler, func() (uint64, error) { return watcher.ReadNow() - }, pollInterval) return watcher } diff --git a/tools/cosmovisor/manual.go b/tools/cosmovisor/manual.go index f6f68a18f446..d8d0045c533e 100644 --- a/tools/cosmovisor/manual.go +++ b/tools/cosmovisor/manual.go @@ -100,7 +100,7 @@ func (cfg *Config) saveManualUpgrades(manualUpgrades ManualUpgradeBatch) error { return err } - return os.WriteFile(cfg.UpgradeInfoBatchFilePath(), manualUpgradesData, 0644) + return os.WriteFile(cfg.UpgradeInfoBatchFilePath(), manualUpgradesData, 0o644) } func sortUpgrades(upgrades ManualUpgradeBatch) { diff --git a/tools/cosmovisor/state_machine.puml b/tools/cosmovisor/state_machine.puml deleted file mode 100644 index 0907667c9aa2..000000000000 --- a/tools/cosmovisor/state_machine.puml +++ /dev/null @@ -1,67 +0,0 @@ -@startuml - -[*] -> ComputeRunPlan - - -state UpgradeIfNeeded { - state ReadUpgradeInfo { - } - state ReadManualUpgradeBatch { - } - ReadUpgradeInfo --> ReadManualUpgradeBatch - ReadManualUpgradeBatch --> ReadLatestHeight -} - -UpgradeIfNeeded --> ComputeRunPlan - -state ComputeRunPlan { - state ReadManualUpgradeBatch { - } - ReadManualUpgradeBatch --> ReadLatestHeight -} - -ComputeRunPlan --> RunProcess - -state RunProcess { -} - -'ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json -'ReadLatestHeight --> RunWithHaltHeight: have manual-upgrade-batch.json -'ReadLatestHeight --> Error: have unapplied upgrades before current height? -'ReadManualUpgradeBatch --> Run: no manual-upgrade-batch.json -' -''ReadUpgradeInfo --> DoUpgrade: have upgrade-info.json -''ComputeRunPlan --> Run -''ComputeRunPlan --> RunWithHaltHeight -'' -'state DoUpgrade { -' DoBackup --> CustomPreUpgrade -' CustomPreUpgrade --> SwitchToNewBinary -' SwitchToNewBinary --> RunPreUpgrade -' RunPreUpgrade --> ComputeRunPlan -'} -'' -'state Run { -' state ConfirmDesiredHaltHeight { -' } -' ConfirmDesiredHaltHeight --> WatchForSignals: true -' state WatchForSignals { -' } -'} -' -'WatchForSignals --> CheckHaveUpgrade: got upgrade-info.json -' -'state CheckHaveUpgrade { -' CheckSkipUpgradeHeight --> Run -'} -' -'CheckSkipUpgradeHeight --> ShutdownNode -'ConfirmDesiredHaltHeight --> ShutdownNode: false -' -'state ShutdownNode { -' SendShutdownSignal --> WaitForShutdown -'} -' -'WaitForShutdown --> ComputeRunPlan -' -@enduml diff --git a/x/upgrade/keeper/keeper.go b/x/upgrade/keeper/keeper.go index 9acf209b7142..b09f3e85f40d 100644 --- a/x/upgrade/keeper/keeper.go +++ b/x/upgrade/keeper/keeper.go @@ -12,10 +12,9 @@ import ( "github.com/hashicorp/go-metrics" + corestore "cosmossdk.io/core/store" errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" - - corestore "cosmossdk.io/core/store" "cosmossdk.io/store/prefix" storetypes "cosmossdk.io/store/types" From c005143d81f66d1418bde4fd9be641c78f3d70c0 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 17:04:47 +0200 Subject: [PATCH 104/115] check that upgrade handlers are called --- simapp/upgrades.go | 6 +++++- tests/systemtests/cosmovisor_test.go | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/simapp/upgrades.go b/simapp/upgrades.go index b9a733827e1c..a0ed0fb2f214 100644 --- a/simapp/upgrades.go +++ b/simapp/upgrades.go @@ -30,7 +30,9 @@ func (app SimApp) RegisterUpgradeHandlers() { app.UpgradeKeeper.SetUpgradeHandler( UpgradeName, func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { - sdk.UnwrapSDKContext(ctx).Logger().Debug("this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") + logger := sdk.UnwrapSDKContext(ctx).Logger() + logger.Debug("this is a debug level message to test that verbose logging mode has properly been enabled during a chain upgrade") + logger.Debug(fmt.Sprintf("applying upgrade %s", UpgradeName)) return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM) }, ) @@ -38,6 +40,8 @@ func (app SimApp) RegisterUpgradeHandlers() { app.UpgradeKeeper.SetUpgradeHandler( ManualUpgradeName, func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + logger := sdk.UnwrapSDKContext(ctx).Logger() + logger.Debug(fmt.Sprintf("applying upgrade %s", ManualUpgradeName)) // do some minimal state breaking update err := app.GovKeeper.Constitution.Set(ctx, fmt.Sprintf("we have expected upgrade %q and that's now our constitution", plan.Name)) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index e8aa45b9eaaa..eb541656107d 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -115,8 +115,12 @@ func TestCosmovisorUpgrade(t *testing.T) { regex, err = regexp.Compile("read upgrade info from disk") require.NoError(t, err) require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage(regex)) + // make sure we ran the upgrade handler systest.Sut.AwaitBlockHeight(t, upgrade2Height+1) + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage( + regexp.MustCompile(fmt.Sprintf(`applying upgrade %q`, upgrade1Name))), + ) requireCurrentPointsTo(t, fmt.Sprintf("upgrades/%s", upgrade2Name)) @@ -167,6 +171,11 @@ func TestCosmovisorUpgrade(t *testing.T) { requireCurrentPointsTo(t, fmt.Sprintf("upgrades/%s", upgradeName)) + // make sure the upgrade handler was called + require.Equal(t, systest.Sut.NodesCount(), systest.Sut.FindLogMessage( + regexp.MustCompile(fmt.Sprintf(`applying upgrade %s`, upgradeName))), + ) + // smoke test that new version runs cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) got := cli.Run("tx", "protocolpool", "fund-community-pool", "100stake", "--from=node0") From 1cf11cda51e2b75ba967b4aac433be653b9972f8 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 30 Jun 2025 17:18:34 +0200 Subject: [PATCH 105/115] only set env when we're using cosmovisor --- systemtests/system.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/systemtests/system.go b/systemtests/system.go index 3ab23f982029..606d52879d6a 100644 --- a/systemtests/system.go +++ b/systemtests/system.go @@ -670,7 +670,9 @@ func (s *SystemUnderTest) startNodesAsync(t *testing.T, useCosmovisor bool, xarg args..., ) cmd.Dir = WorkDir - cmd.Env = env + if useCosmovisor { + cmd.Env = env + } s.watchLogs(i, cmd) require.NoError(t, cmd.Start(), "node %d", i) s.Logf("Node started: %d\n", cmd.Process.Pid) From 018306802fe7918ccd1fca8f6c026ed489c95c5d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 1 Jul 2025 14:19:08 +0200 Subject: [PATCH 106/115] Update tools/cosmovisor/internal/watchers/fsnotify_watcher.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../internal/watchers/fsnotify_watcher.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go index cfa10025c680..1f04757598e0 100644 --- a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go +++ b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go @@ -28,7 +28,18 @@ func NewFSNotifyWatcher(ctx context.Context, logger log.Logger, dir string, file return nil, fmt.Errorf("failed to watch directory %s: %w", dir, err) } - // TODO check that filenames are in dir & fully qualified + // TODO check that filenames are in dir & fully qualified + // Validate filenames are absolute paths within the watched directory + filenameSet := make(map[string]struct{}) + for _, filename := range filenames { + if !filepath.IsAbs(filename) { + return nil, fmt.Errorf("filename must be absolute path: %s", filename) + } + if !strings.HasPrefix(filename, dir) { + return nil, fmt.Errorf("filename must be within watched directory: %s", filename) + } + filenameSet[filename] = struct{}{} + } filenameSet := make(map[string]struct{}) for _, filename := range filenames { filenameSet[filename] = struct{}{} From 61ad5e9900719759f3279f45ce018870c5222a81 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 1 Jul 2025 14:20:13 +0200 Subject: [PATCH 107/115] fix code suggestion --- .../internal/watchers/fsnotify_watcher.go | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go index 1f04757598e0..e6d419f48527 100644 --- a/tools/cosmovisor/internal/watchers/fsnotify_watcher.go +++ b/tools/cosmovisor/internal/watchers/fsnotify_watcher.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "os" + "path/filepath" + "strings" "github.com/fsnotify/fsnotify" @@ -28,20 +30,15 @@ func NewFSNotifyWatcher(ctx context.Context, logger log.Logger, dir string, file return nil, fmt.Errorf("failed to watch directory %s: %w", dir, err) } - // TODO check that filenames are in dir & fully qualified - // Validate filenames are absolute paths within the watched directory - filenameSet := make(map[string]struct{}) - for _, filename := range filenames { - if !filepath.IsAbs(filename) { - return nil, fmt.Errorf("filename must be absolute path: %s", filename) - } - if !strings.HasPrefix(filename, dir) { - return nil, fmt.Errorf("filename must be within watched directory: %s", filename) - } - filenameSet[filename] = struct{}{} - } + // validate filenames are absolute paths within the watched directory filenameSet := make(map[string]struct{}) for _, filename := range filenames { + if !filepath.IsAbs(filename) { + return nil, fmt.Errorf("filename must be absolute path: %s", filename) + } + if !strings.HasPrefix(filename, dir) { + return nil, fmt.Errorf("filename must be within watched directory: %s", filename) + } filenameSet[filename] = struct{}{} } From f08259a4279949f1cce7b84f8af2e81aee5c43f5 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 11:23:03 +0200 Subject: [PATCH 108/115] fix test isolation --- systemtests/test_runner.go | 28 ++++++++++++++++++++++++++++ tests/systemtests/cosmovisor_test.go | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/systemtests/test_runner.go b/systemtests/test_runner.go index e4b79483a120..b0c8066a203c 100644 --- a/systemtests/test_runner.go +++ b/systemtests/test_runner.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strconv" "strings" "testing" @@ -17,6 +18,9 @@ var ( Sut *SystemUnderTest Verbose bool execBinaryName string + // Store original configuration for ResetSut + originalNodesCount int + originalBlockTime time.Duration ) func RunTests(m *testing.M) { @@ -47,6 +51,11 @@ func RunTests(m *testing.M) { } execBinaryName = *execBinary + // store original configuration for ResetSut, we do this with global variables for now since Sut is global + // and we want the same initial configuration when we do a complete reset + originalNodesCount = *nodesCount + originalBlockTime = *blockTime + Sut = NewSystemUnderTest(*execBinary, Verbose, *nodesCount, *blockTime) Sut.SetupChain() // setup chain and keyring @@ -120,6 +129,25 @@ const ( |_| \__,_|_|_|\___|\__,_|` ) +// ResetSut completely resets Sut by deleting all state and creating a fresh instance of Sut. +func ResetSut(t *testing.T) { + t.Helper() + // stop current instance if it exists + if Sut != nil { + Sut.StopChain() + } + + // delete entire testnet directory to remove all state + err := os.RemoveAll(filepath.Join(WorkDir, "testnet")) + if err != nil { + t.Fatalf("failed to remove testnet directory: %v", err) + } + + // create fresh Sut instance with original configuration + Sut = NewSystemUnderTest(execBinaryName, Verbose, originalNodesCount, originalBlockTime) + Sut.SetupChain() +} + func printResultFlag(ok bool) { if ok { fmt.Println(successFlag) diff --git a/tests/systemtests/cosmovisor_test.go b/tests/systemtests/cosmovisor_test.go index eb541656107d..c5497c223754 100644 --- a/tests/systemtests/cosmovisor_test.go +++ b/tests/systemtests/cosmovisor_test.go @@ -37,7 +37,7 @@ func TestCosmovisorUpgrade(t *testing.T) { upgrade2Name = "manual1" ) - systest.Sut.StopChain() + systest.ResetSut(t) currentBranchBinary := systest.Sut.ExecBinary() @@ -143,7 +143,7 @@ func TestCosmovisorUpgrade(t *testing.T) { // start a legacy chain with some state // when a chain upgrade proposal is executed // then the chain upgrades successfully - systest.Sut.StopChain() + systest.ResetSut(t) currentBranchBinary := systest.Sut.ExecBinary() From 9113c9d059a2fd16ec5fb256ac17265d9224c558 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 15:30:04 +0200 Subject: [PATCH 109/115] isolate all system tests --- tests/systemtests/protocolpool_test.go | 6 +++--- tests/systemtests/staking_test.go | 2 +- tests/systemtests/unordered_tx_test.go | 2 +- tests/systemtests/upgrade_test.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/systemtests/protocolpool_test.go b/tests/systemtests/protocolpool_test.go index 89df4ad8f43a..be6672fc9e9e 100644 --- a/tests/systemtests/protocolpool_test.go +++ b/tests/systemtests/protocolpool_test.go @@ -145,8 +145,8 @@ func TestQueryProtocolPool(t *testing.T) { // delegate tokens to validator // check distribution + systemtests.ResetSut(t) sut := systemtests.Sut - sut.ResetChain(t) // set up gov params so we can pass props quickly modifyGovParams(t) @@ -263,7 +263,7 @@ func TestQueryProtocolPool(t *testing.T) { // - submit prop and vote until passed // Check that funds are distributed and continuous fund is cleaned up once expired func TestContinuousFunds(t *testing.T) { - systemtests.Sut.ResetChain(t) + systemtests.ResetSut(t) cli := systemtests.NewCLIWrapper(t, systemtests.Sut, systemtests.Verbose) // set up gov params so we can pass props quickly @@ -366,7 +366,7 @@ func TestContinuousFunds(t *testing.T) { // // Check that some funds have been distributed and that the fund is canceled. func TestCancelContinuousFunds(t *testing.T) { - systemtests.Sut.ResetChain(t) + systemtests.ResetSut(t) cli := systemtests.NewCLIWrapper(t, systemtests.Sut, systemtests.Verbose) // set up gov params so we can pass props quickly diff --git a/tests/systemtests/staking_test.go b/tests/systemtests/staking_test.go index 4e07bf220fa3..9647ca0efc0c 100644 --- a/tests/systemtests/staking_test.go +++ b/tests/systemtests/staking_test.go @@ -15,8 +15,8 @@ func TestStakeUnstake(t *testing.T) { // Scenario: // delegate tokens to validator // undelegate some tokens + systemtests.ResetSut(t) sut := systemtests.Sut - sut.ResetChain(t) cli := systemtests.NewCLIWrapper(t, sut, systemtests.Verbose) diff --git a/tests/systemtests/unordered_tx_test.go b/tests/systemtests/unordered_tx_test.go index 93df6c5bde46..eab8503d17ea 100644 --- a/tests/systemtests/unordered_tx_test.go +++ b/tests/systemtests/unordered_tx_test.go @@ -20,7 +20,7 @@ func TestUnorderedTXDuplicate(t *testing.T) { // when a new tx with the same unordered nonce is broadcasted, // then the new tx should be rejected. - systest.Sut.ResetChain(t) + systest.ResetSut(t) cli := systest.NewCLIWrapper(t, systest.Sut, systest.Verbose) // add genesis account with some tokens account1Addr := cli.AddKey("account1") diff --git a/tests/systemtests/upgrade_test.go b/tests/systemtests/upgrade_test.go index c72c5b00f026..42bc5774ed11 100644 --- a/tests/systemtests/upgrade_test.go +++ b/tests/systemtests/upgrade_test.go @@ -27,7 +27,7 @@ func TestChainUpgrade(t *testing.T) { // start a legacy chain with some state // when a chain upgrade proposal is executed // then the chain upgrades successfully - systest.Sut.StopChain() + systest.ResetSut(t) currentBranchBinary := systest.Sut.ExecBinary() currentInitializer := systest.Sut.TestnetInitializer() From be003aa7ec70234a24b61e1351d5439cd88055a6 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 15:39:04 +0200 Subject: [PATCH 110/115] go mod tidy --- tools/cosmovisor/go.mod | 3 ++- tools/cosmovisor/go.sum | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index d075c1157e00..0605669e3064 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -184,6 +184,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect @@ -203,7 +204,7 @@ require ( gotest.tools/v3 v3.5.2 // indirect nhooyr.io/websocket v1.8.11 // indirect pgregory.net/rapid v1.2.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.5.0 // indirect ) // Replace all unreleased direct deps upgraded to comet v1 diff --git a/tools/cosmovisor/go.sum b/tools/cosmovisor/go.sum index f002753424d2..cb607477795b 100644 --- a/tools/cosmovisor/go.sum +++ b/tools/cosmovisor/go.sum @@ -1593,6 +1593,10 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -2420,6 +2424,6 @@ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From e2b2fceaf36f03739499404b1b6e309ea2fec292 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 19:02:37 +0200 Subject: [PATCH 111/115] attempt to fix cosmovisor tests --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index bce48ca5adae..824c1c7bb24e 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -23,13 +23,17 @@ type MockChainSetup struct { } func mockNodeWrapper(args string) string { + // Get the current working directory to find the mock_node source + wd, _ := os.Getwd() + mockNodeDir := filepath.Join(wd, "..", "mock_node") return fmt.Sprintf( `#!/usr/bin/env bash set -e echo "$@" -exec mock_node %s "$@" -`, args) +cd %s +exec go run . %s "$@" +`, mockNodeDir, args) } func (m MockChainSetup) Setup(t *testing.T) (string, string) { From 03c5cacd9f5dccee8b5bbbbc193ac54308336085 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 19:55:14 +0200 Subject: [PATCH 112/115] fix comments --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 824c1c7bb24e..fdf8c959bcae 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -164,7 +164,7 @@ func TestMockChain(t *testing.T) { // add a second batch of manual upgrades go addManualUpgrade2() case 3: - // next restart after adding more manual upgrades + // next restart after adding more manual upgrades // ensure that the binary is still the genesis binary require.Contains(t, currentBin, "genesis") case 4: @@ -180,7 +180,7 @@ func TestMockChain(t *testing.T) { // should have upgraded to manual40 require.Contains(t, currentBin, "manual40") case 8: - // should have upgraded to manual40 + // should have upgraded to gov2 require.Contains(t, currentBin, "gov2") // this is the end of our test so we shutdown after a bit here go func() { From 857e8091e5ea518a3d021b1501adf98916981a46 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 19:55:35 +0200 Subject: [PATCH 113/115] fail on unexpected callback count --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index fdf8c959bcae..01527ea67900 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -187,6 +187,8 @@ func TestMockChain(t *testing.T) { time.Sleep(pollInterval * 2) cancel() }() + default: + t.Errorf("Unexpected callback count: %d", callbackCount) } } var wg sync.WaitGroup From a92aba1ee2579a53774fe16372a3e6e3e1abc009 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 22:19:43 +0200 Subject: [PATCH 114/115] attempt to fix cosmovisor tests --- tools/cosmovisor/.gitignore | 1 + .../cmd/cosmovisor/mockchain_test.go | 48 ++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/tools/cosmovisor/.gitignore b/tools/cosmovisor/.gitignore index fe1602e647d3..f9e533602faa 100644 --- a/tools/cosmovisor/.gitignore +++ b/tools/cosmovisor/.gitignore @@ -1 +1,2 @@ /cosmovisor +build/ diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index 01527ea67900..f87c54f96df0 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "os/exec" "path/filepath" "sync" "testing" @@ -15,6 +16,45 @@ import ( "cosmossdk.io/tools/cosmovisor/v2/internal" ) +var mockNodeBinPath string + +func TestMain(m *testing.M) { + // build mock_node binary for tests + if err := buildMockNode(); err != nil { + fmt.Printf("Failed to build mock_node: %v\n", err) + os.Exit(1) + } + + // run tests + os.Exit(m.Run()) +} + +func buildMockNode() error { + wd, err := os.Getwd() + if err != nil { + return err + } + + // create build directory if it doesn't exist + buildDir := filepath.Join(wd, "build") + if err := os.MkdirAll(buildDir, 0o755); err != nil { + return err + } + + mockNodeDir := filepath.Join(wd, "..", "mock_node") + binPath := filepath.Join(buildDir, "mock_node") + + // store the absolute path for use in mockNodeWrapper + mockNodeBinPath, err = filepath.Abs(mockNodeDir) + if err != nil { + return err + } + + cmd := exec.Command("go", "build", "-o", binPath, ".") + cmd.Dir = mockNodeDir + return cmd.Run() +} + type MockChainSetup struct { Genesis string GovUpgrades map[string]string @@ -23,17 +63,13 @@ type MockChainSetup struct { } func mockNodeWrapper(args string) string { - // Get the current working directory to find the mock_node source - wd, _ := os.Getwd() - mockNodeDir := filepath.Join(wd, "..", "mock_node") return fmt.Sprintf( `#!/usr/bin/env bash set -e echo "$@" -cd %s -exec go run . %s "$@" -`, mockNodeDir, args) +exec %s %s "$@" +`, mockNodeBinPath, args) } func (m MockChainSetup) Setup(t *testing.T) (string, string) { From 0fac9ff8b5a8e7477e317753b409449dfd357bcb Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 7 Jul 2025 22:42:47 +0200 Subject: [PATCH 115/115] attempt to fix cosmovisor tests --- tools/cosmovisor/cmd/cosmovisor/mockchain_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go index f87c54f96df0..78e610fa3fd0 100644 --- a/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/mockchain_test.go @@ -45,7 +45,7 @@ func buildMockNode() error { binPath := filepath.Join(buildDir, "mock_node") // store the absolute path for use in mockNodeWrapper - mockNodeBinPath, err = filepath.Abs(mockNodeDir) + mockNodeBinPath, err = filepath.Abs(binPath) if err != nil { return err }

    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�yG9?^ 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 WR%}9=_FYv5AnXF=n;dl0 zrd^CX`rkjU4Z2ol*316)N95m^@B5Jde}6W)R9pA|{>N@{Wh8{rruGWY%uvW&Mab+h^C9)*{~G6J z$Go3Bc`-fh=>F^H&!1dYo#zx36gqO&iJnta3z!Isj*i}XNR^45J)SMz>agL@ikI^C zBb7=cV_9<={QUeh>7Q?2`Tpu?v{F2eXrp<-q14eDLx+)VG=@X1MUy}8vKN+&it2QI zi=DbHE-*r5#3=b}>4jX;YpZy8-43CKDS|K`q)!f`1w;diBxZ{x8+cPc` z1zP9i<)3|xknOt9+fcgBWju$iUN(xqnHlR@Ur=g5Gad_MKP-mIDXM zzQw9lmY46AJS{0%c5B0)#4FWDoTh3U6^|Xe_Ch9zMRN7Zm8I8~tr>1`=(}9~Mt5@f ztE^S6(K{>FTXtB=kGD6~q*z4UxpU_t?FpY;>$d8PdU^&}tI3%@mw2)>sXcFN?Je(T zKKAko3lg+3z;}?v=XMyqB3T~DZ!zrYGA&;4UGdUGvsR?Opt;`E_-bI{7z^CY7;S z4K8y|!b63e_TxX?txAN)PI-O4zdgd8!Tyx!WSQr1yvvMKmAZwrPusxI&{q$7!9wa+ zGoPM$ibRFXO}E+4&5V&||9|r(olRM{%%l?XMw`kqUWgdbO<1 zo_CFk$B*CCbe?FDIxu&ws^evQ`y~sH5}Tf+s{E1?*$o>u_zT&+$+4N8olWgc?P8MT ztuwv8eC@W1SVN6Me_w9HCw8%GI(ie#%*^Ee`|rWEYuC6gm4-5CJY!$Gjq8-G=uD!z zhK7QcmWhZGBO~L?=({ZMPZ#8Lbac|%diwg}aW&!LQgL%DE2|S7J;S4u9an-*_q5HN z)=FbwU}&8j?(c6O9a)IIQo+CVLX1)B%$ajoVxL^)gIk@((i$_)Y#9mm6q!85o;e=I z;L!bArf(o?wkyltv0=|ikMk=)05$ii zjEoGy9)Yf&oI6n)nK!v#oU*Nd%XSq zqn50oj!aLl(=J3PT2*P-X!M?Ck9=1pd$KDr!7j`H8{5K-+biXY9{N*thy5>qePPv= z-0?L+%1b7Y?`f5$gS6;Wb;)qqNyY=5obq^xNL4kpB)y_-oQ{{3w6x-R8*IhH!^7`* zdR8H!_Pe;aq$DSQx$xR@%Gck2TdZb=s)~xro!hs|crypqBEzgzzDluYFx~wWhJcCu zI<-y6_EoUUtev})w6rwBF5sZ}-c+7O*&>`uuIt$Ov&d%Z9w7&fM zdX2j@?AEkdPE*#Oa&rw4`Q&4W65W{}>s;FVC-2|CubQBD|HzReTsohrih=}{8X6kt zH6vnUrHzb?%x!F{f@~TNNUEk;$EvETHuvr!}tuva-XAZ@2{_TC)kL(Nn+Q-5jV)TQPxXcM$&wDecjF&B4+Wy0x@mspxkY%IOSO;3* zX=&+7-@`ggl0$7JHtne_lE;r9M^XEVmc3VUo$&C3;vm5Yl#XW_Ya1tuZ8l@G$;I#7 zyH`OP1u%ybN)EkdF3P;5q$G9QO2kr{W^Jr$(rLg4c~tY(LceF@JC@8WXW`V|mW zlb?Ar%Z$4sXX9FRyT89CJ6lszGd$T9-!)RDRYiKZl9rQ#7aQLS;j(J(=W!uA%F4dg z52tWft-ep5Y_+qqyPW5>m&WmgHCmG@(wLf2B<9{dD#>!@5XZ1(Yqk=+by`YFUR(Pq z)8(1Tk<^iIYDKSuV&~{M_0&xd#Um$J6=NO@>)skg0=e@T*F|>4XK4})R8swjzbkLD zqeCF0G+oQ+?|$YKRsSi!lpS^s4pLrE*fdxq0rMU)pevHBSGI1t-2J4rrorC+uo(M z`WtShp@{%q{8Ck=r1s8OL0tU$w-}XYjnl2X)VU(#P1i{=oShxZ>S7k1kn|~OLy36A zVBfxLXz0!s*5Q!3iIB-(jC1X;=Eetp)uh`ckV0wEc<5bS7^i;mekP{pUx(6?6Z?h+ zDxCs&jLtebrZZhG2^Ky=CBAm;T8(c;G+Vd65S{+9u4i-(eeQ5bRz-$g%6D#UUZB#U znMk>3h5S9z8XCWjl?=QHFeaa$oo*BD3U=sy$doZRJEfX#`}%pW@SZe>z6>Rk%!wBb z&QnIh2xIEiyEn!w$tcjS5{_brkY`OZf-=q1ud^} zV2=9O#p2X7=hPubhfV{v-^jY1{x7x<4Vf@#uZnxN-pIfpipQwtxv<0NrO)^1(9zFI zO5WajP`R{6$K1kVucWNdz>RCyucv9q1J*IUjEXvq`l!eIb!gf0<@vd}Cm%k1h+R7^ zE2|_Wb+@i=hl>#X_U){a`FVLA{gc*cQ}#neyxx9(9i@Ie`}d!sqobPyc#f@3(3kRh z^X3gx(z@f;Th8^y0oyzEzh6P16Sql&(D9Qeqmz@9)pRt})di>w?CtFZ9D9q?)w})M zZ>6QBp{J!U$g$7cY8X zj$r=z>le3q^C?uaE~H8Ml669%lauzxB_#On^<0{%6@r@#*zB#rytxgdKl=* z>-o&o*gL^F6dHjHYg^mo7qTI2k~J9)HW9f5KvY&LcvaNYuyhoJwoPqv|{3pl@*+hQ^GJ{a2azEk7pN#k1F&}=&vZ2NQ7Oyk^4h^Ets zLcL9+6aDhz?3JI2rEI=QxReF8B|A=-lZOTVzmApOynVZ&NIL!rcyosR%dlR zH*LOj&DB6UfHO0_Hd*qZO{DY|1@IP>l*HHOe*E+)4-YzrJB@8^)w8I>&ZD51SlP`q z;sajKBZBQu>lX*6oXg3o-O;zu254`tNk%{ z=+Ggr=jbKwGo!z{Iy(!UvL-9z6OC$lwrtsw2bfn`S(#s0__#<9z~k!GtCLv5==wAp zLsTyNsh)QuScDGnr^@nhskE5O$WT(mNMorrPLn2Or@`UWsap(o%^OnY@m?XvBslA7Fo6K`P=nbx1shAw;;`qIx||Un@~MGJyas)1tyq? zXdwwiJ{b|>9EB#UB07;LYl-x=u@Bm?=S1Yomy!XzCQr0!2|O(S`c*+gBet{CAVIJv z)vD`aLUp2U!B%UKgDkgGf=7)6(ke zXK|wIzH<5U;hb=K;eg=eQf}}X%lB=U-XWQ}xwruB)#c>mT4#HOo()L{2`K1%zE_r1 zr5TM5VKIj+*s@{6)9ohdaz~FI?fGn#IGdLCgq4MbDep&#TalhT>Ji0spaRqn-s#Ce-JTYT{bZigIuSjMd=Ok;RJ_-_s9q`nI2i=3a0!aD{Zzdqk@eH|Mc`&q|RSdWTcpu!ekcYh13BCSoml}}k|>3N1nK0f+4+}ySyVm@ol zO)C8YHso=mrZ>28;|6Vs=v=yXLiLWgxjvV#P7rXLaDIfeyup?UJc7)vLu;X_N zr|x)o@Qn7>*LGNSmt3ezHgjXz!^Kr$J2O&se+7eW_DU)$E(Vurvu80eh01TQ`2G0# z^8&djKwHRtvPh<8W{2m0;ra8A+Hs&@`d1n=>y_VJyseT8gk#`{hK zKW{VNL-nE#kv>T%)SlhDKWJ;&#FtfCOh=)xZkq$KuIcIPq_b)UspLT|thAnj;=#Br zS~1q(T~_pn%hRNE<9dNT0s=J;E;>Ri;vF0uyoY*OMGJ7ZD{s8a)A^#~z$Y4-z=XRR zqQV*xZe_D2AH~$th*)EW`e>ahvQYL*yGB*0p82@2)BkP*O>a~XBoLwqQbU?!ZC|FQ zs-E;@s1%(YSm!iatzR$jIW8vV%*~rO!5@9zy&F4TX*oH3xuvz0g^y1e@jW;)($k;p z=C)i{U!PS(L=%5{GEFXDepsZJ)-F)2boOjmf_@3hu3bj~b7|5&v}pzGha*qi-xhxK z`tkz+PH4f@xq-qCii;L4YMlTC-9k%io0nw$oO!7H(M0Bh-op3b15z`6bF=Y)Txm`X zLsnz_2-E{dj-1_kH#}#^a*R&l+_^6~InU}-ry*N4x3%e8GxgqH6Wg+;qlkYh>aI#e z#)N)+R%|`6hdhefWSi)mo!r=heCW!MkjkI-N9@f0#P2+vOqu`!24La9{{3I3*>L*~ zqum|f4!5nLrdBjH?cZCug@GZizrTMCLmRl*^XJcHvTST@OlS^fDns?SbL&?7sMW!( zcRzbFnmBvhzi-5A++O;WI%J9^@}QNAroBKTO?iMtsb8;1?vEUI8@u3Mk!L`EmdT=A zT*KFN1TCH>)p<7rC&Z`vTGa#$i9Gm28&lo?^{=OppU`lkkcl10&W*c-I9T9fECUnwdo(W@Wj&^L(FemeS_XQ&{;yE7-=nqRdjqI@QsR<{^HZ9~dX*n|-A=-1_YeAqj)mHqg%W{a(x|zNAcw5iE ze94L-rxxwv%#RieF-pr3!&V&kJ6wlWDsfR^dah_g+q!$?TMOrt|lXDsxha?S*jTJRCf|y=DeR>qIa30BnLGlGgCnYJM zHT1-p&GHu$xdpPL^^~E30kDz%WnSxG5a)tVEgxna1O(VPI7)vcK$V@InV}!Gv$g$_ zo13d0;^X6!o{>Rc>@aIa-?I>tw2^9*n^i|_g_pWC|bw;iw&t+>+I_UqT4b?epvF8eo5 zO-|tn}X0;Ux}97g8XQ=2Z_|M2p=+O%#7cmHf0rM;y4eI?|k z%Brg9%uG#OQZG9hwI(SkDJni*393)pukJo98lE z8$W8Cg$kmlT*+`H$!57o)}B|wA6fX9U!R$oY3b~&i8X7lnFxyL(e4-$FUoiw|MsmS zK8sB}B3^;VlDDu(sHRI}H8wUL92(-Rausg6vQBs%L`F1vNWBlx<)){ncW5qIym-eb zSlwuT!os60@$mkeaTMBD{{H?%o(6JIP*REpp}^+1W)3wfDJbxFV{19Zzl=6rKG_{~ z-O16h&^1DMY3HwB&s<$8AdZHmp-0KJJUu%TG3Cat)ak)94M$H+Q6$);qPeSU*> z+KU$_$c?{zd04yz1zE4n*nzh%DvE`hmp2*@1*ubSJmt-sv$(E+O>ZF3l)iQQEX&L8c9YEUmom&9L{xpV166Ui~WOmlu336{h#^yr%^ch(rd&Yx2?F> zb5&<56VXdu(uGZjOlYwk${*d6L=*4|A_7+Hegip$l)H`V(gQjn2K+B@Dcg|zqChBj zMO0(8OA1HhevM{Pna+w#IZx?N*&r@^ihB^R%YAxwwZ=R5c4uU;^3C5)yUo84cg?Is zqG^9me%3#rGzQV=tzbxee0;TcB?H&n75A_w2-@@RPNQSjSFGa^%t}p3;r}5?dBsVU z^P|K;sV{31l{%$b#+_2fbFeDyQ`_kl+K-t#6r~DLB%bKi=@x69tm=Hci+^zL(D}^NvTRVG6kP$ zX=xE~7>$`I348PAh`U8{N{Zk6mZ#$GEPx?Yhuv@7;B%y2yH-I=%=J<4R`STMtPKCk zRGc0#G`gL|N;)5J@0W>*i5la{=(PB0Pw@t9Ao(Ux&iC)%ucoHvN%OBs3v}jo?QUyp zyOFlcGzmnH+9l;@p&w^!TY33e%z^x$wnDu=h9#%|B`hRVXqq7WWd}&Tc8*(;!^psZ zS-Hl1lPhK4@OPMYe&_4zD|)nj!yRAsDDepd@KgUn;O)lJRo*>f} z<@Jlj8ZQ|e^W+qQ@tC!jUzu}kEu`B*Lld2tc#bEaXh&CDTb1@LzyCPN(>Adj|2vd$ zosE0_T28+tSON5$PC{gEX5z;$z=FkAllw zS!F@UGRJ?e2@-Kqg?<#2Im%VBgO2VcFf__1-#{<95+(WKUul41`h=9pkBY$Tm$Q4Y zJk*OW0^6d1vFzS`?8lEE8ky_Xta)_z?%hp&8!6|N@O!$U6L@|W78dh%dq%9huP^OQ zcXw9&`r*TesY9MUdp6M~M6MGmy611`h?zu3MwUW`r{{5K|30g#t{wwEZgDRlAOM`5 zVEb3EI5C>m%09(loVbQR($4{IP=w7#WEv6`D#YzYUgPS`X!5%e$z|Gi^J_ea1i^;oR*xpU{9fB0~0%hs*_@AVvfp(kK$ zW#si7Rm=B%OT00j-UnavsBeDiYr5>;HEG9@G(``-O=vd*pe1mcOH1FkOGgXcy=M^c8F@1y;H3@FK9@e6=ZSF)|Kg@tht^FdL?jjhE?EyzQ7sfmy`3qcp<)Cy4D8yq zOK;rvx!Jd2FJv}|m zX^3%}XpDfEWrNSj$ar8b`2xhs?`SL1gwAajl%M;whr=|OTfcu-#u5c)J)YkyC5FF1 z(pAa=1zaZQ3*;dP29KFiu*IMWnBkqXv`kE?7Ck8;@#Pal9UJt5K%~d}r)!2Lv|G_% zCChBvtL~H!=o zQ~^mZ005Bb*7af{qQ0n82XN2KjmVqi^2bk}2v};1Sa}^oj3M$KP{>RHUjNG=k$_3u_S z8Jth3M%r9*dsxy4CL0HoBk2Xf5+jv3UteEqwJTR*&|`xfnejoWd&Y_gi6fkx-VYyY zXX@$e2L$uZDE;f&eQ_ zE{kA9XY+cFf|DRPsHEk-jPvb7>}=e)k;{47RyvSR z?$JK!a!kg?3slL~OfUaM7unv=Q&LpO)ryL^-;wKpb)sa>rzr%~{m9X0Cp~!t_tc8# zq1VY|LK1VGefH)}d3UBGE=MnHe-6X9b5H`n=rADSf}St!&y`1YcpaliU9~R371`eZ z>@CB*z|we<{DK0D?-+7EM#fm5q9#!63#v_P{9(`g_rsU$#HaS6Y91CZRE$=@KNt~# zqJ3?8f1Mk%v%SpBtcMTFty;CpU%)B~vi$)ygV3c+20Rw(2f^|P_8kHo zvVnXLvm{8xZ8G>DL%~@qt0cNpZ%yDBA^U~F1QST{DactJ9FK*7%a|KUcSKoahazr>!CnPpD4^;t*KcOCvq7mW6cmejlVo}6Q*rMH4?^)& zED&grl-mTYW5|^B?x&y4g1FgiF$HDiNT;dM%Xqk&1bxpK#ptN$=$0Qpo}$_aSf2NQ z_Jh)vAVq@1wfJu$bG(AkxnEFF4QmM@_?)%%TXNxmfM~Q^%w18hkDQQV>285)zMLO5a7ydOV)4)|g~))A5)%usmEtAALf>+H+$@Z%)A zu!1K}xJ}q2m6P7SB|eJRuV0^rRRs?>Apkp23YtusBR(Fg^`)TTE4Uv#CBy^yxboT4PO_XdnRl5QYDI(_iwJo#G!5fRGGF8qo8bOQ25>Ne&N3#yNl=SiJ)X zO(?mXS~?dlG`F_4b`K)OU0F|~1QJOH&qqujki_I5ia>Urw{hV4;2HAgG4dyQpg}5* zD=XvQWcR_G_;&TKYJ(*IzU|w$LqdE3tn1E}M>_7AGw7v038d0fmOuaYjU+Vdp+iO8 z*@1q3q>bXH{OWy>0p7m8?cL_2;;7sGt?}(n;7tOKV-j|H#qXKY@oq@A59vdlV57L0TSp z07lVZS0ZK*zkPpdPbMrS|^IIAz`Dot( zKECe@&9pri7AC9yrlUP|DKKSRuE9JTQqE@ZL)b75aC0m0)>}T>CmVbaC2S!Wh7bM8 z+w96v^m`<&o=-CdNF6ADEbQz)OlbL+;oI})(7r}26{u_m!|i1RpqRIiXN$8$GnyLf z;}vA@`~F%S8@qbdsxla}B2*IeI^a=Ix^N*ZL5hr1OQ-&>;u2{X;HY=-g(3yp(YZ+w z0wfb_A@F#D_#gtV(9|5%BB6j(PgCzpU^+CKadB~q5)wCv^3HG3+D=C5mo7yve7$MM z->+AC1DeG7_=<}#P&3nkg9pzLoP&8`k?A1p8zeWIqey@?%_X?|@R~HlV0KAD8BCr$ zDV=5}Or7&x<1G2L;T?bSvFUVaO-lj|~P#sQ$hGdREJ0j-t z71#gr5`;el zzma-?DR(j3c6so&$ zBy~3Q2m-0-&NQI@qx+$VMrCJ<5%k8-uY%noJ|sY~iKNfuqop@Le?HOk{dEo%J0mU# zJ(4^-9l($79Z0$hDP)@T_X5M@_c7Z1PPI=BFz4dMFM0_FcJ36z#(jHyK)%p*5Y~*} z%~E{;&X*+2F*(ygd}D|N*jtyP`s(~u%r0wH`iFz zTUCMEp2P{i0YA82V_@*S& zAIG3Y2`=p0>A!CIC0ZEBh|En}5L*#30RquS0vr&3)Q20Z2z zDU-Lse+=PdgMISRTa(5(v^nVj9%*vp#43a;%%3KK`}!w!xn2ziDn+X=YYTBP&V2IZ z33`zYk#JYX!c!SK@J_sNWd*q1R*kAWRk144K)8>A76>GD4t z+#Uk{M(P`9V7g8&H3XL5;9ZY{A59Auk$FCxhYwLGr@SDs5ZmtM9JjS3wJ^qrs;D@3 zNcoi*=DY8pp#{zdscFieAaxxAE@TlDtbJ-Yh`AC(^C);ZIp<@F7&S4PDQ26Od|ZuR zLY^csg)DG_#fbPPi2vl~8k$ZI!QN72_d*Jk`uts@K|!asA3BOP8ltEGJd*{FRY^Dj`_-yZ~O`Bg_{H?b>Ey-cbm?Q*bj|Z?&0Gc2IUw{jCkuXBF z#K5X=fjv=7Qvou`5rpgOM2P1q6m){n72;BOa!@4i(x?|duy(m^AO%Zn7IG}4-n4;7AGS#G> z9F!wSqJZSTJye6zMtC8`rF_`9MfERUywuUb1C^H>CqTkqykNmf$w$a|W9XL^9rF8=<5xLmVF9YR%bxkY z10xuYR~K=yB7bh;F(yLR?`_0=D1Zkq+#cJ<&erbL6h1`HIWx1@!A_$pm}3KICTfu!15_`$q{dJ4vQj8AgRv$~ zQb?r99ut?Y?z8yXO`w?v@cSg>v4PPTx1hTdTRSY*X`2}sdb>9gY$eF5ts5HhyE`ST zL!u}=|EAKQ66m-B_~&n?A|MOu>+7pqNO!4o zfJ0(RB~vxx*$00nWDku7v+D)22iaE__pX}doYCMkrr`wR4`jyFiPnImi+LOC?Ym;9 z=f@~S01C~zY+z81i5n4ypcLr)kZhnNA8T%DxeRK7utB_hg_2b9?b{2;Na4?)?+2bD ztyswJ$Xo_#An2YiFoEr=imw7m8y?7IBsKb7y>-A7cC9#gs~(0BRu%ep;FVnd{R$6i zk(i)$cPR=T2?h-B$XOHl7iZ+an+aEkQ+PjIAAk18`8md0CRsCYN$3F@5Nf1~xC1tQ z0YO19jXV^U(4Ss5nMMF!b;3!4U$Her4}c5%3(a~N9sQM5N&r=y?I0^gQWP#tLi};! z2z>-c$^5jw62eNMu&H2>CNeWDPwd%!S#z1cO+VxkqqQb&>?g7OO=th~8Pm5I)str+ zP#+vR$)-;etc?|1fNEa^cH;2ivU!ltEQkpxuqL=_Gs%@~iUMRZ;&CKrw_vIwqiABf z!9gc^m~|JP!~3^~MLiap9*HL5{_G`DadF?!!(Nc%6%S7`l=V4}r5Ok50IS${zs{|w ztBb*W{%%}2l;hywU?-6m>=nR_z_<2rJZ|2$ElRLASv5A$SerVxj`xGsKKkCn-<4o5yli^<77B37dJO~SC?KQLSFK6Yp87A((p?&E~uE1v=TB%W8y)RpJSf$QAl6!if_JPrrXOdrPc1wnj zOwRnMnz52i>(NUO_H@rpj<*Q1(5&hZ5_(~g72j2t>7=IWyJP$IyGR;wNy$|(=Rb)H ztb?n!y`v*fkOkf)r)K4tGZGRKgBxjR+#y%!Ah=G+%AR~@T+e~xO-0K=!AYM%4~$YI zBpBoQCY+y1k$;ttFh_WC`yJDz`0^O3-hO*$KPzjNf=AZu2 z+^vG1V`^pf2^S2FjXl)INB(Y@(Ym92@A&xm`H8`n|K#KrQ7U59Xy6y1xMnzvg+)fL zL_E>*neL>VZ_IT1Qc~iM)R-LYSwnenim7~Y8^ora0s`B>!(k|V$gg$f%E1pgIdTy) z^r#`{Au#aS4Kmf$)jja`z6qPUn2HJ=9%3=&V{Y!Rca6?Dh{e)OCx_wA$~Bby`ZgSu z`JQIOZd{jq70C0#_gC7brKS6M#qFTMY@nyV4<)N2N&zM`YO+w2CKMn(v!6?4f`x8F z&g*W-*q$Bj{i0(C!x2w7nRe~lw;WG>>;8R+S<5XgEzcVn-8cL7`Tgh5%eQXbYNAmm zOBMltm`p086vRVBoJu2P8HJpuX~9X3iHT9{N4swWMO{GA>mMAvhDA+(wh@b8cJdl4 z>n5j*nwgHb!MH;sBTqHR-NK)C(c=g7;Nyr4F2OB+L*48!@SjW`T@}t*Ysr-6o5fRXOhDfD&28_I3ft&gI`TannY0=wX zfYUfXnwngJ-Ol(OS$1rtZ$QAW?5ik!D94e}(c*G)8!#>VPf4k*i%oPBBw0S|Zdx2` zx@%mYdi3PU6^e?A%1H)WqLku2uu;EYYd$3W!W?znH$PnWPq#= zLuXLWbflwU*SrxFMDIL3a1uhs3LF)|IyKoel6C)hK^G$6?%iP}mFVWBQ3|Yesa9(; z>_?74X4}BX$X*}!^5rTBaDGnr?%ywiNVhQ;olzXYXa;VDxsp#axum;=#uDxFOR?5aZUT@$sI(B_Ap(Hbawc>E#68yhnR@ zv9-1JrTSEElv>=^!FG=8DL6)_c0su5baBsR_g2P zSHx*--?nX=skOCRLc&nM^#1le7~tO(ogH5S37>JDLhE=m)K(0_`GZg5lB54T?hNTXz<7#9>_4fkneOu&zPWBp=;g6^ox zaQg;a&J-RCh@wk4^$Nw5m1z$iJoq3eNTY=cV0Y!})hu!`!cbeAo14!g2U=PqWYvxy zU4+rZD_|fn*%dIiwU>vl;kojivS-g8*xZ-DGim&cI=f;0dI|#i@*?5i3sBZ3BQ3oa z%?|1N37q=yrP7@-0cC?2r!Kq~Ks{xnRcFOfh`t*(Z@vKnW0%WBLTTNtPxfP$VD-vr)`x%)+~Gl0;LpJf=RruwI$YFm z6x@+Q!3Pxvj)%&gz?AZg*_*3Z7h%0#0qDSwuMGs9Y(K)nAmVrn*ybLC@HQZ^2R2vm z43lRd;J(LC8xE7A!V(S*Ryp1FFx0zOV0n~mNUd^N&Q51SMCftP-WJF3q1}_~7g`q{ zzO<_9B+3^wn}P0X&Ojl%!B`Kt)Yd}Qy=)f$?AadhM8-3Ld>f$uzIyd4`~G&`{5ls1 zjo;tsuEC5Ok#p6cGRoUQ1T=w?4cdJAN^jJX>GSxWq!Ct6DCn?ER6iW*_ogwtbg za1_igg@w?yPU8AaL3zGb<%P+UR4DPmGmYhX<{;&fwu z`Gtl-k03w4+t`IYtgLI0DWzY(=Hz>`n*IE^D&2PASgP0Q*FSYWfBN*kq+}gtAlD(K z7uwCm+J<1PaTGNV8s|}rUo9O@pI(jgp(-6zp9+hL<6o-g$|`01_{~WxYx>l^S|BB+0xN*8!`DM5mSIo zxS3;^Z7!mWbXJNR8?#}7cHYPa88<^ZBA$0b#a2BS7!-8ByZcIq844H&n1yB6x3!Rw zd^z>0(Wq8YUL__Jvy*xQNUq26dlDMC?H$!DMRh zs2uhaQoAX z0KxqUBZOBVVi@}jJ|<~z6H1!<~%b=(cH4jroD8LQoQEz``fr*;T03ekc}`u^pB2y#)b@x zHM;yl%Vk6%_|e+x1`sWVatfKy74Qdh9GFhl;(lnKow)x2uWB$G>(#(%)J4Ebr_P+Y z5gxu5f=5$Z+X~X5?d>lBLUM3%eMDM3@b|xiZmd)OT(W;?h}9|t+jWm#a4Xo-ihcuy zb{Ic~Q4Bv&g&zPmgl6*!LK^8z$bb_vGSnD0hDJr*fi$#x=S~W#!3c(ZXMA1pSb3X| ze0AS-BO3tEbb8z@kk4!}>K9t{_26Izm}RMOs_A=8O&Ds`wD8?OF}ddnuo4VsIgO2s zSUSk*%P}1H03i#*5ryA4*JPi^znZ#hX`E)p9jJ(01{Et&fe$3eVjcd=u(txL%lY_m zDXdXPW*9`B?*ZE9pv3|&ADL=vZVr9*YCY!fmueCZaB*>694#s-ISJ=i;bHl+XG2kw z4(*eXm35DcGftVJh#45LpkJ<#mzVE&n}2b?6dvBVA$=A8J2EOt(SXr@4k&TgAsG>v z7>1oODLx8>UYca61Ri-h^(-%*W&V$oDoE0G^bZ}>AKSs^Rhg5WtmBiDAHYf*%E&52 z8vHGr5{y|O`n!%C*$CSjK|gp3DtH7fd+X>h^}Pw)jIIo{x4$ov@Pvhj~_p7ruQTlVfp+U>7Y!H<2bc63ywA;Dfd#+j;o*_~jQo;Z^}h!g#E|$p zX0;N>kMCk(Sxv*H?kchSC@K-4@;b}D2KpcYt81u$3oteh!}8T&uc7hr?0x&m)0hJ` z)04K0S);^G4w6I!CTUr0WJa;8!6F#0w%gFPpu=Iw^hH zMOqRKGcz-|l{RqCpvN&>fXSvLL^K!D?2ooKcSQdgUyemM*E1Iy9nIQziQK}l%fH8A zoQDQ;KDV&iZ72L`TU`4W?oTIla%r`s*C)nT5}XU9wb+Pb>SoSd8-@d_45?V?Sf0l<1B4sotN8~a`QQl9xQen$a~c=qf%qx1M8pffk@ z3=Dek4X;5>Ikpf6B2otx_hCWFnx5WptO*Sl=jHhAM>>}Y3kyRW-U8ywiiyNkEti=s zvcW>{p<%zqTW0w9_{20cG#Xz2DfR7~;XvbfmBJ5v2a2^Df@eAPj$05u?M~vz*zU=g zSHGL$sbc5385=Om?9$Tppva$~8J^eHUW6WZ#-D2yx^UlgAb=<>rydp4##O179oG6}`UjX*F&?-ErbT9 ztm`EMgWUiLOO{hT=->qz#YY!W$if0|Df#AsNaILIq(byojMU_Rrhm$92bR9A9{y4* zO9Z_Cbp5D(=9rl=MOQOm@Z6~CqP@BOaS%CwKz1I-+fuN5S0P>!rk`xlMvX8dy!{8p z4@3tz;mL3xfG~52ilomu9xw)4TG}sy%Qq|n*SH2atydC!A1F#2Mir!DdtC~r^JE7z zj?TPqXkfr0h8Bi4>3wO=`%(L^1E`Qp!Xeq+0E#~BTA$%8d5Sp4)s!ZD<7G3YWn?~A zX}PRLph!s-oMmnmaTuk-!Jb!d-h2SrYcvqj#ZXKfgbmS!f5;jb&s8zoXT_f}iAv0? zMZyuVg}(w@1(IsnIE9&Yc0s`!vU330B9`5!al(!-CME{tgQ;)-@T#uLXqq_nlxtuV zKcE>LP0%YM7~d|JgurCULQx}%gV^!o;SvGJe#&`pp1zhqZi5O}5Noh2Qvi_54p3Hz zz(F1minU${+7yoAKX$15twG&A6Y%DZdH4|su}2jYk_wF|`E{E9<(cU$hZ?8&#sZ=W zlUNL?Okk5^0C~Hfkia=SGNNs0co%PGg28eFgboQ9^Uf?vxVD&)nV|SQtgG59gpXbB z&fOGt&2+TE75x}~o`(mhd{qKoY4s6-bR8zaCMyAMg4$j|4&DE}mlshFU`Et-qE5y<8;>8;{Cmsq4#Ane;1&5-1SeAzeH6g>X zYP>M!rjI0_Moqo8XvuOhHMQ*&a80ixQw;*<*mXf916>0yAz}jNc3&Z(jY_= zyK@TeoJF<@Lh4r_SA5TK;DxHrYy>dJh*9qSrl#|kYena#uY=z(AeX>Gb1~Ka(bwk* zrNYl5IV}y8^1{dibi-%BMgX8kdH?}FJ=i7wJVXo95?;?k7&?6O%(5SFBc!t>=*!-o z*}HKS5y>#7sUb>mkIRhB4!9iL(ZadUM%?gmz$B_2IeXqx>De zfTy)uem&uyz$rp{%+SBTHQJ7Og2IKgJ9kzC3r0C+M!^p+0a$ZhU*8?aoOX+dWL7u> zQ4rovNgrF-zsbo-%50yV{AF^2Vp%O?biT|j<|wKqOO{-zOWpvfj}#GqUK0;Yj(|4* zTj*7@7JmT6@CHP{2T#%2l@wa|R}(Y9FO)H`v8>dkkR->^Y7WR36ftn;nAq6Y@E_c` zc~cu6A3mo^E>ezA{tE&lOd2zR6nqXS7MGOdf*gk;`p^eX*CxDq^B^$r9*Wjt3W2Mb z_JaWYgj7bM>{@!|$`wkw>ch51EmK$zmm*P5T)40saGP3GH0xceOr!JEAnc52@|Cs#HA8R`gD%$8(sC2okX=|vg(AQ@QZtW~feM!aLJ~OuT!Y~KZM??! zP@W2QWW^Yc?9;)S_+#qoJ22Y42HoqJtn4~ySVWt}^a6=|l@N;EyB9+!6GsF!j;E6B zhQ`cBUd-^4x5N^3i9$3nWFe0BtjE~_-qvtF^RxfYq94YGj9J^=!IvnnaZ3`QT?G@HK9 z$XK({Zk6ZZ;gX_AtnFaFODIfx_e$0>2-)rw?gbu}J9q8|h>SO%zPb4kRKQYv?udmq z@5|J%ycX}E=bB}9v%+>$(lCIWx|@qvrX5D}TDo-UDW%VBie*m9-<%Z7_*|7rUgE$Y zj*3uC<1I{ki`PDO@!X`~=Iy&s5I0B!BY3*yiX z^8{DVuap?xkF^=pYb)|`6JQz*Dn1QNRV1tTPQv4_dFk@yyu|xE_|I0vsK`&+L2Oy< z$_MXMVi9KX$g#wa129J~rI?L$D(t#JmY6W*FvQxi-4Ac!`Bv|`@g4q5P=WBm%ykSx z>nTL^eQtyu+eEoI8)E(A!_CF6-?Ln@5{s&B5#VLpS<_QzSQ_*->`l@%A8=KS857jtRZr6yfYZ z=au!zX1EkaQHNe1f|H9Z24y{2Gz<}N_wYKlqk!ISWCSZD1wjG>K-hgB%?4{h@zO?- z=@0?c+vqeoya5twTC!z_*QM$=;g|?K#Gr%4u_raiFkETe4LwvC10%8pFz!G>ci$+YDu1kvH);g6`^d+HfzY4-!O z9h^~bNRHFWVxyv>x`9c6?ZD5QsF`3UPtnTJ(aHDY2T;fk<5b3vt}fOY;S4@^C{>Y) zvHrHh?GFoS^^5%XLg!+ET|r97Wc{&~aBgYq=VXi^Pa!$93ERU+{c4+yp#tj9T7<3WY`pMJ7F5U zhyrnGF@;QVNW-joaT%O~hb$Kc&sA46P1 zwo~91dd_-!4(~|At6`a$BToA@G}jSvT|#0hO6g4uwa)`NtX#9^`puim;80!k*x3l4 z-KT&yGjmRxHW6DiEmVZPlZvgX8EnkVE3m^v(}Q69U%aL;QIQc{xhOp(J}@$*0ZYFI z84zI-<>)lexNfxD;UnkrZIIqiD_)8|v#T{rqlJ`}PwIo&&=j@`MV0 zDJ(q5EpGSg2sig9XiY?%0z+LwNww+ofQR%a%!Q@p<%|0}Rq|n1euZZs40Q+5iQ%gI z1mOPM2%&%?ww_@9qNNmMF1bE%V;OJ?-mrLDNn#X(GanxsS0xkank*MlL_Wck7;Y`5 zaB*{ML%^~)gAncLtn=nCNjrkn;S&(}iAKAAo>rbZb;=c#9IkYBjaBJkQ?FvC3d_si zd@yr_djqLVf;}`MVuh@1$;L5hI})y!%EFexbNU)@4vEpqV!%_kuoTQgH`lYDG=6WD zIF#z^sPu%RLc^fi&O@aXr+yTJt7ylV^40z@>EflF|Al4X;4V?c2%^I)qJE;Fad2}V z2iOh`55I}7e*t68FU7?tpwY4j$?B3r8d?(*eL7DR+trAOY=pTPpKbq6*rX{A4i58W zxiVHkq1pn6G?v(ow1i*1dekp>lGo7C5NP8EnYrMBG4dSf{~^xD&+i=&9b;$(U6!;_ z=*!13Wre^D4)aVK}r+Y_>oOUQU&N>dxbOChkaZu2(T%EQcu2hLW8^va5Za2W^ znlF7ezFnyA4oCtHi$C8~kJnJVzErlGC=F;UuM!fXD>78SWoR|%{*1Z#{M%dS8=~_`SVqjCNTE!;H-miFn+|Mnc);f zVX*LagszuybYZf=x0u@y*>*v1hD+!@@EV0oA+Bd-i9&I}*v=KNaPdja!f*sLuN$xm zZ3VS(#Z$qGb`QgAa|4m@uyzx=+oRepwVHGvy!Q;nE zL(RtwRK}d`ekD6j909VFI`h+IM%g=s=2pXr`c$h8Ted92YZ)Nj?q*_IibCTvTRZ>k z-eav&=gH;~;a>-{b>OyNU`^z`M}<5PT5wDtKXKKr|CE)-Kew@J?$U>K5qo$mu|6kf z38nw5tf)5hTLppE+UA${PzoR)XycViN3@==+QpJISKAEh8yynmuzv9lKlTYY+FwDW zI5PI=6cbTC@V1=WczB{S;udtb@~+usieyDuT8>pNEh{^k5}U~|9fbKy`1xY|1RIp=llQnT;AgxhwAlwJ|6eS{eD}ICCirOW3pND z{fmi_)5Y~@&$ot7!Tl^08YlYE>9<-^+^W*QJ%7QfF*r5*K^5Ks1LoGR+oBgg8-8$j{n||Dt6C2qew9|*^&v-}Ky<=8L%C&s2FKFv-n}t@LeqRQ z6GD%Lg$>cXZwd-pMfWAJrZRPs%J@;EykNrW``k>7wQ}nlcr6pEU_8Zyg`FCfI~x^E|h;Y>;dQ*6c*O$kI!1>QZuE&EbXP` z6qfgntx`j&R-?w=@Nk7*K^84^5nz5`a&q#Jm$8LsXmU)Z-O!eWF!AhPm-)6w36Epy zAL=w?{KGSV*Z~Mym}@XA!}m5xSn=cRjhaKQzGOCSIBeLk$OnZt>Pi+_Uze& zA-smdLe%Zntx+eZiygaGupB?iYPT|Lj+VP*;{eNj?f3MV@L=$X=Lc^{DLDVya)Kh< z0i#Y^$PC9RlMqvki?ywT&3x;OPDblb#>WTEh`UkEnT^t%`RnZ2v!++mgIf@O&>HSe zc89KsNj*VwSSh0c{OMJ~0Y;whablQ(KAo0MM0EDo^8)8zD_)3*fL$4h=~9U?GdX8JF#ikDRf@vstN-~A1A{8> z7s)y~Ir~9gkjB{2g&NDrTPe`X=Bv)_PJ2!MM+;D?BGK#FjM9vT1x;sFq170M ztG+J`-V1@}jn*IG(mIGT;dgMJFf`}`B2At>dp#p6jr9ON4;-Hi|4=Pz3IW}Xkm$uD z{{tLuWv+GuY3W(h;x~*9`wTlaCgs%&U+4*?Y$4-Nrp-?f8N+UhQbtqlY#>2+czZjhpCYzSF@flP)Y;1a=58(bvl^w%@#m zC)7DF6LE5Ju4&M#B6~@E^r#v3>tve82#z17Ic`&^TGbu#(3p=jB}dX#Xn5_2zGkZCh0BxJ$u=Im638!5a8dT&z=phDL>d>^c8+9@S16<-G7y_ubNMqz^mshv63K z+;}P+cG7UVHagv?v#IIoz(5TitUD~_jAd?a#zY5tGjJ>IJ*$`~c&htPTfHEjPuAMl zc+}a|3tUsHE7CP1x#6fXA=_E;^7mKgA;WWisDG%$5ogWS=)4#c(Y!6>mH~!eWk541qSzdeZGh0{2w$(kAMN@^*~b7Ie~|POzpD$;|_LpaW{UmU!()K}~@`pdGZM zM?I10i4)8lx8`3v0sH8EC!YzO6aw(W9LsG5%XNu6G!V1^yv>q^$D90kQsRR@|Ix8y zLj(%-Q3*LiC?s9KJ`@Do5AWQJFi!k+SdP^okK#!(cYl^t-GoTk8Efh1$HW!$)RXhB zuG<%Oy;=4f2x8sFGHCM8&Lsuc^a%fxHeL~#hb9zRBWSvM z>`@RI=BQ+PBuiV{ZGuWDIy1APb)oI$$oIkaxV?|ZG0wF=%KETKlMD(2bSw;h=R$$VTZp^Y+R?<=kzx%dx&p4te$8RYzNUJ6*@VI>9i z>-OCtY9H$6S^#i!_WZ+LQYxA< z@~uovf|}$%+SQIPEQOmt&yZ{T;-pkWBoU%=5$P~2gJ`v0j}88Iv`cUkD|7QI5Q!OR z^8iq4&;v0xZ+`vJsK`As27xCt<#xI!5tAbDc=hJZ_1=x>T{2SJi>#s*h>045oRHf# zE8HpzM5{m^QS;u5YV)zl7PlHRroJMjS-Z~?V`yb`hkl|T1cOKdYmkr7^FZa+ZUMpl z^PA|qIMSYh3yAIW!8TB7pdP_tv_3z()Zo*pfOWyGID`s6r{~0;*Lb}^v^Z?GaM~GG z^8gykf)-NYU%50mFwVV|tP%c-SpwCU3Scd&Ea(1R6m4GfZ3;H2T=3*u#&F0P5Q>T#6DTqJi6h#*$ zyDQX_fkox~(&23`9o4D$c_LRHKY8*h^vp&|I)y=1Bu{P6bz{#anOV*=U>P7NgF_!Q zZCYE#ULVRuQ%Z#C4}%49^*xQiIqKD~zZ&Rwa{AT|!V!S&)uiwH&Ca&B28PSk^?UV; zk}m)cnrY3f2{H^GbdV_SJ`o`L_LG}1H@_g?=Rb7 z#Nb@@e|}px@E^Z`0P{cpY!9#_fCWE*kPFglh#CMWp|U1aKJdU%2v9VUa>IuUM@v;r zL=Y=~+UUg?I>U5PCI&pZ3;IJs7kaTSMYaO9CA8~GtS zPCVO?#OD^2|C)>`v;yh9o61J3(GFbK!&fKr*MDind^9Wma4`|YDtPiV8#P+XD)^4C z@;0Rw6pIm+(_TS0@!y|dWDaUFu>v48s)>4-^0aAjrSP=FzbdIu52*12$!O!DxRbuo z2O^o8D#yKaki0vtOg488?)8@M>F8}a{^_H z?qm%PQu#f|5s=qxKC)-_^yvZu{rBg-HEPkKZgjrxG)539iC3=d&*MTvkehEa!!@VtTkeRlq2H zbuHFDg)iDpFIzbrUa)g?s1!%~nGI%+?lHeWjoyc+xC-$vI(MOJnMKyZZ=Y{nK{s4j zFk-|A8D{B0pI6Faa>Z(Fg5FzrInMP}v?}LY%S*VR@t?2X0_7!*jT!*_dT#RZ%@%txU@K1q?478mxMVI8U4J@(d zJ=_#IY`S#u28seNtJ=GFZ)URLxhDUJV*LDatwrNb*Yc_8aWlSm53C>+8lwspYWJ8; zixs`iBmz}s9^fcVtE8cV97Vg?zP%b83>skH4IAoE5kaI@GL$|QSp@=k=*%nTzb7Us zF9kHqbwRsCi2~~>o49iAEj^HM%rwL5#s8r#dbI}Txvsoml!NGreu~J9dDfWQ5_n9&eN{j)0_@dg)n&smd z)c=Y{^}qBSxik9|U;mJwD*7t@ed%#pi$1;>4(8QgFK9BXW&4gD4`ueGYugtahoWA% zJURn&BqcuIE`mESlZkAFGRu*uqR<>!K%^Vf*2*Brv+*LC2NB^@xTFQ^=?Fl-A~_oX zhb$3D3Q38gID%e0VabQc6Z3dR7k_@8#m{R$etddPRbjnU3F44W05IR?Y$MBG`VbOU zGRT`&63gka`11#^{X}T#Yway^*Bm}jMvpwNl8ihLh|=fC|$u^VTUSZ zcf7T69eyi6xRzx);_#7>2!RMg@^;Rijx$A19y}`T!Awi4=_ohhk(eTl!fKMjXW2kc0T0TDCbtS6;Upl8AADx@c% zZA@P}mOf}*80 zXNP#0w3{@c($x{c9Y4P@z|U;d|1XyC6=d1-lTUGW9WS@Xke*Vy{%vATvvDsz(YjLaz0)Gr)#cI62xf0Q=n0<*@U z-8knIuWj3GL%%^gA*4B%{_@)<;ss*Q2ht}1JH zA^w@*mQ^!Wh9b$?*t};IA{V78rnvk*UQZ|eyFPbgc^^d6MEQ3EuBQHlvj@Hq8d73$SaE60tw^H(P9KS;Qr(5LfKc93LK$&K!u{d z5uE}_-2#m$2TBJhvAQ5+j4;-z>$#-`F6IpgO)GvBac_w z3NSINxJehLni98^k1vEupm%oBy8t+i`aQ3o`|L_3St-CH7gK3KruBzV1s)K801n0c z|I6Z5Si=~|bl!DeT;wQE;#T=Q_Oz5QfZ^3psc@Qu zPvbCFK-jNPM>CGO4h2BoJLZ9(nH92dp~3K_-Hx^EB*-byUdcu>2rWMi;-INKP%jYV z)P=hUTHq6-7cmDrcf&d$&@U62*d>km?*B+tGp5f#S-H9Yt(J7C~CTsU0kX@wTbw+^T<}Yx`4i93bjnCj6iRPa4k_K z)~s7MbYI*Qhx~qvi<b+;TpKg($w zb0qAp3yKe!w&)zi4ksQW0OJ}8(>-A3#It(G;mZIH3`;vlr!n75o;=wLx#Y9!oi2&x zI_gnCt=FyaAK{+hfD8Gy?GWEk#p{M=9fg_35@-$xS1eQPm~jTpGF}cr5>7gRi0J4d z)^Pe?uRJ`OO{ow^^A0ZrAysjPA$s-Tcspc}A0Pu;_wCz4=p{syJj-=((h;0nmAYzP ze;Yg66-MlBD=xY{tAcLIpC;Gk$JbK*uaIS$cyQbnEZ`m-Q9QqLJ zQ)m|Wi;=9=y?gh>rJ#r*%rw^8?kfNK*{fH69BiQRG68cUijGad3+}r*_-5?AFO-LP z;0{J!3n!;mxLv)e8|?Reo|nEA^#&evModIAx`tp1{|{Rizq_9I{CRawXO7HsWyMbd zX$|pbIKyWs3MyV{q)-gwfb{}&VTM%+PYN3_P?TL1rU(jW!ooL=WQ9Ov;MbY-@FfS7 z;zeQJy6jNiJBEc$@rNtd(FvE%~_i z*Lo}Ut`(xA#cC#=j0#u4rzQ-8!u)O8xziBMYQ51mHa_w3CY-=SX75BK!FsKNHxl07 znfC70-Pn-7^#4H&HWSIAs^C1Xu5_7oueK5%5%J;I{d2`fqc{Fo`LuZA+4kW&2KCnj z=$$f5F={`v|7hn_Ydh=ndIRIVve#B`y?VzIEtllDi5a8*sCLe|*x!G(iofdWrPT*+ zx|sO==hFutU8XcUn&20f{jp&1=Uop%Z-q8Vno_o4+RWhLLx)xkyHz7Fl>@t3Gcls@ zvu!zdgI3R-^Xk)^Yz?40RRtVNP+nS|&DwVm^Cf(3;^O$j++uC*RiU9xIL_8cF9XWb z-pEJ}9caytWA35prDYbCGW?%I+=>QPfI^)0h{IJ!olN=&!z^N4h{DUryN;M`?b24J ztM1)vD1s5KHzB0Bsxs-)3Ooa9(kZmrkK{s+sn=_{XQy`$y<)uQM(|=6lvVV8US%`u-8N=BkD-z^8wgp9_WIp%9mBs9X{#rC?{RBOP{AD zuwOWS5}8>a}*mXkUUM;Y8q)Jk=muJsV1G9|A(|YWt;tWYS35`ksb2&;@b5ChSTI=(GnGO zBO^yY)QV3zUQ#HpXUfGNHuSJX!sZ41och}wfSAD7y-5L^4BCi%EiT8Teo2SCkL6#u z*Crfrf)bR+*}R=}&tq5gU%K78;&Jmo)>lyc=F{yX;u7tFyg(Y$Db&LG zA@x?={0LJ*nF+}H_4CIvF-y&UL{lV=Qg_Z-?AJqQA2S^l+~q)IWR>v3&h5etrm)!8 z8MYpl6+EuSsFTOKgx#_t>NMGOUhesz1oOB}oG}a&;vi8A}_k!2IwxjggQd3uSF*>UNjSeY!ynsG;7C8D_V$-38B`C zAEkC8PNq$Fe&fjj_+_~n5O9d z`-OLY6t)4z`)O*cmC1Ch#*-5jSKsI^NHauwcw@e(1|OU~xu1zqmKv!$XxHqf`qdmg zY0^f3$Y;!rK+aYGZgLELDt^*RI8D}~R_+V%Sq>KaSzX(^64RK56WH*MRdO1m!|1cYH)S{mMh>Zc3s#%61O9Gr9h$EZ0~ zTP-qra&9q~F~6@`b{2}zo{&SwVtvb3;)@hekc^ds*T#5T6Wn7c6CvZ7ap%rs7xnPo zq_N(lZAtOkryrI`2S>k&?OxIfq!k?DDh@jI(zJ_w{{`wN1TpgWzf4usKgGE4#04`5 znxmex=ggUPSkG|_>wx9u#-j+zPkM3?97`{ijjSWlh>Pqi7UP(%yXXD*T`xDh@NKms zk;*6773$^#e;|rSFx2GC5Q0F&h8qpJxhLk?FyM# zgYoIcuAU9Z;T9VZ;)tlWhFsWU?&K8qEh(@0)z zQ$V&?PcHRS`d3QLNe8(M`eXt^LvKIvNb>csH~a~r5K|K^iD`q@1+RA&6ijtYa8p6H zSw%r<{EV4>{^<`9;;aP#@}h4Otc?CUhlNR01EX0AyLiW6$L1GTYk2S?bm~dpujBLVPcx+ zm=VJ>0OA)ey{yBx^*M=as*YJ$rv*Nv&C`6#3hx>!0GO-pf2^`f>3`SYGIjLn##-08 zOLTrJ4>t%=6C*qJN8L`HG(arHR2EWT7(0x1)r7gj+t*h>CuqKba?&>Mxf;vPK@-A~ z8q!<5M}LfcLbOEjgOkm9xBSMdz$9)&wyba3wWgxU;m;JZzIo63iNBn#mQnN>>8@bH zi8y%BpLgm)dn_XDqYNTJXOTDh0~uuvS^76FCZ>zin@gB(Oeko>NI@(zYqXSy?iu9a(G##f z;oA#(t18i7zu6~LS-`JVtFkJsuNzuVNbJHcs-P(%AASESqcvlzND%Z4jTmgvFxO$~ z)hqizF2f)YP!qC=ltc_M@)4LBuViG@qdRweK)WNFT+!kQb89+{dQ1zK_tid)PLIX8 z;P9mhYPy*|Yu8GQjaa3zdW!ki)z?xsn_iROM`o7^RaSYCI%(W8Fcm;yYm2%w264-V z17~Ochb1>|-3sKIp!U6^H0IKpbc8ouwtMsDB*mD{y~v(Iypx1v)1+BP(O?+U1TR9q zAODh_0fd-S=vWNL9d$E=m(f|7O#xhZ1MkmDm41gp{a1~&J>cVOzk&Ft!&Y6#V!d> z@7*P{HvQN!sLkV?$M@)o_8&ZWjcM~T{D9V&Ul@V5qjWJZ)bnnCliBP6ORR~q|GVTc4#6uLPq%6UO=LT1lF^uZSFWs?F>BUwp4Ks(vE4v@r{UM&xuygM-R+lo)QJi+PX%~o8O=rnLsE9AB!Xb6N;az*TUl5-CV~IMA+3)4hvHRDWB=utlxcO zJcFQD%lBWT~2n6Ykc}uWvdn0vNvsh^p$v^`+kMBS+C)8AHSz z$)VSO-gFIFKPH?ZBnVxF-M1%j()v=b-o20WqD_#hb)&ul#hBjXRGcg^```iM zq3yJILrMDLaF?cjb56d=JFf z9N1Cw5!NkwqdtigCJhS z--|D-{QjW^%8>xJ{*fm$oAM!$0;3%D=HMX9D7)thkB0{X!Azy%cI;Rif7OePr{Ol3 zh&U^blCZf4%}qa4_} z@TK&1&e!F%pp0H-Q=7T{RT9*tQNW&&c`x4i&OCRqX|E7$RCQ@$>0Z0f+*Obtyk}2c zu0@ncBLEJyx^-XJdbF|3K+B}rvgJk)mi|cgF+I$D9}@&+Dx`;GX@|F0*Bw5ZYK8U( zZ^#;4Iuf0LxfBmcJ>X_J0M4YVap48IA60pjr58h1qV@vT5QK-FH8^L$-h_nh-!a8~higaiXT5}7azgoG5KcwV>l;Lu-wTR7{ma+6e} z5K&ljp?2&DQW9^UzZ`FOImDo-T1Em$k3Z z!n}X+cQa`a!#qESW?=>>dH>~u3kU83!-?5;)IsbtO~=*H4ckGB3TEFLyD_bD61`$@ zi~6#}tcRD`1h-JBTy0VA@`^arE%38RlKERV>(0;f@~W^)mU9y$+I(@b1h>en(U}~Q zGwJFLg1MPTY>wiOgy0uM7x6Mw+* za$J!kyK^rAGS_efUm^@F^Eno^opC_5WuPzE^WgJF0B&62>ermO$blkq*t~f%)>S~N z2*wx1i^qNif>Ii?rJveYD#>(8egY;5cu1AA9x}un#+Y1Uqf}$S2h0N!KtwAX zA$a+29?>{kTjVhth8|nLapT(lIw1Cqh=RXy7pc`+rxoSZ8A%1w!AF_8yTRv!de+Sv zKRbDM`UtiBVSqSjbPx(uA+w4Xd#$T#$>yxJo1#_>0~cPg!ptre`4RxlDy+Uwsaw?) z8Sp_h#(Znfq^ZdK;%~wl)w~;)5z?)n%C&wP2v?m0W634lP%rvXpTouW)^Fxb>1TJ~ z)v11;&kQr`OKrSL`^^1Z?PT{bCZyN(@3H_Xy6`NOh;F3%rUT*Dn2UGbjDxsxRRg-GwB9T=<0i?jez9wM`0;9|}C}WJda_?RvayPLtU1kShy_^R}={ILOl^ec79J_X@3@P!% z&*7YKPv!GIef?UU>o~+^!@-`&c(6}nZeQzWAt(g0mW@!`ZRXjO6d)-?(x-E2oKmu3 z)%i9jRji9sI}w9G9*GDY%)Y(t%zU9J__Ewy-PV1-qz`nf^}D^|U_~MgYQq$|rRjn9 z5mg*Mj?KKahMN@c=kv&do38yz55$6*H!s1r?))|4+Et$eOjXq#AE-pf8V~Vs8gG`tS6y*t;%Hl@+S-b zPD`6iPoDFJC1au>J3MdefYM%isG?E^M(w~<7gkvPN3bkYC?1xf%*Y?}q-Xq!A4^re zt|lkDrLrZ{kSr!-XQqUog*uxyb?Ue`34P|I6{Z5enka8si#hRZ*lr+}XTDm0`3YKQ zePL~3L1@_K6@&C!gmm3l8hT*;T~D*S({l}C-tFfkAHFk%uWSFNNBN0~=a&S*A64{< zqen-)bxUh!gpG+`ibpNC^ukIq=WlRLQT%{<<~w#_%rFL5nl^XtP4F)VWH0v_=N;hu zcc*vh&Q+T{pj``J-CjjEd5Y+SqCFS)kBn!<5VX5hy4#a;9&MxbYu~$h1w0tBEM~*b zn!_vcf%4nJw8_{FI+R}5XSw;3J}o&hHkx^UolK)Eqw3+en|3s(R488H_llcg5`W~F z?E`4F_vOpkFNzni;;`YvC7R~Uby7d-Ba^rI&9listD;(4+Y5QJzsbY|w^gxWCMkD2 zNk%MGRs`t`qMIiR#fw`CIHIQDKGt*PB3BW0AwhN~18&T>mjaf7t0ZJ2ari>4^BTqu zzve5ye(FGj;mbMY)4SW0q-y7vmeypjt^elb$651?Y(Cnfq(m^SqAV`TQE{masxv0P z(M%MEe8x#{zA)`4{H#&XEadA=^$5Yw-AH$G(1m*7BxS5#pwfj!x;1 z9uSeZl2+8(=I&X~U5s&UvcGTr)Y+gY;gI&5-Xnbqc8~0T<;5I}hj3CL@m4>(-W9eZ@Wvooh9+@pt0O zaREt|J_7(|Yl|^k^j;4g?LPR#|Bj ?u|FliC-I6pND2vbSq1OVw<3yzX)aEE+J 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