From 026256b11b99c28b00d5f2a82f69e5513819524c Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Fri, 20 Dec 2024 19:00:53 +0100 Subject: [PATCH 01/10] fix: upgrade cosmos sdk to 0.47.15 (#3323) * fix: upgrade cosmos sdk to 0.47.15 * changelog --------- Co-authored-by: Alex Gartner --- changelog.md | 4 ++++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 434d69161e..590878fccc 100644 --- a/changelog.md +++ b/changelog.md @@ -30,6 +30,10 @@ * [3289](https://github.com/zeta-chain/node/pull/3289) - remove all dynamic peer discovery (zetaclient) * [3314](https://github.com/zeta-chain/node/pull/3314) - update `last_scanned_block_number` metrics more frequently for Solana chain +## v24.0.0 + +* [3323](https://github.com/zeta-chain/node/pull/3323) - upgrade cosmos sdk to 0.47.15 + ## v23.0.0 ### Features diff --git a/go.mod b/go.mod index 1e6316203c..8c069a886b 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/cometbft/cometbft v0.37.5 github.com/cometbft/cometbft-db v0.12.0 github.com/cosmos/btcutil v1.0.5 - github.com/cosmos/cosmos-sdk v0.47.14 + github.com/cosmos/cosmos-sdk v0.47.15 github.com/cosmos/gogoproto v1.7.0 github.com/cosmos/ibc-go/v7 v7.4.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc diff --git a/go.sum b/go.sum index 47a9ac45ef..4e2c220279 100644 --- a/go.sum +++ b/go.sum @@ -451,8 +451,8 @@ 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-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/cosmos-sdk v0.47.14 h1:vD9JyIdlbVaXMOE/BLamViQvylfUq0E0FpqdPVv/fWw= -github.com/cosmos/cosmos-sdk v0.47.14/go.mod h1:GrDj/zd9Tiuy8ZpG9PbUbhghCVU7lwyH0GS7CpxHpyM= +github.com/cosmos/cosmos-sdk v0.47.15 h1:xuIkX4IgpnRydiwxY2fI5nWy0SkRVvWvumWtGigwJdk= +github.com/cosmos/cosmos-sdk v0.47.15/go.mod h1:6L16fRG0ZinyyYMclrVAGqTUyb5UGu/hlx5oZEI6NAY= github.com/cosmos/cosmos-sdk/ics23/go v0.8.0 h1:iKclrn3YEOwk4jQHT2ulgzuXyxmzmPczUalMwW4XH9k= github.com/cosmos/cosmos-sdk/ics23/go v0.8.0/go.mod h1:2a4dBq88TUoqoWAU5eu0lGvpFP3wWDPgdHPargtyw30= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= From 94faf092ead43f8ddc4ce04da41bd0bed38fcfe2 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:10:49 +0100 Subject: [PATCH 02/10] feat(pkg): scheduler package (#3319) * Implement ticker.StopBlocking() * Add Scheduler package * Scheduler improvements & test coverage * Rename ticker.Run to ticker.Start for consistency * Implement block ticker * Update changelog * Rename Task to Executable. Rename Definition to Task * Use atomic.Bool * Fix blockTicker concurrency issues. Add intervalTicker * Simplify Task. Add support for different tickers * Add metrics --- changelog.md | 1 + pkg/scheduler/metrics.go | 29 +++ pkg/scheduler/opts.go | 46 ++++ pkg/scheduler/scheduler.go | 211 +++++++++++++++++ pkg/scheduler/scheduler_test.go | 403 ++++++++++++++++++++++++++++++++ pkg/scheduler/tickers.go | 172 ++++++++++++++ pkg/ticker/ticker.go | 56 +++-- pkg/ticker/ticker_test.go | 122 +++++++++- zetaclient/metrics/metrics.go | 21 ++ 9 files changed, 1040 insertions(+), 21 deletions(-) create mode 100644 pkg/scheduler/metrics.go create mode 100644 pkg/scheduler/opts.go create mode 100644 pkg/scheduler/scheduler.go create mode 100644 pkg/scheduler/scheduler_test.go create mode 100644 pkg/scheduler/tickers.go diff --git a/changelog.md b/changelog.md index 590878fccc..8060dbd540 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,7 @@ * [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient * [3291](https://github.com/zeta-chain/node/pull/3291) - revamp zetaclient initialization (+ graceful shutdown) +* [3319](https://github.com/zeta-chain/node/pull/3319) - implement scheduler for zetaclient ### Fixes diff --git a/pkg/scheduler/metrics.go b/pkg/scheduler/metrics.go new file mode 100644 index 0000000000..96d581ebfb --- /dev/null +++ b/pkg/scheduler/metrics.go @@ -0,0 +1,29 @@ +package scheduler + +import ( + "time" + + "github.com/zeta-chain/node/zetaclient/metrics" +) + +// Note that currently the hard-coded "global" metrics are used. +func recordMetrics(task *Task, startedAt time.Time, err error, skipped bool) { + var status string + switch { + case skipped: + status = "skipped" + case err != nil: + status = "failed" + default: + status = "ok" + } + + var ( + group = string(task.group) + name = task.name + dur = time.Since(startedAt).Seconds() + ) + + metrics.SchedulerTaskInvocationCounter.WithLabelValues(status, group, name).Inc() + metrics.SchedulerTaskExecutionDuration.WithLabelValues(status, group, name).Observe(dur) +} diff --git a/pkg/scheduler/opts.go b/pkg/scheduler/opts.go new file mode 100644 index 0000000000..8e5d54e370 --- /dev/null +++ b/pkg/scheduler/opts.go @@ -0,0 +1,46 @@ +package scheduler + +import ( + "time" + + cometbft "github.com/cometbft/cometbft/types" +) + +// Opt Task option +type Opt func(task *Task, taskOpts *taskOpts) + +// Name sets task name. +func Name(name string) Opt { + return func(t *Task, _ *taskOpts) { t.name = name } +} + +// GroupName sets task group. Otherwise, defaults to DefaultGroup. +func GroupName(group Group) Opt { + return func(t *Task, _ *taskOpts) { t.group = group } +} + +// LogFields augments Task's logger with some fields. +func LogFields(fields map[string]any) Opt { + return func(_ *Task, opts *taskOpts) { opts.logFields = fields } +} + +// Interval sets initial task interval. +func Interval(interval time.Duration) Opt { + return func(_ *Task, opts *taskOpts) { opts.interval = interval } +} + +// Skipper sets task skipper function +func Skipper(skipper func() bool) Opt { + return func(t *Task, _ *taskOpts) { t.skipper = skipper } +} + +// IntervalUpdater sets interval updater function. +func IntervalUpdater(intervalUpdater func() time.Duration) Opt { + return func(_ *Task, opts *taskOpts) { opts.intervalUpdater = intervalUpdater } +} + +// BlockTicker makes Task to listen for new zeta blocks +// instead of using interval ticker. IntervalUpdater is ignored. +func BlockTicker(blocks <-chan cometbft.EventDataNewBlock) Opt { + return func(_ *Task, opts *taskOpts) { opts.blockChan = blocks } +} diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go new file mode 100644 index 0000000000..2328cbddd7 --- /dev/null +++ b/pkg/scheduler/scheduler.go @@ -0,0 +1,211 @@ +// Package scheduler provides a background task scheduler that allows for the registration, +// execution, and management of periodic tasks. Tasks can be grouped, named, and configured +// with various options such as custom intervals, log fields, and skip conditions. +// +// The scheduler supports dynamic interval updates and can gracefully stop tasks either +// individually or by group. +package scheduler + +import ( + "context" + "sync" + "time" + + cometbft "github.com/cometbft/cometbft/types" + "github.com/google/uuid" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/pkg/bg" +) + +// Scheduler represents background task scheduler. +type Scheduler struct { + tasks map[uuid.UUID]*Task + mu sync.RWMutex + logger zerolog.Logger +} + +// Executable arbitrary function that can be executed. +type Executable func(ctx context.Context) error + +// Group represents Task group. Tasks can be grouped for easier management. +type Group string + +// DefaultGroup is the default task group. +const DefaultGroup = Group("default") + +// tickable ticker abstraction to support different implementations +type tickable interface { + Start(ctx context.Context) error + Stop() +} + +// Task represents scheduler's task. +type Task struct { + // ref to the Scheduler is required + scheduler *Scheduler + + id uuid.UUID + group Group + name string + + exec Executable + + // ticker abstraction to support different implementations + ticker tickable + skipper func() bool + + logger zerolog.Logger +} + +type taskOpts struct { + interval time.Duration + intervalUpdater func() time.Duration + + blockChan <-chan cometbft.EventDataNewBlock + + logFields map[string]any +} + +// New Scheduler instance. +func New(logger zerolog.Logger) *Scheduler { + return &Scheduler{ + tasks: make(map[uuid.UUID]*Task), + logger: logger.With().Str("module", "scheduler").Logger(), + } +} + +// Register registers and starts new Task in the background +func (s *Scheduler) Register(ctx context.Context, exec Executable, opts ...Opt) *Task { + id := uuid.New() + task := &Task{ + scheduler: s, + id: id, + group: DefaultGroup, + name: id.String(), + exec: exec, + } + + config := &taskOpts{ + interval: time.Second, + } + + for _, opt := range opts { + opt(task, config) + } + + task.logger = newTaskLogger(task, config, s.logger) + task.ticker = newTickable(task, config) + + task.logger.Info().Msg("Starting scheduler task") + bg.Work(ctx, task.ticker.Start, bg.WithLogger(task.logger)) + + s.mu.Lock() + s.tasks[id] = task + s.mu.Unlock() + + return task +} + +// Stop stops all tasks. +func (s *Scheduler) Stop() { + s.StopGroup("") +} + +// StopGroup stops all tasks in the group. +func (s *Scheduler) StopGroup(group Group) { + var selectedTasks []*Task + + s.mu.RLock() + + // Filter desired tasks + for _, task := range s.tasks { + // "" is for wildcard i.e. all groups + if group == "" || task.group == group { + selectedTasks = append(selectedTasks, task) + } + } + + s.mu.RUnlock() + + if len(selectedTasks) == 0 { + return + } + + // Stop all selected tasks concurrently + var wg sync.WaitGroup + wg.Add(len(selectedTasks)) + + for _, task := range selectedTasks { + go func(task *Task) { + defer wg.Done() + task.Stop() + }(task) + } + + wg.Wait() +} + +// Stop stops the task and offloads it from the scheduler. +func (t *Task) Stop() { + t.logger.Info().Msg("Stopping scheduler task") + start := time.Now() + + t.ticker.Stop() + + t.scheduler.mu.Lock() + delete(t.scheduler.tasks, t.id) + t.scheduler.mu.Unlock() + + timeTakenMS := time.Since(start).Milliseconds() + t.logger.Info().Int64("time_taken_ms", timeTakenMS).Msg("Stopped scheduler task") +} + +// execute executes Task with additional logging and metrics. +func (t *Task) execute(ctx context.Context) error { + startedAt := time.Now().UTC() + + // skip tick + if t.skipper != nil && t.skipper() { + recordMetrics(t, startedAt, nil, true) + return nil + } + + err := t.exec(ctx) + + recordMetrics(t, startedAt, err, false) + + return err +} + +func newTaskLogger(task *Task, opts *taskOpts, logger zerolog.Logger) zerolog.Logger { + logOpts := logger.With(). + Str("task.name", task.name). + Str("task.group", string(task.group)) + + if len(opts.logFields) > 0 { + logOpts = logOpts.Fields(opts.logFields) + } + + taskType := "interval_ticker" + if opts.blockChan != nil { + taskType = "block_ticker" + } + + return logOpts.Str("task.type", taskType).Logger() +} + +func newTickable(task *Task, opts *taskOpts) tickable { + // Block-based ticker + if opts.blockChan != nil { + return newBlockTicker(task.execute, opts.blockChan, task.logger) + } + + return newIntervalTicker( + task.execute, + opts.interval, + opts.intervalUpdater, + task.name, + task.logger, + ) +} diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go new file mode 100644 index 0000000000..a993bc875a --- /dev/null +++ b/pkg/scheduler/scheduler_test.go @@ -0,0 +1,403 @@ +package scheduler + +import ( + "bytes" + "context" + "fmt" + "io" + "sync/atomic" + "testing" + "time" + + cometbft "github.com/cometbft/cometbft/types" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScheduler(t *testing.T) { + t.Run("Basic case", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + var counter int32 + + exec := func(ctx context.Context) error { + atomic.AddInt32(&counter, 1) + return nil + } + + // ACT + // Register task and stop it after x1.5 interval. + ts.scheduler.Register(ts.ctx, exec) + time.Sleep(1500 * time.Millisecond) + ts.scheduler.Stop() + + // ASSERT + // Counter should be 2 because we invoke a task once on a start, + // once after 1 second (default interval), + // and then at T=1.5s we stop the scheduler. + assert.Equal(t, int32(2), counter) + + // Check logs + assert.Contains(t, ts.logBuffer.String(), "Stopped scheduler task") + assert.Contains(t, ts.logBuffer.String(), `"task.group":"default"`) + }) + + t.Run("More opts", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + var counter int32 + + exec := func(ctx context.Context) error { + atomic.AddInt32(&counter, 1) + return nil + } + + // ACT + // Register task and stop it after x1.5 interval. + ts.scheduler.Register( + ts.ctx, + exec, + Name("counter-inc"), + GroupName("my-custom-group"), + Interval(300*time.Millisecond), + LogFields(map[string]any{ + "blockchain": "doge", + "validators": []string{"alice", "bob"}, + }), + ) + + time.Sleep(time.Second) + ts.scheduler.Stop() + + // ASSERT + // Counter should be 1 + 1000/300 = 4 (first run + interval runs) + assert.Equal(t, int32(4), counter) + + // Also check that log fields are present + assert.Contains(t, ts.logBuffer.String(), `"task.name":"counter-inc","task.group":"my-custom-group"`) + assert.Contains(t, ts.logBuffer.String(), `"blockchain":"doge","validators":["alice","bob"]`) + }) + + t.Run("Task can stop itself", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + var counter int32 + + exec := func(ctx context.Context) error { + atomic.AddInt32(&counter, 1) + return nil + } + + // ACT + // Register task and stop it after x1.5 interval. + task := ts.scheduler.Register(ts.ctx, exec, Interval(300*time.Millisecond)) + + time.Sleep(time.Second) + task.Stop() + + // ASSERT + // Counter should be 1 + 1000/300 = 4 (first run + interval runs) + assert.Equal(t, int32(4), counter) + }) + + t.Run("Skipper option", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + var counter int32 + + exec := func(ctx context.Context) error { + atomic.AddInt32(&counter, 1) + return nil + } + + const maxValue = 5 + + // Skipper function that drops the task after reaching a certain counter value. + skipper := func() bool { + allowed := atomic.LoadInt32(&counter) < maxValue + return !allowed + } + + // ACT + // Register task and stop it after x1.5 interval. + task := ts.scheduler.Register(ts.ctx, exec, Interval(50*time.Millisecond), Skipper(skipper)) + + time.Sleep(time.Second) + task.Stop() + + // ASSERT + assert.Equal(t, int32(maxValue), counter) + }) + + t.Run("IntervalUpdater option", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + var counter int32 + + exec := func(ctx context.Context) error { + atomic.AddInt32(&counter, 1) + return nil + } + + // Interval updater that increases the interval by 50ms on each counter increment. + intervalUpdater := func() time.Duration { + return time.Duration(atomic.LoadInt32(&counter)) * 50 * time.Millisecond + } + + // ACT + // Register task and stop it after x1.5 interval. + task := ts.scheduler.Register(ts.ctx, exec, Interval(time.Millisecond), IntervalUpdater(intervalUpdater)) + + time.Sleep(time.Second) + task.Stop() + + // ASSERT + assert.Equal(t, int32(6), counter) + + assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":1,"ticker.new_interval":50`) + assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":50,"ticker.new_interval":100`) + assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":100,"ticker.new_interval":150`) + assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":150,"ticker.new_interval":200`) + assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":200,"ticker.new_interval":250`) + assert.Contains(t, ts.logBuffer.String(), `"ticker.old_interval":250,"ticker.new_interval":300`) + }) + + t.Run("Multiple tasks in different groups", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + // Given multiple tasks + var counterA, counterB, counterC int32 + + // Two tasks for Alice + taskAliceA := func(ctx context.Context) error { + atomic.AddInt32(&counterA, 1) + time.Sleep(60 * time.Millisecond) + return nil + } + + taskAliceB := func(ctx context.Context) error { + atomic.AddInt32(&counterB, 1) + time.Sleep(70 * time.Millisecond) + return nil + } + + // One task for Bob + taskBobC := func(ctx context.Context) error { + atomic.AddInt32(&counterC, 1) + time.Sleep(80 * time.Millisecond) + return nil + } + + // ACT + // Register all tasks with different intervals and groups + ts.scheduler.Register(ts.ctx, taskAliceA, Interval(50*time.Millisecond), GroupName("alice"), Name("a")) + ts.scheduler.Register(ts.ctx, taskAliceB, Interval(100*time.Millisecond), GroupName("alice"), Name("b")) + ts.scheduler.Register(ts.ctx, taskBobC, Interval(200*time.Millisecond), GroupName("bob"), Name("c")) + + // Wait and then stop Alice's tasks + time.Sleep(time.Second) + ts.scheduler.StopGroup("alice") + + // ASSERT #1 + shutdownLogPattern := func(group, name string) string { + const pattern = `"task\.name":"%s","task\.group":"%s",.*"message":"Stopped scheduler task"` + return fmt.Sprintf(pattern, name, group) + } + + // Make sure Alice.A and Alice.B are stopped + assert.Regexp(t, shutdownLogPattern("alice", "a"), ts.logBuffer.String()) + assert.Regexp(t, shutdownLogPattern("alice", "b"), ts.logBuffer.String()) + + // But Bob.C is still running + assert.NotRegexp(t, shutdownLogPattern("bob", "c"), ts.logBuffer.String()) + + // ACT #2 + time.Sleep(200 * time.Millisecond) + ts.scheduler.StopGroup("bob") + + // ASSERT #2 + // Bob.C is not running + assert.Regexp(t, shutdownLogPattern("bob", "c"), ts.logBuffer.String()) + }) + + t.Run("Block tick: tick is faster than the block", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + // Given a task that increments a counter by block height + var counter int64 + + task := func(ctx context.Context) error { + // Note that ctx contains the block event + blockEvent, ok := BlockFromContext(ctx) + require.True(t, ok) + + atomic.AddInt64(&counter, blockEvent.Block.Height) + time.Sleep(100 * time.Millisecond) + return nil + } + + // Given block ticker + blockChan := ts.mockBlockChan(200*time.Millisecond, 0) + + // ACT + // Register block + ts.scheduler.Register(ts.ctx, task, BlockTicker(blockChan)) + time.Sleep(1200 * time.Millisecond) + ts.scheduler.Stop() + + // ASSERT + assert.Equal(t, int64(21), counter) + assert.Contains(t, ts.logBuffer.String(), "Stopped scheduler task") + assert.Contains(t, ts.logBuffer.String(), `"task.type":"block_ticker"`) + }) + + t.Run("Block tick: tick is slower than the block", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + // Given a task that increments a counter on start + // and then decrements before finish + var counter int64 + + exec := func(ctx context.Context) error { + _, ok := BlockFromContext(ctx) + require.True(t, ok) + + atomic.AddInt64(&counter, 1) + time.Sleep(256 * time.Millisecond) + atomic.AddInt64(&counter, -1) + return nil + } + + // Given block ticker + blockChan := ts.mockBlockChan(100*time.Millisecond, 0) + + // ACT + // Register block + ts.scheduler.Register(ts.ctx, exec, BlockTicker(blockChan)) + time.Sleep(1200 * time.Millisecond) + ts.scheduler.Stop() + + // ASSERT + // zero indicates that Stop() waits for current iteration to finish (graceful shutdown) + assert.Equal(t, int64(0), counter) + }) + + t.Run("Block tick: chan closes unexpectedly", func(t *testing.T) { + t.Parallel() + + // ARRANGE + ts := newTestSuite(t) + + // Given a task that increments a counter on start + // and then decrements before finish + var counter int64 + + exec := func(ctx context.Context) error { + _, ok := BlockFromContext(ctx) + require.True(t, ok) + + atomic.AddInt64(&counter, 1) + time.Sleep(200 * time.Millisecond) + atomic.AddInt64(&counter, -1) + return nil + } + + // Given block ticker that closes after 3 blocks + blockChan := ts.mockBlockChan(100*time.Millisecond, 3) + + // ACT + // Register block + ts.scheduler.Register(ts.ctx, exec, BlockTicker(blockChan), Name("block-tick")) + + // Wait for a while + time.Sleep(1000 * time.Millisecond) + + // Stop the scheduler. + // Note that actually the ticker is already stopped. + ts.scheduler.Stop() + + // ASSERT + // zero indicates that Stop() waits for current iteration to finish (graceful shutdown) + assert.Equal(t, int64(0), counter) + assert.Contains(t, ts.logBuffer.String(), "Block channel closed") + }) +} + +type testSuite struct { + ctx context.Context + scheduler *Scheduler + + logger zerolog.Logger + logBuffer *bytes.Buffer +} + +func newTestSuite(t *testing.T) *testSuite { + logBuffer := &bytes.Buffer{} + logger := zerolog.New(io.MultiWriter(zerolog.NewTestWriter(t), logBuffer)) + + return &testSuite{ + ctx: context.Background(), + scheduler: New(logger), + logger: logger, + logBuffer: logBuffer, + } +} + +// mockBlockChan mocks websocket blocks. Optionally halts after lastBlock. +func (ts *testSuite) mockBlockChan(interval time.Duration, lastBlock int64) chan cometbft.EventDataNewBlock { + producer := make(chan cometbft.EventDataNewBlock) + + go func() { + var blockNumber int64 + + for { + blockNumber++ + ts.logger.Info().Int64("block_number", blockNumber).Msg("Producing new block") + + header := cometbft.Header{ + ChainID: "zeta", + Height: blockNumber, + Time: time.Now(), + } + + producer <- cometbft.EventDataNewBlock{ + Block: &cometbft.Block{Header: header}, + } + + if blockNumber > 0 && blockNumber == lastBlock { + ts.logger.Info().Int64("block_number", blockNumber).Msg("Halting block producer") + close(producer) + return + } + + time.Sleep(interval) + } + }() + + return producer +} diff --git a/pkg/scheduler/tickers.go b/pkg/scheduler/tickers.go new file mode 100644 index 0000000000..613194c44b --- /dev/null +++ b/pkg/scheduler/tickers.go @@ -0,0 +1,172 @@ +package scheduler + +import ( + "context" + "fmt" + "sync" + "time" + + cometbft "github.com/cometbft/cometbft/types" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/pkg/ticker" +) + +// intervalTicker wrapper for ticker.Ticker. +type intervalTicker struct { + ticker *ticker.Ticker +} + +func newIntervalTicker( + task Executable, + interval time.Duration, + intervalUpdater func() time.Duration, + taskName string, + logger zerolog.Logger, +) *intervalTicker { + wrapper := func(ctx context.Context, t *ticker.Ticker) error { + if err := task(ctx); err != nil { + logger.Error().Err(err).Msg("task failed") + } + + if intervalUpdater != nil { + // noop if interval is not changed + t.SetInterval(intervalUpdater()) + } + + return nil + } + + tt := ticker.New(interval, wrapper, ticker.WithLogger(logger, taskName)) + + return &intervalTicker{ticker: tt} +} + +func (t *intervalTicker) Start(ctx context.Context) error { + return t.ticker.Start(ctx) +} + +func (t *intervalTicker) Stop() { + t.ticker.StopBlocking() +} + +// blockTicker represents custom ticker implementation that ticks on new Zeta block events. +// Pass blockTicker ONLY by pointer. +type blockTicker struct { + exec Executable + + // block channel that will be used to receive new blocks + blockChan <-chan cometbft.EventDataNewBlock + + // stopChan is used to stop the ticker + stopChan chan struct{} + + // doneChan is used to signal that the ticker has stopped (i.e. "blocking stop") + doneChan chan struct{} + + isRunning bool + mu sync.Mutex + + logger zerolog.Logger +} + +type blockCtxKey struct{} + +func newBlockTicker(task Executable, blockChan <-chan cometbft.EventDataNewBlock, logger zerolog.Logger) *blockTicker { + return &blockTicker{ + exec: task, + blockChan: blockChan, + logger: logger, + } +} + +func withBlockEvent(ctx context.Context, event cometbft.EventDataNewBlock) context.Context { + return context.WithValue(ctx, blockCtxKey{}, event) +} + +// BlockFromContext returns cometbft.EventDataNewBlock from the context or false. +func BlockFromContext(ctx context.Context) (cometbft.EventDataNewBlock, bool) { + blockEvent, ok := ctx.Value(blockCtxKey{}).(cometbft.EventDataNewBlock) + return blockEvent, ok +} + +func (t *blockTicker) Start(ctx context.Context) error { + if err := t.init(); err != nil { + return err + } + + defer t.cleanup() + + // release Stop() blocking + defer func() { close(t.doneChan) }() + + for { + select { + case block, ok := <-t.blockChan: + // channel closed + if !ok { + t.logger.Warn().Msg("Block channel closed") + return nil + } + + ctx := withBlockEvent(ctx, block) + + if err := t.exec(ctx); err != nil { + t.logger.Error().Err(err).Msg("Task error") + } + case <-ctx.Done(): + t.logger.Warn().Err(ctx.Err()).Msg("Content error") + return nil + case <-t.stopChan: + // caller invoked t.stop() + return nil + } + } +} + +func (t *blockTicker) Stop() { + t.mu.Lock() + defer t.mu.Unlock() + + // noop + if !t.isRunning { + return + } + + // notify async loop to stop + close(t.stopChan) + + // wait for the loop to stop + <-t.doneChan + + t.isRunning = false +} + +func (t *blockTicker) init() error { + t.mu.Lock() + defer t.mu.Unlock() + + if t.isRunning { + return fmt.Errorf("ticker already started") + } + + t.stopChan = make(chan struct{}) + t.doneChan = make(chan struct{}) + t.isRunning = true + + return nil +} + +// if ticker was stopped NOT by Stop() method, we want to make a cleanup +func (t *blockTicker) cleanup() { + t.mu.Lock() + defer t.mu.Unlock() + + // noop + if !t.isRunning { + return + } + + t.isRunning = false + close(t.stopChan) +} diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index ebac3e1a96..9ec0d4cb06 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -56,6 +56,7 @@ type Ticker struct { stopped bool ctxCancel context.CancelFunc + runCompleteChan chan struct{} externalStopChan <-chan struct{} logger zerolog.Logger } @@ -94,7 +95,7 @@ func New(interval time.Duration, task Task, opts ...Opt) *Ticker { // Run creates and runs a new Ticker. func Run(ctx context.Context, interval time.Duration, task Task, opts ...Opt) error { - return New(interval, task, opts...).Run(ctx) + return New(interval, task, opts...).Start(ctx) } // Run runs the ticker by blocking current goroutine. It also invokes BEFORE ticker starts. @@ -102,8 +103,17 @@ func Run(ctx context.Context, interval time.Duration, task Task, opts ...Opt) er // - context is done (returns ctx.Err()) // - task returns an error or panics // - shutdown signal is received -func (t *Ticker) Run(ctx context.Context) (err error) { +func (t *Ticker) Start(ctx context.Context) (err error) { + // prevent concurrent runs + t.runnerMu.Lock() + defer t.runnerMu.Unlock() + + ctx = t.setStartState(ctx) + defer func() { + // used in StopBlocking() + close(t.runCompleteChan) + if r := recover(); r != nil { stack := string(debug.Stack()) lines := strings.Split(stack, "\n") @@ -116,21 +126,14 @@ func (t *Ticker) Run(ctx context.Context) (err error) { } }() - // prevent concurrent runs - t.runnerMu.Lock() - defer t.runnerMu.Unlock() - - // setup - ctx, t.ctxCancel = context.WithCancel(ctx) - t.ticker = time.NewTicker(t.interval) - t.stopped = false - // initial run - if err := t.task(ctx, t); err != nil { + if err = t.task(ctx, t); err != nil { t.Stop() return fmt.Errorf("ticker task failed (initial run): %w", err) } + defer t.setStopState() + for { select { case <-ctx.Done(): @@ -172,8 +175,35 @@ func (t *Ticker) SetInterval(interval time.Duration) { t.ticker.Reset(interval) } -// Stop stops the ticker. Safe to call concurrently or multiple times. +// Stop stops the ticker in a NON-blocking way. If the task is running in a separate goroutine, +// this call *might* not wait for it to finish. To wait for task finish, use StopBlocking(). +// It's safe to call Stop() multiple times / concurrently / within the task. func (t *Ticker) Stop() { + t.setStopState() +} + +// StopBlocking stops the ticker in a blocking way i.e. it waits for the task to finish. +// DO NOT call this within the task. +func (t *Ticker) StopBlocking() { + t.setStopState() + <-t.runCompleteChan +} + +func (t *Ticker) setStartState(ctx context.Context) context.Context { + t.stateMu.Lock() + defer t.stateMu.Unlock() + + ctx, t.ctxCancel = context.WithCancel(ctx) + t.ticker = time.NewTicker(t.interval) + t.stopped = false + + // this signals that Run() is about to return + t.runCompleteChan = make(chan struct{}) + + return ctx +} + +func (t *Ticker) setStopState() { t.stateMu.Lock() defer t.stateMu.Unlock() diff --git a/pkg/ticker/ticker_test.go b/pkg/ticker/ticker_test.go index 4e03a28d4f..276eb9457f 100644 --- a/pkg/ticker/ticker_test.go +++ b/pkg/ticker/ticker_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "sync/atomic" "testing" "time" @@ -19,6 +20,8 @@ func TestTicker(t *testing.T) { ) t.Run("Basic case with context", func(t *testing.T) { + t.Parallel() + // ARRANGE // Given a counter var counter int @@ -35,7 +38,7 @@ func TestTicker(t *testing.T) { }) // ACT - err := ticker.Run(ctx) + err := ticker.Start(ctx) // ASSERT assert.ErrorIs(t, err, context.DeadlineExceeded) @@ -45,6 +48,8 @@ func TestTicker(t *testing.T) { }) t.Run("Halts when error occurred", func(t *testing.T) { + t.Parallel() + // ARRANGE // Given a counter var counter int @@ -62,7 +67,7 @@ func TestTicker(t *testing.T) { }) // ACT - err := ticker.Run(ctx) + err := ticker.Start(ctx) // ASSERT assert.ErrorContains(t, err, "oops") @@ -70,6 +75,8 @@ func TestTicker(t *testing.T) { }) t.Run("Dynamic interval update", func(t *testing.T) { + t.Parallel() + // ARRANGE // Given a counter var counter int @@ -93,7 +100,7 @@ func TestTicker(t *testing.T) { }) // ACT - err := ticker.Run(ctx) + err := ticker.Start(ctx) // ASSERT assert.ErrorIs(t, err, context.DeadlineExceeded) @@ -104,6 +111,8 @@ func TestTicker(t *testing.T) { }) t.Run("Stop ticker", func(t *testing.T) { + t.Parallel() + // ARRANGE // Given a counter var counter int @@ -124,7 +133,7 @@ func TestTicker(t *testing.T) { }() // ACT - err := ticker.Run(ctx) + err := ticker.Start(ctx) // ASSERT assert.NoError(t, err) @@ -135,7 +144,96 @@ func TestTicker(t *testing.T) { }) }) + t.Run("Stop ticker in a blocking fashion", func(t *testing.T) { + t.Parallel() + + const ( + tickerInterval = 100 * time.Millisecond + workDuration = 600 * time.Millisecond + stopAfterStart = workDuration + tickerInterval/2 + ) + + newLogger := func(t *testing.T) zerolog.Logger { + return zerolog.New(zerolog.NewTestWriter(t)).With().Timestamp().Logger() + } + + // test task that imitates some work + newTask := func(counter *int32, logger zerolog.Logger) Task { + return func(ctx context.Context, _ *Ticker) error { + logger.Info().Msg("Tick start") + atomic.AddInt32(counter, 1) + + time.Sleep(workDuration) + + logger.Info().Msgf("Tick end") + atomic.AddInt32(counter, -1) + + return nil + } + } + + t.Run("Non-blocking stop fails do finish the work", func(t *testing.T) { + t.Parallel() + + // ARRANGE + // Given some test task that imitates some work + testLogger := newLogger(t) + counter := int32(0) + task := newTask(&counter, testLogger) + + // Given a ticker + ticker := New(tickerInterval, task, WithLogger(testLogger, "test-non-blocking-ticker")) + + // ACT + // Imitate the ticker run in the background + go func() { + err := ticker.Start(context.Background()) + require.NoError(t, err) + }() + + // Then stop the ticker after some delay + time.Sleep(stopAfterStart) + testLogger.Info().Msg("Stopping ticker") + ticker.Stop() + testLogger.Info().Msg("Stopped ticker") + + // ASSERT + // If ticker is stopped BEFORE the work is done i.e. "in the middle of work", + // thus the counter would be `1. You can also check the logs + assert.Equal(t, int32(1), counter) + }) + + t.Run("Blocking stop works as expected", func(t *testing.T) { + t.Parallel() + + // ARRANGE + // Now if we have the SAME test but with blocking stop, it should work + testLogger := newLogger(t) + counter := int32(0) + task := newTask(&counter, testLogger) + + ticker := New(tickerInterval, task, WithLogger(testLogger, "test-non-blocking-ticker")) + + // ACT + go func() { + err := ticker.Start(context.Background()) + require.NoError(t, err) + }() + + time.Sleep(stopAfterStart) + testLogger.Info().Msg("Stopping ticker") + ticker.StopBlocking() + testLogger.Info().Msg("Stopped ticker") + + // ASSERT + // If ticker is stopped AFTER the work is done + assert.Equal(t, int32(0), counter) + }) + }) + t.Run("Panic", func(t *testing.T) { + t.Parallel() + // ARRANGE // Given a context ctx := context.Background() @@ -146,15 +244,17 @@ func TestTicker(t *testing.T) { }) // ACT - err := ticker.Run(ctx) + err := ticker.Start(ctx) // ASSERT assert.ErrorContains(t, err, "panic during ticker run: oops") // assert that we get error with the correct line number - assert.ErrorContains(t, err, "ticker_test.go:145") + assert.ErrorContains(t, err, "ticker_test.go:243") }) t.Run("Nil panic", func(t *testing.T) { + t.Parallel() + // ARRANGE // Given a context ctx := context.Background() @@ -167,7 +267,7 @@ func TestTicker(t *testing.T) { }) // ACT - err := ticker.Run(ctx) + err := ticker.Start(ctx) // ASSERT assert.ErrorContains( @@ -176,10 +276,12 @@ func TestTicker(t *testing.T) { "panic during ticker run: runtime error: invalid memory address or nil pointer dereference", ) // assert that we get error with the correct line number - assert.ErrorContains(t, err, "ticker_test.go:165") + assert.ErrorContains(t, err, "ticker_test.go:265") }) t.Run("Run as a single call", func(t *testing.T) { + t.Parallel() + // ARRANGE // Given a counter var counter int @@ -202,6 +304,8 @@ func TestTicker(t *testing.T) { }) t.Run("With stop channel", func(t *testing.T) { + t.Parallel() + // ARRANGE var ( tickerInterval = 100 * time.Millisecond @@ -232,6 +336,8 @@ func TestTicker(t *testing.T) { }) t.Run("With logger", func(t *testing.T) { + t.Parallel() + // ARRANGE out := &bytes.Buffer{} logger := zerolog.New(out) diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index 28fe897504..36dc5ad813 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -170,6 +170,27 @@ var ( Name: "num_connected_peers", Help: "The number of connected peers (authenticated keygen peers)", }) + + // SchedulerTaskInvocationCounter tracks invocations categorized by status, group, and name + SchedulerTaskInvocationCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: ZetaClientNamespace, + Name: "scheduler_task_invocations_total", + Help: "Total number of task invocations", + }, + []string{"status", "task_group", "task_name"}, + ) + + // SchedulerTaskExecutionDuration measures the execution duration of tasks + SchedulerTaskExecutionDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: ZetaClientNamespace, + Name: "scheduler_task_duration_seconds", + Help: "Histogram of task execution duration in seconds", + Buckets: []float64{0.05, 0.1, 0.2, 0.3, 0.5, 1, 1.5, 2, 3, 5, 7.5, 10, 15}, // 50ms to 15s + }, + []string{"status", "task_group", "task_name"}, + ) ) // NewMetrics creates a new Metrics instance From f0c42674097786c12d0341b44b215ba12d41fa1b Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 30 Dec 2024 16:42:54 -0800 Subject: [PATCH 03/10] feat: add configurable signer latency correction (#3317) * feat: add configurable signer latency correction * coderabbit feedback * changelog --- changelog.md | 2 + cmd/zetae2e/local/local.go | 3 +- docs/cli/zetacored/cli.md | 57 +++--- docs/openapi/openapi.swagger.yaml | 5 + docs/spec/observer/messages.md | 2 +- e2e/e2etests/e2etests.go | 15 +- e2e/e2etests/test_operational_flags.go | 51 +++++- .../zetacore/observer/operational.proto | 8 + proto/zetachain/zetacore/observer/tx.proto | 2 +- testutil/sample/observer.go | 5 +- .../zetacore/observer/operational_pb.d.ts | 10 +- .../zetachain/zetacore/observer/tx_pb.d.ts | 2 +- .../client/cli/tx_update_operational_flags.go | 8 +- ...sg_server_update_operational_flags_test.go | 12 +- x/observer/types/errors.go | 22 ++- .../message_update_operational_flags_test.go | 16 +- x/observer/types/operational.go | 19 ++ x/observer/types/operational.pb.go | 97 ++++++++-- x/observer/types/operational_test.go | 33 +++- x/observer/types/tx.pb.go | 172 +++++++++--------- zetaclient/metrics/metrics.go | 7 + zetaclient/orchestrator/orchestrator.go | 61 +++++++ zetaclient/orchestrator/orchestrator_test.go | 54 ++++++ 23 files changed, 514 insertions(+), 149 deletions(-) diff --git a/changelog.md b/changelog.md index 8060dbd540..72d1ea2c59 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,8 @@ ### Features * [3235](https://github.com/zeta-chain/node/pull/3235) - add /systemtime telemetry endpoint (zetaclient) +* [3317](https://github.com/zeta-chain/node/pull/3317) - add configurable signer latency correction (zetaclient) + ### Tests diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 4385101a24..65b5fdcf44 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -362,7 +362,8 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if testAdmin { eg.Go(adminTestRoutine(conf, deployerRunner, verbose, - e2etests.TestOperationalFlagsName, + e2etests.TestZetaclientSignerOffsetName, + e2etests.TestZetaclientRestartHeightName, e2etests.TestWhitelistERC20Name, e2etests.TestPauseZRC20Name, e2etests.TestUpdateBytecodeZRC20Name, diff --git a/docs/cli/zetacored/cli.md b/docs/cli/zetacored/cli.md index 56e293efe4..5f6d963e6f 100644 --- a/docs/cli/zetacored/cli.md +++ b/docs/cli/zetacored/cli.md @@ -13347,34 +13347,35 @@ zetacored tx observer update-operational-flags [flags] ### Options ``` - -a, --account-number uint The account number of the signing account (offline mode only) - --aux Generate aux signer data instead of sending a tx - -b, --broadcast-mode string Transaction broadcasting mode (sync|async) - --chain-id string The network chain ID - --dry-run ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it (when enabled, the local Keybase is not accessible) - --fee-granter string Fee granter grants fees for the transaction - --fee-payer string Fee payer pays fees for the transaction instead of deducting from the signer - --fees string Fees to pay along with transaction; eg: 10uatom - --file string Path to a JSON file containing OperationalFlags - --from string Name or address of private key with which to sign - --gas string gas limit to set per-transaction; set to "auto" to calculate sufficient gas automatically. Note: "auto" option doesn't always report accurate results. Set a valid coin value to adjust the result. Can be used instead of "fees". (default 200000) - --gas-adjustment float adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored (default 1) - --gas-prices string Gas prices in decimal format to determine the transaction fee (e.g. 0.1uatom) - --generate-only Build an unsigned transaction and write it to STDOUT (when enabled, the local Keybase only accessed when providing a key name) - -h, --help help for update-operational-flags - --keyring-backend string Select keyring's backend (os|file|kwallet|pass|test|memory) - --keyring-dir string The client Keyring directory; if omitted, the default 'home' directory will be used - --ledger Use a connected Ledger device - --node string [host]:[port] to tendermint rpc interface for this chain - --note string Note to add a description to the transaction (previously --memo) - --offline Offline mode (does not allow any online functionality) - -o, --output string Output format (text|json) - --restart-height int Height for a coordinated zetaclient restart - -s, --sequence uint The sequence number of the signing account (offline mode only) - --sign-mode string Choose sign mode (direct|amino-json|direct-aux), this is an advanced feature - --timeout-height uint Set a block timeout height to prevent the tx from being committed past a certain height - --tip string Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux, and is ignored if the target chain didn't enable the TipDecorator - -y, --yes Skip tx broadcasting prompt confirmation + -a, --account-number uint The account number of the signing account (offline mode only) + --aux Generate aux signer data instead of sending a tx + -b, --broadcast-mode string Transaction broadcasting mode (sync|async) + --chain-id string The network chain ID + --dry-run ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it (when enabled, the local Keybase is not accessible) + --fee-granter string Fee granter grants fees for the transaction + --fee-payer string Fee payer pays fees for the transaction instead of deducting from the signer + --fees string Fees to pay along with transaction; eg: 10uatom + --file string Path to a JSON file containing OperationalFlags + --from string Name or address of private key with which to sign + --gas string gas limit to set per-transaction; set to "auto" to calculate sufficient gas automatically. Note: "auto" option doesn't always report accurate results. Set a valid coin value to adjust the result. Can be used instead of "fees". (default 200000) + --gas-adjustment float adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored (default 1) + --gas-prices string Gas prices in decimal format to determine the transaction fee (e.g. 0.1uatom) + --generate-only Build an unsigned transaction and write it to STDOUT (when enabled, the local Keybase only accessed when providing a key name) + -h, --help help for update-operational-flags + --keyring-backend string Select keyring's backend (os|file|kwallet|pass|test|memory) + --keyring-dir string The client Keyring directory; if omitted, the default 'home' directory will be used + --ledger Use a connected Ledger device + --node string [host]:[port] to tendermint rpc interface for this chain + --note string Note to add a description to the transaction (previously --memo) + --offline Offline mode (does not allow any online functionality) + -o, --output string Output format (text|json) + --restart-height int Height for a coordinated zetaclient restart + -s, --sequence uint The sequence number of the signing account (offline mode only) + --sign-mode string Choose sign mode (direct|amino-json|direct-aux), this is an advanced feature + --signer-block-time-offset duration Offset from the zetacore block time to initiate signing + --timeout-height uint Set a block timeout height to prevent the tx from being committed past a certain height + --tip string Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux, and is ignored if the target chain didn't enable the TipDecorator + -y, --yes Skip tx broadcasting prompt confirmation ``` ### Options inherited from parent commands diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index 65f305da0f..694f839db6 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -58110,6 +58110,11 @@ definitions: description: |- Height for a coordinated zetaclient restart. Will be ignored if missed. + signer_block_time_offset: + type: string + description: |- + Offset from the zetacore block time to initiate signing. + Should be calculated and set based on max(zetaclient_core_block_latency). description: Flags for the top-level operation of zetaclient. observerPendingNonces: type: object diff --git a/docs/spec/observer/messages.md b/docs/spec/observer/messages.md index acceee29f2..0e194bed85 100644 --- a/docs/spec/observer/messages.md +++ b/docs/spec/observer/messages.md @@ -169,7 +169,7 @@ message MsgUpdateGasPriceIncreaseFlags { ```proto message MsgUpdateOperationalFlags { string creator = 1; - OperationalFlags operationalFlags = 2; + OperationalFlags operational_flags = 2; } ``` diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index cabc1e394f..1b9f3ef419 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -132,7 +132,8 @@ const ( TestMigrateERC20CustodyFundsName = "migrate_erc20_custody_funds" TestMigrateTSSName = "migrate_tss" TestSolanaWhitelistSPLName = "solana_whitelist_spl" - TestOperationalFlagsName = "operational_flags" + TestZetaclientRestartHeightName = "zetaclient_restart_height" + TestZetaclientSignerOffsetName = "zetaclient_signer_offset" /* Operational tests @@ -880,10 +881,16 @@ var AllE2ETests = []runner.E2ETest{ TestMigrateERC20CustodyFunds, ), runner.NewE2ETest( - TestOperationalFlagsName, - "operational flags functionality", + TestZetaclientRestartHeightName, + "zetaclient scheduled restart height", []runner.ArgDefinition{}, - TestOperationalFlags, + TestZetaclientRestartHeight, + ), + runner.NewE2ETest( + TestZetaclientSignerOffsetName, + "zetaclient signer offset", + []runner.ArgDefinition{}, + TestZetaclientSignerOffset, ), /* Special tests diff --git a/e2e/e2etests/test_operational_flags.go b/e2e/e2etests/test_operational_flags.go index 5084c38b58..0a3d0ee38c 100644 --- a/e2e/e2etests/test_operational_flags.go +++ b/e2e/e2etests/test_operational_flags.go @@ -11,11 +11,13 @@ import ( ) const ( - startTimestampMetricName = "zetaclient_last_start_timestamp_seconds" + startTimestampMetricName = "zetaclient_last_start_timestamp_seconds" + blockTimeLatencyMetricName = "zetaclient_core_block_latency" + blockTimeLatencySleepMetricName = "zetaclient_core_block_latency_sleep" ) -// TestOperationalFlags tests the functionality of operations flags. -func TestOperationalFlags(r *runner.E2ERunner, _ []string) { +// TestZetaclientRestartHeight tests scheduling a zetaclient restart via operational flags +func TestZetaclientRestartHeight(r *runner.E2ERunner, _ []string) { _, err := r.Clients.Zetacore.Observer.OperationalFlags( r.Ctx, &observertypes.QueryOperationalFlagsRequest{}, @@ -60,3 +62,46 @@ func TestOperationalFlags(r *runner.E2ERunner, _ []string) { require.Greater(r, currentStartTime, originalStartTime+1) } + +// TestZetaclientSignerOffset tests scheduling a zetaclient restart via operational flags +func TestZetaclientSignerOffset(r *runner.E2ERunner, _ []string) { + startBlockTimeLatencySleep, err := r.Clients.ZetaclientMetrics.FetchGauge(blockTimeLatencySleepMetricName) + require.NoError(r, err) + require.InDelta(r, 0, startBlockTimeLatencySleep, .01, "start block time latency should be 0") + + // get starting block time latency. + // we need to ensure it's not zero (if zetaclient just finished a restart) + var startBlockTimeLatency float64 + require.Eventually(r, func() bool { + startBlockTimeLatency, err = r.Clients.ZetaclientMetrics.FetchGauge(blockTimeLatencyMetricName) + require.NoError(r, err) + return startBlockTimeLatency > 1 + }, time.Second*15, time.Millisecond*100) + + desiredSignerBlockTimeOffset := time.Duration(startBlockTimeLatency*float64(time.Second)) + time.Millisecond*200 + + updateMsg := observertypes.NewMsgUpdateOperationalFlags( + r.ZetaTxServer.MustGetAccountAddressFromName(utils.OperationalPolicyName), + observertypes.OperationalFlags{ + SignerBlockTimeOffset: &desiredSignerBlockTimeOffset, + }, + ) + + _, err = r.ZetaTxServer.BroadcastTx(utils.OperationalPolicyName, updateMsg) + require.NoError(r, err) + + operationalFlagsRes, err := r.Clients.Zetacore.Observer.OperationalFlags( + r.Ctx, + &observertypes.QueryOperationalFlagsRequest{}, + ) + require.NoError(r, err) + require.InDelta(r, desiredSignerBlockTimeOffset, *(operationalFlagsRes.OperationalFlags.SignerBlockTimeOffset), .01) + + require.Eventually(r, func() bool { + blockTimeLatencySleep, err := r.Clients.ZetaclientMetrics.FetchGauge(blockTimeLatencySleepMetricName) + if err != nil { + return false + } + return blockTimeLatencySleep > .05 + }, time.Second*20, time.Second*1) +} diff --git a/proto/zetachain/zetacore/observer/operational.proto b/proto/zetachain/zetacore/observer/operational.proto index f94c7e7d11..62ff737074 100644 --- a/proto/zetachain/zetacore/observer/operational.proto +++ b/proto/zetachain/zetacore/observer/operational.proto @@ -1,6 +1,9 @@ syntax = "proto3"; package zetachain.zetacore.observer; +import "gogoproto/gogo.proto"; +import "google/protobuf/duration.proto"; + option go_package = "github.com/zeta-chain/node/x/observer/types"; // Flags for the top-level operation of zetaclient. @@ -8,4 +11,9 @@ message OperationalFlags { // Height for a coordinated zetaclient restart. // Will be ignored if missed. int64 restart_height = 1; + + // Offset from the zetacore block time to initiate signing. + // Should be calculated and set based on max(zetaclient_core_block_latency). + google.protobuf.Duration signer_block_time_offset = 2 + [ (gogoproto.stdduration) = true ]; } \ No newline at end of file diff --git a/proto/zetachain/zetacore/observer/tx.proto b/proto/zetachain/zetacore/observer/tx.proto index 0d4ecf275e..b3e1c1ab97 100644 --- a/proto/zetachain/zetacore/observer/tx.proto +++ b/proto/zetachain/zetacore/observer/tx.proto @@ -143,7 +143,7 @@ message MsgUpdateGasPriceIncreaseFlagsResponse {} message MsgUpdateOperationalFlags { string creator = 1; - OperationalFlags operationalFlags = 2 [ (gogoproto.nullable) = false ]; + OperationalFlags operational_flags = 2 [ (gogoproto.nullable) = false ]; } message MsgUpdateOperationalFlagsResponse {} diff --git a/testutil/sample/observer.go b/testutil/sample/observer.go index aa8e0d82c4..1a0ed8f2ba 100644 --- a/testutil/sample/observer.go +++ b/testutil/sample/observer.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "testing" + "time" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/cosmos/cosmos-sdk/testutil/testdata" @@ -14,6 +15,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/cosmos" zetacrypto "github.com/zeta-chain/node/pkg/crypto" + "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/x/observer/types" ) @@ -287,6 +289,7 @@ func GasPriceIncreaseFlags() types.GasPriceIncreaseFlags { func OperationalFlags() types.OperationalFlags { return types.OperationalFlags{ - RestartHeight: 1, + RestartHeight: 1, + SignerBlockTimeOffset: ptr.Ptr(time.Second), } } diff --git a/typescript/zetachain/zetacore/observer/operational_pb.d.ts b/typescript/zetachain/zetacore/observer/operational_pb.d.ts index 5bae51a4e0..c9fc213927 100644 --- a/typescript/zetachain/zetacore/observer/operational_pb.d.ts +++ b/typescript/zetachain/zetacore/observer/operational_pb.d.ts @@ -3,7 +3,7 @@ /* eslint-disable */ // @ts-nocheck -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; +import type { BinaryReadOptions, Duration, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; import { Message, proto3 } from "@bufbuild/protobuf"; /** @@ -20,6 +20,14 @@ export declare class OperationalFlags extends Message { */ restartHeight: bigint; + /** + * Offset from the zetacore block time to initiate signing. + * Should be calculated and set based on max(zetaclient_core_block_latency). + * + * @generated from field: google.protobuf.Duration signer_block_time_offset = 2; + */ + signerBlockTimeOffset?: Duration; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; diff --git a/typescript/zetachain/zetacore/observer/tx_pb.d.ts b/typescript/zetachain/zetacore/observer/tx_pb.d.ts index 9afe867fdf..40ea5f9e8f 100644 --- a/typescript/zetachain/zetacore/observer/tx_pb.d.ts +++ b/typescript/zetachain/zetacore/observer/tx_pb.d.ts @@ -694,7 +694,7 @@ export declare class MsgUpdateOperationalFlags extends Message signerBlockTimeOffsetLimit { + return cosmoserrors.Wrapf(ErrOperationalFlagsSignerBlockTimeOffsetLimit, "(%s)", signerBlockTimeOffset) + } + } return nil } diff --git a/x/observer/types/operational.pb.go b/x/observer/types/operational.pb.go index b7af5711cf..e5fb9c3281 100644 --- a/x/observer/types/operational.pb.go +++ b/x/observer/types/operational.pb.go @@ -5,16 +5,21 @@ package types import ( fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" + github_com_cosmos_gogoproto_types "github.com/cosmos/gogoproto/types" + _ "google.golang.org/protobuf/types/known/durationpb" io "io" math "math" math_bits "math/bits" + time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf +var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. @@ -27,6 +32,9 @@ type OperationalFlags struct { // Height for a coordinated zetaclient restart. // Will be ignored if missed. RestartHeight int64 `protobuf:"varint,1,opt,name=restart_height,json=restartHeight,proto3" json:"restart_height,omitempty"` + // Offset from the zetacore block time to initiate signing. + // Should be calculated and set based on max(zetaclient_core_block_latency). + SignerBlockTimeOffset *time.Duration `protobuf:"bytes,2,opt,name=signer_block_time_offset,json=signerBlockTimeOffset,proto3,stdduration" json:"signer_block_time_offset,omitempty"` } func (m *OperationalFlags) Reset() { *m = OperationalFlags{} } @@ -69,6 +77,13 @@ func (m *OperationalFlags) GetRestartHeight() int64 { return 0 } +func (m *OperationalFlags) GetSignerBlockTimeOffset() *time.Duration { + if m != nil { + return m.SignerBlockTimeOffset + } + return nil +} + func init() { proto.RegisterType((*OperationalFlags)(nil), "zetachain.zetacore.observer.OperationalFlags") } @@ -78,19 +93,25 @@ func init() { } var fileDescriptor_ea3eed2ec55093b5 = []byte{ - // 184 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xad, 0x4a, 0x2d, 0x49, - 0x4c, 0xce, 0x48, 0xcc, 0xcc, 0xd3, 0x07, 0xb3, 0xf2, 0x8b, 0x52, 0xf5, 0xf3, 0x93, 0x8a, 0x53, - 0x8b, 0xca, 0x52, 0x8b, 0xf4, 0xf3, 0x0b, 0x52, 0x8b, 0x12, 0x4b, 0x32, 0xf3, 0xf3, 0x12, 0x73, - 0xf4, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x85, 0xa4, 0xe1, 0xca, 0xf5, 0x60, 0xca, 0xf5, 0x60, 0xca, - 0x95, 0x2c, 0xb9, 0x04, 0xfc, 0x11, 0x3a, 0xdc, 0x72, 0x12, 0xd3, 0x8b, 0x85, 0x54, 0xb9, 0xf8, - 0x8a, 0x52, 0x8b, 0x4b, 0x12, 0x8b, 0x4a, 0xe2, 0x33, 0x52, 0x33, 0xd3, 0x33, 0x4a, 0x24, 0x18, - 0x15, 0x18, 0x35, 0x98, 0x83, 0x78, 0xa1, 0xa2, 0x1e, 0x60, 0x41, 0x27, 0xd7, 0x13, 0x8f, 0xe4, - 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, - 0xe5, 0x18, 0x6e, 0x3c, 0x96, 0x63, 0x88, 0xd2, 0x4e, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, - 0xce, 0xcf, 0x05, 0xbb, 0x50, 0x17, 0xe2, 0xd8, 0xbc, 0xfc, 0x94, 0x54, 0xfd, 0x0a, 0x84, 0x53, - 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0xae, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, - 0xcf, 0xd8, 0x7d, 0x54, 0xd6, 0x00, 0x00, 0x00, + // 282 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0xc1, 0x4a, 0xec, 0x30, + 0x14, 0x86, 0x27, 0xf7, 0x8a, 0x8b, 0x8a, 0x22, 0x83, 0x42, 0x1d, 0x21, 0x0e, 0x82, 0x30, 0x20, + 0x93, 0x80, 0xbe, 0x41, 0x51, 0x71, 0x37, 0x30, 0xb8, 0x10, 0x37, 0x25, 0xed, 0x9c, 0xa6, 0xc1, + 0xb6, 0xa7, 0x24, 0xa9, 0xa8, 0x4f, 0x21, 0xb8, 0xf1, 0x91, 0x5c, 0xce, 0xd2, 0x9d, 0xd2, 0xbe, + 0x88, 0x4c, 0x32, 0x75, 0x76, 0x27, 0x27, 0xdf, 0x7f, 0x3e, 0xf8, 0x83, 0xe9, 0x2b, 0x58, 0x91, + 0xe6, 0x42, 0x55, 0xdc, 0x4d, 0xa8, 0x81, 0x63, 0x62, 0x40, 0x3f, 0x81, 0xe6, 0x58, 0x83, 0x16, + 0x56, 0x61, 0x25, 0x0a, 0x56, 0x6b, 0xb4, 0x38, 0x3c, 0xfe, 0xc3, 0x59, 0x8f, 0xb3, 0x1e, 0x1f, + 0x1d, 0x48, 0x94, 0xe8, 0x38, 0xbe, 0x9a, 0x7c, 0x64, 0x44, 0x25, 0xa2, 0x2c, 0x80, 0xbb, 0x57, + 0xd2, 0x64, 0x7c, 0xd1, 0xf8, 0xa3, 0xfe, 0xff, 0xf4, 0x9d, 0x04, 0xfb, 0xb3, 0x8d, 0xe8, 0xa6, + 0x10, 0xd2, 0x0c, 0xcf, 0x82, 0x3d, 0x0d, 0xc6, 0x0a, 0x6d, 0xe3, 0x1c, 0x94, 0xcc, 0x6d, 0x48, + 0xc6, 0x64, 0xf2, 0x7f, 0xbe, 0xbb, 0xde, 0xde, 0xba, 0xe5, 0xf0, 0x3e, 0x08, 0x8d, 0x92, 0x15, + 0xe8, 0x38, 0x29, 0x30, 0x7d, 0x8c, 0xad, 0x2a, 0x21, 0xc6, 0x2c, 0x33, 0x60, 0xc3, 0x7f, 0x63, + 0x32, 0xd9, 0xb9, 0x38, 0x62, 0x5e, 0xcf, 0x7a, 0x3d, 0xbb, 0x5a, 0xeb, 0xa3, 0xad, 0x8f, 0xef, + 0x13, 0x32, 0x3f, 0xf4, 0x07, 0xa2, 0x55, 0xfe, 0x4e, 0x95, 0x30, 0x73, 0xe9, 0xe8, 0xfa, 0xb3, + 0xa5, 0x64, 0xd9, 0x52, 0xf2, 0xd3, 0x52, 0xf2, 0xd6, 0xd1, 0xc1, 0xb2, 0xa3, 0x83, 0xaf, 0x8e, + 0x0e, 0x1e, 0xce, 0xa5, 0xb2, 0x79, 0x93, 0xb0, 0x14, 0x4b, 0x57, 0xd9, 0xd4, 0xb7, 0x57, 0xe1, + 0x02, 0xf8, 0xf3, 0xa6, 0x3b, 0xfb, 0x52, 0x83, 0x49, 0xb6, 0x9d, 0xf6, 0xf2, 0x37, 0x00, 0x00, + 0xff, 0xff, 0xf0, 0xe0, 0x3c, 0x60, 0x67, 0x01, 0x00, 0x00, } func (m *OperationalFlags) Marshal() (dAtA []byte, err error) { @@ -113,6 +134,16 @@ func (m *OperationalFlags) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.SignerBlockTimeOffset != nil { + n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.SignerBlockTimeOffset, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintOperational(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0x12 + } if m.RestartHeight != 0 { i = encodeVarintOperational(dAtA, i, uint64(m.RestartHeight)) i-- @@ -141,6 +172,10 @@ func (m *OperationalFlags) Size() (n int) { if m.RestartHeight != 0 { n += 1 + sovOperational(uint64(m.RestartHeight)) } + if m.SignerBlockTimeOffset != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset) + n += 1 + l + sovOperational(uint64(l)) + } return n } @@ -198,6 +233,42 @@ func (m *OperationalFlags) Unmarshal(dAtA []byte) error { break } } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SignerBlockTimeOffset", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowOperational + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthOperational + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthOperational + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.SignerBlockTimeOffset == nil { + m.SignerBlockTimeOffset = new(time.Duration) + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(m.SignerBlockTimeOffset, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOperational(dAtA[iNdEx:]) diff --git a/x/observer/types/operational_test.go b/x/observer/types/operational_test.go index 39ef06ef82..79c8caf0dc 100644 --- a/x/observer/types/operational_test.go +++ b/x/observer/types/operational_test.go @@ -2,8 +2,10 @@ package types_test import ( "testing" + "time" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/x/observer/types" ) @@ -14,18 +16,45 @@ func TestOperationalFlags_Validate(t *testing.T) { errContains string }{ { - name: "invalid operational flags", + name: "invalid restart height", of: types.OperationalFlags{ RestartHeight: -1, }, errContains: types.ErrOperationalFlagsRestartHeightNegative.Error(), }, { - name: "valid", + name: "valid restart height", of: types.OperationalFlags{ RestartHeight: 1, }, }, + { + name: "valid signer offset", + of: types.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(time.Second), + }, + }, + { + name: "negative signer offset", + of: types.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(-time.Second), + }, + errContains: types.ErrOperationalFlagsSignerBlockTimeOffsetNegative.Error(), + }, + { + name: "signer offset limit exceeded", + of: types.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(time.Minute), + }, + errContains: types.ErrOperationalFlagsSignerBlockTimeOffsetLimit.Error(), + }, + { + name: "all flags valid", + of: types.OperationalFlags{ + RestartHeight: 1, + SignerBlockTimeOffset: ptr.Ptr(time.Second), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/x/observer/types/tx.pb.go b/x/observer/types/tx.pb.go index 7ea1ae4086..2481756e9e 100644 --- a/x/observer/types/tx.pb.go +++ b/x/observer/types/tx.pb.go @@ -1242,7 +1242,7 @@ var xxx_messageInfo_MsgUpdateGasPriceIncreaseFlagsResponse proto.InternalMessage type MsgUpdateOperationalFlags struct { Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` - OperationalFlags OperationalFlags `protobuf:"bytes,2,opt,name=operationalFlags,proto3" json:"operationalFlags"` + OperationalFlags OperationalFlags `protobuf:"bytes,2,opt,name=operational_flags,json=operationalFlags,proto3" json:"operational_flags"` } func (m *MsgUpdateOperationalFlags) Reset() { *m = MsgUpdateOperationalFlags{} } @@ -1362,91 +1362,91 @@ func init() { } var fileDescriptor_eda6e3b1d16a4021 = []byte{ - // 1332 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcb, 0x6f, 0xdb, 0xc6, - 0x13, 0x36, 0x7f, 0xce, 0xc3, 0x1e, 0x5b, 0xb2, 0xc3, 0x9f, 0xe3, 0xd8, 0x74, 0xa3, 0xa6, 0x6c, - 0xe2, 0x28, 0x2f, 0x29, 0x56, 0x8a, 0x3e, 0x81, 0x02, 0x8e, 0x93, 0xd8, 0x6e, 0xea, 0x24, 0xa0, - 0xdc, 0xa0, 0xc8, 0x85, 0x58, 0x91, 0x6b, 0x8a, 0x35, 0xbd, 0x2b, 0x70, 0x29, 0x3b, 0x4e, 0x83, - 0x02, 0x3d, 0x16, 0xe8, 0x21, 0xa7, 0x9e, 0x0a, 0xf4, 0xde, 0xff, 0xa0, 0xf7, 0x1e, 0xd2, 0x43, - 0x81, 0x1c, 0x7b, 0x2a, 0x8a, 0xe4, 0xd4, 0xff, 0xa2, 0xe0, 0xee, 0x72, 0x25, 0x51, 0x32, 0x45, - 0xb9, 0xe8, 0x49, 0xe4, 0xec, 0xf7, 0xcd, 0x7c, 0xb3, 0x8f, 0x99, 0x15, 0xe1, 0xe2, 0x33, 0x1c, - 0x21, 0xa7, 0x89, 0x7c, 0x52, 0xe5, 0x4f, 0x34, 0xc4, 0x55, 0xda, 0x60, 0x38, 0xdc, 0xc7, 0x61, - 0x35, 0x7a, 0x5a, 0x69, 0x85, 0x34, 0xa2, 0xfa, 0x92, 0x42, 0x55, 0x12, 0x54, 0x25, 0x41, 0x19, - 0x73, 0x1e, 0xf5, 0x28, 0xc7, 0x55, 0xe3, 0x27, 0x41, 0x31, 0x2e, 0x67, 0x39, 0x6e, 0x04, 0x68, - 0x0f, 0x4b, 0x60, 0x2d, 0x0b, 0xe8, 0x84, 0x94, 0x31, 0x3e, 0x68, 0xef, 0x04, 0xc8, 0x63, 0x92, - 0x73, 0x35, 0x8b, 0x93, 0x3c, 0x48, 0x6c, 0x39, 0x0b, 0xdb, 0x42, 0x21, 0xda, 0x4b, 0xbc, 0xde, - 0xcc, 0x44, 0x62, 0xe2, 0xfa, 0xc4, 0xb3, 0x09, 0x25, 0x0e, 0x4e, 0x18, 0x97, 0x32, 0x67, 0x8f, - 0x25, 0xb0, 0x1b, 0x99, 0x72, 0x5b, 0x38, 0x44, 0x91, 0x4f, 0x09, 0x0a, 0x32, 0xb2, 0x6b, 0xed, - 0x7a, 0x55, 0x6e, 0x62, 0xf2, 0x67, 0x08, 0xb6, 0x15, 0x52, 0xba, 0xc3, 0xe4, 0x8f, 0xc0, 0x9a, - 0x7f, 0x6b, 0x70, 0x66, 0x8b, 0x79, 0x5f, 0xb4, 0x5c, 0x14, 0xe1, 0x87, 0x32, 0xbe, 0xbe, 0x00, - 0xa7, 0x9d, 0x10, 0xa3, 0x88, 0x86, 0x0b, 0xda, 0x05, 0xad, 0x3c, 0x69, 0x25, 0xaf, 0xfa, 0x4d, - 0x98, 0xa3, 0x81, 0x6b, 0x27, 0x4a, 0x6d, 0xe4, 0xba, 0x21, 0x66, 0x6c, 0xe1, 0x7f, 0x1c, 0xa6, - 0xd3, 0xc0, 0x4d, 0x9c, 0xac, 0x8a, 0x91, 0x98, 0x41, 0xf0, 0x41, 0x3f, 0x63, 0x5c, 0x30, 0x08, - 0x3e, 0x48, 0x33, 0x1e, 0x43, 0xa1, 0xcd, 0xf5, 0xd8, 0x21, 0x46, 0x8c, 0x92, 0x85, 0x13, 0x17, - 0xb4, 0x72, 0xb1, 0xb6, 0x52, 0xc9, 0xd8, 0x71, 0x95, 0xc4, 0x89, 0xc8, 0xc4, 0xe2, 0x44, 0x6b, - 0xba, 0xdd, 0xf5, 0x66, 0x2e, 0xc1, 0x62, 0x5f, 0xaa, 0x16, 0x66, 0x2d, 0x4a, 0x18, 0x36, 0x7f, - 0xd3, 0x40, 0xdf, 0x62, 0xde, 0x63, 0x1a, 0xe1, 0xdb, 0x01, 0x75, 0x76, 0x37, 0x30, 0x72, 0x33, - 0x67, 0x62, 0x11, 0x26, 0xc4, 0x26, 0xf4, 0x5d, 0x9e, 0xfd, 0xb8, 0x75, 0x9a, 0xbf, 0x6f, 0xba, - 0xfa, 0x79, 0x80, 0x46, 0xec, 0xc3, 0x6e, 0x22, 0xd6, 0xe4, 0x89, 0x4e, 0x5b, 0x93, 0xdc, 0xb2, - 0x81, 0x58, 0x53, 0x9f, 0x87, 0x53, 0x4d, 0xec, 0x7b, 0xcd, 0x88, 0x27, 0x36, 0x6e, 0xc9, 0x37, - 0x7d, 0x3d, 0xb6, 0xc7, 0x51, 0x17, 0x4e, 0x5e, 0xd0, 0xca, 0x53, 0xb5, 0x2b, 0x83, 0x12, 0x6e, - 0xed, 0x7a, 0x15, 0xb9, 0x82, 0x42, 0xe2, 0x1d, 0x14, 0xa1, 0xdb, 0x27, 0x5e, 0xfe, 0xf9, 0xf6, - 0x98, 0x25, 0xe9, 0xe6, 0x57, 0x60, 0xf4, 0xa7, 0x92, 0x64, 0xaa, 0x5f, 0x82, 0x62, 0x03, 0x05, - 0x01, 0x8d, 0x6c, 0x9e, 0x0a, 0x76, 0x79, 0x66, 0x13, 0x56, 0x41, 0x58, 0xd7, 0x84, 0x31, 0x86, - 0xed, 0xd3, 0x08, 0xdb, 0x3b, 0x3e, 0x41, 0x81, 0xff, 0x0c, 0x8b, 0x2c, 0x27, 0xac, 0x42, 0x6c, - 0xbd, 0x97, 0x18, 0xcd, 0xe7, 0x30, 0xa7, 0x26, 0x75, 0x2d, 0x96, 0xfa, 0x88, 0x1f, 0x9f, 0x8c, - 0x89, 0xfb, 0x0c, 0xa6, 0x9c, 0x0e, 0x90, 0x7b, 0x9d, 0xaa, 0x95, 0x33, 0x17, 0xb7, 0xcb, 0xb1, - 0xd5, 0x4d, 0x36, 0x4b, 0xf0, 0xd6, 0xa0, 0xe8, 0x6a, 0x55, 0xef, 0x73, 0x75, 0x16, 0xde, 0xa3, - 0xfb, 0x39, 0xd5, 0x1d, 0xbd, 0xac, 0x32, 0x58, 0x9f, 0x33, 0x15, 0xec, 0x57, 0x0d, 0x8a, 0x5b, - 0xcc, 0x5b, 0x75, 0xdd, 0x1c, 0x07, 0xe9, 0x0a, 0xcc, 0x1e, 0x71, 0x88, 0x66, 0x68, 0xea, 0x3c, - 0x7c, 0x0c, 0x8b, 0x7c, 0x4a, 0x02, 0x1f, 0x93, 0xc8, 0xf6, 0x42, 0x44, 0x22, 0x8c, 0xed, 0x56, - 0xbb, 0xb1, 0x8b, 0x0f, 0xe5, 0x31, 0x3a, 0xd7, 0x01, 0xac, 0x8b, 0xf1, 0x47, 0x7c, 0x58, 0x5f, - 0x81, 0xb3, 0xc8, 0x75, 0x6d, 0x42, 0x5d, 0x6c, 0x23, 0xc7, 0xa1, 0x6d, 0x12, 0xd9, 0x94, 0x04, - 0x87, 0x7c, 0xeb, 0x4d, 0x58, 0x3a, 0x72, 0xdd, 0x07, 0xd4, 0xc5, 0xab, 0x62, 0xe8, 0x21, 0x09, - 0x0e, 0xcd, 0x05, 0x98, 0xef, 0xcd, 0x42, 0x25, 0xf8, 0xbd, 0x06, 0xd3, 0x6a, 0x63, 0xa1, 0x3d, - 0x7c, 0xbc, 0xd3, 0xb1, 0x1e, 0x9f, 0x0e, 0xb4, 0x87, 0x6d, 0x9f, 0xec, 0x50, 0xae, 0x7f, 0xaa, - 0x66, 0x66, 0x2e, 0x3f, 0x0f, 0x26, 0xf7, 0xf8, 0x24, 0xe7, 0x6e, 0x92, 0x1d, 0x6a, 0xce, 0xf3, - 0xc5, 0x55, 0x6a, 0x94, 0xcc, 0x55, 0x98, 0x51, 0x9b, 0xe2, 0x3e, 0x3e, 0xf4, 0x30, 0xc9, 0x10, - 0x3a, 0x07, 0x27, 0xf9, 0xc9, 0x94, 0x2a, 0xc5, 0x8b, 0xb9, 0x08, 0xe7, 0x52, 0x2e, 0x94, 0xf7, - 0x1f, 0x35, 0xf8, 0x3f, 0xdf, 0x06, 0x0c, 0x47, 0x7c, 0x17, 0x3c, 0xe0, 0xd5, 0xff, 0x78, 0x73, - 0xb1, 0x0c, 0x33, 0x62, 0x88, 0xb7, 0x10, 0x3b, 0xa0, 0x07, 0x7c, 0x42, 0xc6, 0xad, 0x82, 0xa3, - 0x5c, 0x7f, 0x4e, 0x0f, 0xf4, 0x32, 0xcc, 0x76, 0xe3, 0x9a, 0xbe, 0xd7, 0x94, 0xc5, 0xa3, 0xd8, - 0x01, 0x6e, 0xf8, 0x5e, 0xd3, 0x3c, 0x0f, 0x4b, 0x03, 0xd4, 0x29, 0xf5, 0xbf, 0x68, 0x00, 0x72, - 0xd2, 0xb6, 0xeb, 0xf5, 0x0c, 0xd1, 0xe7, 0x01, 0x22, 0xc6, 0x92, 0x5d, 0x26, 0x76, 0xe6, 0x64, - 0xc4, 0x98, 0xdc, 0x57, 0xd7, 0x41, 0xdf, 0xe5, 0xf3, 0x62, 0xc7, 0xcb, 0x65, 0xcb, 0x7a, 0x26, - 0xb4, 0xcf, 0x8a, 0x91, 0x27, 0x38, 0x42, 0x1b, 0xa2, 0xb2, 0xdd, 0x81, 0x53, 0x2c, 0x42, 0x51, - 0x9b, 0xc9, 0x52, 0x7e, 0xfd, 0xa8, 0xca, 0x26, 0xfb, 0x98, 0x85, 0x1d, 0xec, 0xef, 0xe3, 0x3a, - 0xe7, 0x58, 0x92, 0x6b, 0x7e, 0xd7, 0x29, 0xd1, 0xdb, 0xf5, 0xfa, 0x7f, 0x53, 0xcf, 0x62, 0x98, - 0x4c, 0x8c, 0xb5, 0x1d, 0x27, 0x69, 0x54, 0x13, 0x56, 0x41, 0x58, 0xeb, 0xc2, 0x68, 0x1e, 0x40, - 0x61, 0x8b, 0x79, 0x77, 0x09, 0x6a, 0x04, 0x78, 0x6d, 0x6d, 0xfb, 0xcb, 0x8c, 0x99, 0xbc, 0x08, - 0x05, 0xcc, 0x71, 0x9b, 0xa4, 0x41, 0xdb, 0x44, 0xc5, 0xed, 0x31, 0xea, 0xcb, 0x50, 0x14, 0x86, - 0x87, 0xed, 0x48, 0xc0, 0x44, 0xdc, 0x94, 0xd5, 0x3c, 0x07, 0x67, 0x7b, 0x02, 0xab, 0x95, 0x7d, - 0xce, 0x8b, 0xcf, 0x1d, 0x9f, 0xe5, 0x90, 0xb4, 0x0c, 0x45, 0x57, 0x00, 0x7b, 0x35, 0xa5, 0xac, - 0x7a, 0x19, 0x66, 0xa4, 0x25, 0xa5, 0x2a, 0x6d, 0x96, 0x45, 0xa3, 0x2b, 0xba, 0xd2, 0xf5, 0xb3, - 0x06, 0x25, 0x75, 0x96, 0xd6, 0x11, 0x7b, 0x14, 0xfa, 0x0e, 0xde, 0x24, 0xb1, 0x14, 0x86, 0xef, - 0xc5, 0x17, 0xb8, 0x0c, 0xa1, 0x04, 0xce, 0x7a, 0x83, 0x28, 0xb2, 0x6b, 0xd4, 0x32, 0xcb, 0xc6, - 0xc0, 0x60, 0xb2, 0x8c, 0x0c, 0x76, 0x6b, 0x96, 0x61, 0x39, 0x5b, 0xab, 0x4a, 0xeb, 0x07, 0xad, - 0xfb, 0x36, 0xd1, 0xb9, 0xaf, 0x0d, 0xcb, 0xc8, 0x86, 0x59, 0x9a, 0x42, 0xcb, 0x64, 0x6e, 0x64, - 0xdf, 0x6f, 0x52, 0x24, 0x99, 0x47, 0x9f, 0x33, 0xf3, 0x5d, 0x78, 0xe7, 0x48, 0x5d, 0x89, 0xfa, - 0xda, 0xef, 0xd3, 0x30, 0xbe, 0xc5, 0x3c, 0x9d, 0xc2, 0x54, 0x77, 0xbb, 0xba, 0x96, 0x29, 0xa1, - 0xb7, 0x2b, 0x18, 0xb7, 0x46, 0x00, 0xab, 0xc3, 0xfa, 0x14, 0x8a, 0xa9, 0xbb, 0x66, 0x65, 0x98, - 0x9b, 0x5e, 0xbc, 0xf1, 0xfe, 0x68, 0x78, 0x15, 0xf9, 0x5b, 0x0d, 0xce, 0xf4, 0x5f, 0x53, 0x56, - 0xf2, 0x79, 0xeb, 0xa2, 0x18, 0x1f, 0x8d, 0x4c, 0xe9, 0xd1, 0xd0, 0x7f, 0x19, 0x19, 0xaa, 0xa1, - 0x8f, 0x32, 0x5c, 0xc3, 0x91, 0xb7, 0x14, 0xdd, 0x87, 0xc9, 0x4e, 0x03, 0xbf, 0x32, 0xcc, 0x8f, - 0x82, 0x1a, 0x2b, 0xb9, 0xa1, 0x2a, 0x54, 0x08, 0xd3, 0x3d, 0x5d, 0xf8, 0x7a, 0xbe, 0x99, 0x13, - 0x68, 0xe3, 0xbd, 0x51, 0xd0, 0x2a, 0xe6, 0xd7, 0x30, 0x93, 0xbe, 0xc3, 0x57, 0xf3, 0x29, 0x57, - 0x04, 0xe3, 0x83, 0x11, 0x09, 0x2a, 0xf8, 0x37, 0x30, 0xdb, 0x77, 0x2f, 0xb8, 0x39, 0x7c, 0xa9, - 0x7a, 0x19, 0xc6, 0x87, 0xa3, 0x32, 0x54, 0x7c, 0x07, 0x4e, 0x27, 0x9d, 0xfd, 0x72, 0x9e, 0x1c, - 0xb6, 0xeb, 0x75, 0xa3, 0x9a, 0x13, 0xa8, 0x82, 0x04, 0x00, 0x5d, 0x7d, 0xef, 0xea, 0x30, 0x7a, - 0x07, 0x6b, 0xd4, 0xf2, 0x63, 0x55, 0x34, 0x0a, 0x53, 0xdd, 0x3d, 0x6d, 0x68, 0x85, 0xea, 0x02, - 0x0f, 0xaf, 0x50, 0x03, 0xfa, 0x95, 0xfe, 0x93, 0x06, 0x4b, 0x59, 0xcd, 0xea, 0x93, 0x7c, 0xdb, - 0x72, 0x20, 0xd9, 0x58, 0xfb, 0x17, 0x64, 0xa5, 0xf0, 0x85, 0x06, 0xf3, 0x47, 0xf4, 0x9d, 0xbc, - 0xc5, 0x31, 0xc5, 0x33, 0x3e, 0x3d, 0x1e, 0x2f, 0x91, 0x74, 0xfb, 0xee, 0xcb, 0xd7, 0x25, 0xed, - 0xd5, 0xeb, 0x92, 0xf6, 0xd7, 0xeb, 0x92, 0xf6, 0xe2, 0x4d, 0x69, 0xec, 0xd5, 0x9b, 0xd2, 0xd8, - 0x1f, 0x6f, 0x4a, 0x63, 0x4f, 0xae, 0x79, 0x7e, 0xd4, 0x6c, 0x37, 0x2a, 0x0e, 0xdd, 0xe3, 0x5f, - 0x23, 0x6e, 0x88, 0x0f, 0x13, 0xf1, 0x1f, 0x92, 0xea, 0xd3, 0xae, 0x0f, 0x23, 0x87, 0x2d, 0xcc, - 0x1a, 0xa7, 0xf8, 0x47, 0x89, 0x5b, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0xa3, 0xa3, 0x3d, 0x21, - 0x82, 0x12, 0x00, 0x00, + // 1336 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcd, 0x6f, 0xdc, 0x44, + 0x14, 0x8f, 0x49, 0x3f, 0x92, 0x97, 0xec, 0x26, 0x35, 0x69, 0x9a, 0x38, 0x74, 0x29, 0xa6, 0x4d, + 0xb7, 0x5f, 0xbb, 0xcd, 0x16, 0xf1, 0x29, 0x21, 0xa5, 0x69, 0x9b, 0x84, 0x92, 0xb6, 0xf2, 0x86, + 0x0a, 0xf5, 0x62, 0x66, 0xed, 0x89, 0xd7, 0xc4, 0x99, 0x59, 0x79, 0xbc, 0x49, 0x53, 0x2a, 0x24, + 0x8e, 0x48, 0x1c, 0x7a, 0xe3, 0x82, 0xc4, 0x9d, 0xff, 0x80, 0x3b, 0x87, 0x72, 0x40, 0xea, 0x91, + 0x13, 0x42, 0xed, 0x89, 0xff, 0x02, 0x79, 0x66, 0x3c, 0xeb, 0xfd, 0x88, 0xd7, 0x5b, 0xc4, 0x69, + 0xed, 0x37, 0xbf, 0xdf, 0x7b, 0xbf, 0x37, 0x1f, 0xef, 0xcd, 0x1a, 0xce, 0x3f, 0xc1, 0x11, 0x72, + 0x9a, 0xc8, 0x27, 0x55, 0xfe, 0x44, 0x43, 0x5c, 0xa5, 0x0d, 0x86, 0xc3, 0x7d, 0x1c, 0x56, 0xa3, + 0xc7, 0x95, 0x56, 0x48, 0x23, 0xaa, 0x2f, 0x29, 0x54, 0x25, 0x41, 0x55, 0x12, 0x94, 0x31, 0xe7, + 0x51, 0x8f, 0x72, 0x5c, 0x35, 0x7e, 0x12, 0x14, 0xe3, 0x62, 0x96, 0xe3, 0x46, 0x80, 0xf6, 0xb0, + 0x04, 0xd6, 0xb2, 0x80, 0x4e, 0x48, 0x19, 0xe3, 0x83, 0xf6, 0x4e, 0x80, 0x3c, 0x26, 0x39, 0x97, + 0xb3, 0x38, 0xc9, 0x83, 0xc4, 0x96, 0xb3, 0xb0, 0x2d, 0x14, 0xa2, 0xbd, 0xc4, 0xeb, 0xf5, 0x4c, + 0x24, 0x26, 0xae, 0x4f, 0x3c, 0x9b, 0x50, 0xe2, 0xe0, 0x84, 0x71, 0x21, 0x73, 0xf6, 0x58, 0x02, + 0xbb, 0x96, 0x29, 0xb7, 0x85, 0x43, 0x14, 0xf9, 0x94, 0xa0, 0x20, 0x23, 0xbb, 0xd6, 0xae, 0x57, + 0xe5, 0x26, 0x26, 0x7f, 0x86, 0x60, 0x5b, 0x21, 0xa5, 0x3b, 0x4c, 0xfe, 0x08, 0xac, 0xf9, 0x8f, + 0x06, 0xa7, 0xb6, 0x98, 0xf7, 0x45, 0xcb, 0x45, 0x11, 0xbe, 0x2f, 0xe3, 0xeb, 0x0b, 0x70, 0xd2, + 0x09, 0x31, 0x8a, 0x68, 0xb8, 0xa0, 0x9d, 0xd3, 0xca, 0x93, 0x56, 0xf2, 0xaa, 0x5f, 0x87, 0x39, + 0x1a, 0xb8, 0x76, 0xa2, 0xd4, 0x46, 0xae, 0x1b, 0x62, 0xc6, 0x16, 0xde, 0xe0, 0x30, 0x9d, 0x06, + 0x6e, 0xe2, 0x64, 0x55, 0x8c, 0xc4, 0x0c, 0x82, 0x0f, 0xfa, 0x19, 0xe3, 0x82, 0x41, 0xf0, 0x41, + 0x2f, 0xe3, 0x21, 0x14, 0xda, 0x5c, 0x8f, 0x1d, 0x62, 0xc4, 0x28, 0x59, 0x38, 0x76, 0x4e, 0x2b, + 0x17, 0x6b, 0x2b, 0x95, 0x8c, 0x1d, 0x57, 0x49, 0x9c, 0x88, 0x4c, 0x2c, 0x4e, 0xb4, 0xa6, 0xdb, + 0xa9, 0x37, 0x73, 0x09, 0x16, 0xfb, 0x52, 0xb5, 0x30, 0x6b, 0x51, 0xc2, 0xb0, 0xf9, 0xbb, 0x06, + 0xfa, 0x16, 0xf3, 0x1e, 0xd2, 0x08, 0xdf, 0x0c, 0xa8, 0xb3, 0xbb, 0x81, 0x91, 0x9b, 0x39, 0x13, + 0x8b, 0x30, 0x21, 0x36, 0xa1, 0xef, 0xf2, 0xec, 0xc7, 0xad, 0x93, 0xfc, 0x7d, 0xd3, 0xd5, 0xcf, + 0x02, 0x34, 0x62, 0x1f, 0x76, 0x13, 0xb1, 0x26, 0x4f, 0x74, 0xda, 0x9a, 0xe4, 0x96, 0x0d, 0xc4, + 0x9a, 0xfa, 0x3c, 0x9c, 0x68, 0x62, 0xdf, 0x6b, 0x46, 0x3c, 0xb1, 0x71, 0x4b, 0xbe, 0xe9, 0xeb, + 0xb1, 0x3d, 0x8e, 0xba, 0x70, 0xfc, 0x9c, 0x56, 0x9e, 0xaa, 0x5d, 0x1a, 0x94, 0x70, 0x6b, 0xd7, + 0xab, 0xc8, 0x15, 0x14, 0x12, 0x6f, 0xa1, 0x08, 0xdd, 0x3c, 0xf6, 0xfc, 0xaf, 0xb7, 0xc7, 0x2c, + 0x49, 0x37, 0xbf, 0x06, 0xa3, 0x3f, 0x95, 0x24, 0x53, 0xfd, 0x02, 0x14, 0x1b, 0x28, 0x08, 0x68, + 0x64, 0xf3, 0x54, 0xb0, 0xcb, 0x33, 0x9b, 0xb0, 0x0a, 0xc2, 0xba, 0x26, 0x8c, 0x31, 0x6c, 0x9f, + 0x46, 0xd8, 0xde, 0xf1, 0x09, 0x0a, 0xfc, 0x27, 0x58, 0x64, 0x39, 0x61, 0x15, 0x62, 0xeb, 0x9d, + 0xc4, 0x68, 0x3e, 0x85, 0x39, 0x35, 0xa9, 0x6b, 0xb1, 0xd4, 0x07, 0xfc, 0xf8, 0x64, 0x4c, 0xdc, + 0x67, 0x30, 0xe5, 0x74, 0x80, 0xdc, 0xeb, 0x54, 0xad, 0x9c, 0xb9, 0xb8, 0x29, 0xc7, 0x56, 0x9a, + 0x6c, 0x96, 0xe0, 0xad, 0x41, 0xd1, 0xd5, 0xaa, 0xde, 0xe5, 0xea, 0x2c, 0xbc, 0x47, 0xf7, 0x73, + 0xaa, 0x3b, 0x7a, 0x59, 0x65, 0xb0, 0x3e, 0x67, 0x2a, 0xd8, 0x6f, 0x1a, 0x14, 0xb7, 0x98, 0xb7, + 0xea, 0xba, 0x39, 0x0e, 0xd2, 0x25, 0x98, 0x3d, 0xe2, 0x10, 0xcd, 0xd0, 0x9e, 0xf3, 0xf0, 0x31, + 0x2c, 0xf2, 0x29, 0x09, 0x7c, 0x4c, 0x22, 0xdb, 0x0b, 0x11, 0x89, 0x30, 0xb6, 0x5b, 0xed, 0xc6, + 0x2e, 0x3e, 0x94, 0xc7, 0xe8, 0x4c, 0x07, 0xb0, 0x2e, 0xc6, 0x1f, 0xf0, 0x61, 0x7d, 0x05, 0x4e, + 0x23, 0xd7, 0xb5, 0x09, 0x75, 0xb1, 0x8d, 0x1c, 0x87, 0xb6, 0x49, 0x64, 0x53, 0x12, 0x1c, 0xf2, + 0xad, 0x37, 0x61, 0xe9, 0xc8, 0x75, 0xef, 0x51, 0x17, 0xaf, 0x8a, 0xa1, 0xfb, 0x24, 0x38, 0x34, + 0x17, 0x60, 0xbe, 0x3b, 0x0b, 0x95, 0xe0, 0x0f, 0x1a, 0x4c, 0xab, 0x8d, 0x85, 0xf6, 0xf0, 0xeb, + 0x9d, 0x8e, 0xf5, 0xf8, 0x74, 0xa0, 0x3d, 0x6c, 0xfb, 0x64, 0x87, 0x72, 0xfd, 0x53, 0x35, 0x33, + 0x73, 0xf9, 0x79, 0x30, 0xb9, 0xc7, 0x27, 0x39, 0x77, 0x93, 0xec, 0x50, 0x73, 0x9e, 0x2f, 0xae, + 0x52, 0xa3, 0x64, 0xae, 0xc2, 0x8c, 0xda, 0x14, 0x77, 0xf1, 0xa1, 0x87, 0x49, 0x86, 0xd0, 0x39, + 0x38, 0xce, 0x4f, 0xa6, 0x54, 0x29, 0x5e, 0xcc, 0x45, 0x38, 0xd3, 0xe3, 0x42, 0x79, 0xff, 0x49, + 0x83, 0x37, 0xf9, 0x36, 0x60, 0x38, 0xe2, 0xbb, 0xe0, 0x1e, 0xaf, 0xfe, 0xaf, 0x37, 0x17, 0xcb, + 0x30, 0x23, 0x86, 0x78, 0x0b, 0xb1, 0x03, 0x7a, 0xc0, 0x27, 0x64, 0xdc, 0x2a, 0x38, 0xca, 0xf5, + 0xe7, 0xf4, 0x40, 0x2f, 0xc3, 0x6c, 0x1a, 0xd7, 0xf4, 0xbd, 0xa6, 0x2c, 0x1e, 0xc5, 0x0e, 0x70, + 0xc3, 0xf7, 0x9a, 0xe6, 0x59, 0x58, 0x1a, 0xa0, 0x4e, 0xa9, 0xff, 0x55, 0x03, 0x90, 0x93, 0xb6, + 0x5d, 0xaf, 0x67, 0x88, 0x3e, 0x0b, 0x10, 0x31, 0x96, 0xec, 0x32, 0xb1, 0x33, 0x27, 0x23, 0xc6, + 0xe4, 0xbe, 0xba, 0x0a, 0xfa, 0x2e, 0x9f, 0x17, 0x3b, 0x5e, 0x2e, 0x5b, 0xd6, 0x33, 0xa1, 0x7d, + 0x56, 0x8c, 0x3c, 0xc2, 0x11, 0xda, 0x10, 0x95, 0xed, 0x16, 0x9c, 0x60, 0x11, 0x8a, 0xda, 0x4c, + 0x96, 0xf2, 0xab, 0x47, 0x55, 0x36, 0xd9, 0xc7, 0x2c, 0xec, 0x60, 0x7f, 0x1f, 0xd7, 0x39, 0xc7, + 0x92, 0x5c, 0xf3, 0xfb, 0x4e, 0x89, 0xde, 0xae, 0xd7, 0xff, 0x9f, 0x7a, 0x16, 0xc3, 0x64, 0x62, + 0xac, 0xed, 0x38, 0x49, 0xa3, 0x9a, 0xb0, 0x0a, 0xc2, 0x5a, 0x17, 0x46, 0xf3, 0x00, 0x0a, 0x5b, + 0xcc, 0xbb, 0x4d, 0x50, 0x23, 0xc0, 0x6b, 0x6b, 0xdb, 0x5f, 0x66, 0xcc, 0xe4, 0x79, 0x28, 0x60, + 0x8e, 0xdb, 0x24, 0x0d, 0xda, 0x26, 0x2a, 0x6e, 0x97, 0x51, 0x5f, 0x86, 0xa2, 0x30, 0xdc, 0x6f, + 0x47, 0x02, 0x26, 0xe2, 0xf6, 0x58, 0xcd, 0x33, 0x70, 0xba, 0x2b, 0xb0, 0x5a, 0xd9, 0xa7, 0xbc, + 0xf8, 0xdc, 0xf2, 0x59, 0x0e, 0x49, 0xcb, 0x50, 0x74, 0x05, 0xb0, 0x5b, 0x53, 0x8f, 0x55, 0x2f, + 0xc3, 0x8c, 0xb4, 0xf4, 0xa8, 0xea, 0x35, 0xcb, 0xa2, 0x91, 0x8a, 0xae, 0x74, 0xfd, 0xa2, 0x41, + 0x49, 0x9d, 0xa5, 0x75, 0xc4, 0x1e, 0x84, 0xbe, 0x83, 0x37, 0x49, 0x2c, 0x85, 0xe1, 0x3b, 0xf1, + 0x05, 0x2e, 0x43, 0x28, 0x81, 0xd3, 0xde, 0x20, 0x8a, 0xec, 0x1a, 0xb5, 0xcc, 0xb2, 0x31, 0x30, + 0x98, 0x2c, 0x23, 0x83, 0xdd, 0x9a, 0x65, 0x58, 0xce, 0xd6, 0xaa, 0xd2, 0xfa, 0x51, 0x4b, 0xdf, + 0x26, 0x3a, 0xf7, 0xb5, 0x61, 0x19, 0x7d, 0x05, 0xa7, 0x52, 0xb7, 0x3b, 0x71, 0x83, 0x95, 0xd9, + 0x5c, 0xcb, 0xbe, 0xe0, 0xf4, 0xc4, 0x90, 0x89, 0xcc, 0xd2, 0x1e, 0xbb, 0xf9, 0x2e, 0xbc, 0x73, + 0xa4, 0xb0, 0x44, 0x7e, 0xed, 0x8f, 0x69, 0x18, 0xdf, 0x62, 0x9e, 0x4e, 0x61, 0x2a, 0xdd, 0xaf, + 0xae, 0x64, 0x4a, 0xe8, 0x6e, 0x0b, 0xc6, 0x8d, 0x11, 0xc0, 0xea, 0xb4, 0x3e, 0x86, 0x62, 0xcf, + 0x65, 0xb3, 0x32, 0xcc, 0x4d, 0x37, 0xde, 0x78, 0x7f, 0x34, 0xbc, 0x8a, 0xfc, 0x9d, 0x06, 0xa7, + 0xfa, 0xef, 0x29, 0x2b, 0xf9, 0xbc, 0xa5, 0x28, 0xc6, 0x47, 0x23, 0x53, 0xba, 0x34, 0xf4, 0xdf, + 0x46, 0x86, 0x6a, 0xe8, 0xa3, 0x0c, 0xd7, 0x70, 0xe4, 0x35, 0x45, 0xf7, 0x61, 0xb2, 0xd3, 0xc1, + 0x2f, 0x0d, 0xf3, 0xa3, 0xa0, 0xc6, 0x4a, 0x6e, 0xa8, 0x0a, 0x15, 0xc2, 0x74, 0x57, 0x1b, 0xbe, + 0x9a, 0x6f, 0xe6, 0x04, 0xda, 0x78, 0x6f, 0x14, 0xb4, 0x8a, 0xf9, 0x0d, 0xcc, 0xf4, 0x5e, 0xe2, + 0xab, 0xf9, 0x94, 0x2b, 0x82, 0xf1, 0xc1, 0x88, 0x04, 0x15, 0xfc, 0x5b, 0x98, 0xed, 0xbb, 0x18, + 0x5c, 0x1f, 0xbe, 0x54, 0xdd, 0x0c, 0xe3, 0xc3, 0x51, 0x19, 0x2a, 0xbe, 0x03, 0x27, 0x93, 0xd6, + 0x7e, 0x31, 0x4f, 0x0e, 0xdb, 0xf5, 0xba, 0x51, 0xcd, 0x09, 0x54, 0x41, 0x02, 0x80, 0x54, 0xe3, + 0xbb, 0x3c, 0x8c, 0xde, 0xc1, 0x1a, 0xb5, 0xfc, 0x58, 0x15, 0x8d, 0xc2, 0x54, 0xba, 0xa9, 0x0d, + 0xad, 0x50, 0x29, 0xf0, 0xf0, 0x0a, 0x35, 0xa0, 0x61, 0xe9, 0x3f, 0x6b, 0xb0, 0x94, 0xd5, 0xad, + 0x3e, 0xc9, 0xb7, 0x2d, 0x07, 0x92, 0x8d, 0xb5, 0xff, 0x40, 0x56, 0x0a, 0x9f, 0x69, 0x30, 0x7f, + 0x44, 0xe3, 0xc9, 0x5b, 0x1c, 0x7b, 0x78, 0xc6, 0xa7, 0xaf, 0xc7, 0x4b, 0x24, 0xdd, 0xbc, 0xfd, + 0xfc, 0x65, 0x49, 0x7b, 0xf1, 0xb2, 0xa4, 0xfd, 0xfd, 0xb2, 0xa4, 0x3d, 0x7b, 0x55, 0x1a, 0x7b, + 0xf1, 0xaa, 0x34, 0xf6, 0xe7, 0xab, 0xd2, 0xd8, 0xa3, 0x2b, 0x9e, 0x1f, 0x35, 0xdb, 0x8d, 0x8a, + 0x43, 0xf7, 0xf8, 0xe7, 0x88, 0x6b, 0xe2, 0xcb, 0x44, 0xfc, 0x8f, 0xa4, 0xfa, 0x38, 0xf5, 0x65, + 0xe4, 0xb0, 0x85, 0x59, 0xe3, 0x04, 0xff, 0x2a, 0x71, 0xe3, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x05, 0x3a, 0x52, 0xd9, 0x83, 0x12, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index 36dc5ad813..6e46037209 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -101,6 +101,13 @@ var ( Help: "Difference between system time and block time from zetacore", }) + // CoreBlockLatencySleep is a gauge of the duration we sleep before signing + CoreBlockLatencySleep = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: ZetaClientNamespace, + Name: "core_block_latency_sleep", + Help: "The duration we sleep before signing", + }) + // Info is a gauge that contains information about the zetaclient environment Info = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: ZetaClientNamespace, diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 6e818329b1..3e475fe442 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -68,6 +68,9 @@ type Orchestrator struct { dbDirectory string baseLogger base.Logger + // signerBlockTimeOffset + signerBlockTimeOffset time.Duration + // misc logger multiLogger ts *metrics.TelemetryServer @@ -135,6 +138,12 @@ func (oc *Orchestrator) Start(ctx context.Context) error { bg.Work(ctx, oc.runScheduler, bg.WithName("runScheduler"), bg.WithLogger(oc.logger.Logger)) bg.Work(ctx, oc.runObserverSignerSync, bg.WithName("runObserverSignerSync"), bg.WithLogger(oc.logger.Logger)) bg.Work(ctx, oc.runAppContextUpdater, bg.WithName("runAppContextUpdater"), bg.WithLogger(oc.logger.Logger)) + bg.Work( + ctx, + oc.runSyncObserverOperationalFlags, + bg.WithName("runSyncObserverOperationalFlags"), + bg.WithLogger(oc.logger.Logger), + ) return nil } @@ -318,6 +327,13 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { continue } + sleepDuration := time.Until(newBlock.Block.Time.Add(oc.signerBlockTimeOffset)) + if sleepDuration < 0 { + sleepDuration = 0 + } + metrics.CoreBlockLatencySleep.Set(sleepDuration.Seconds()) + time.Sleep(sleepDuration) + balance, err := oc.zetacoreClient.GetZetaHotKeyBalance(ctx) if err != nil { oc.logger.Error().Err(err).Msgf("couldn't get operator balance") @@ -795,3 +811,48 @@ func (oc *Orchestrator) syncObserverSigner(ctx context.Context) error { return nil } + +func (oc *Orchestrator) runSyncObserverOperationalFlags(ctx context.Context) error { + // every other block + const cadence = 2 * constant.ZetaBlockTime + + task := func(ctx context.Context, _ *ticker.Ticker) error { + if err := oc.syncObserverOperationalFlags(ctx); err != nil { + oc.logger.Error().Err(err).Msg("syncObserverOperationalFlags failed") + } + + return nil + } + + return ticker.Run( + ctx, + cadence, + task, + ticker.WithLogger(oc.logger.Logger, "SyncObserverOperationalFlags"), + ticker.WithStopChan(oc.stop), + ) +} + +func (oc *Orchestrator) syncObserverOperationalFlags(ctx context.Context) error { + client := oc.zetacoreClient + flags, err := client.GetOperationalFlags(ctx) + if err != nil { + return fmt.Errorf("get operational flags: %w", err) + } + + oc.mu.Lock() + defer oc.mu.Unlock() + newSignerBlockTimeOffsetPtr := flags.SignerBlockTimeOffset + if newSignerBlockTimeOffsetPtr == nil { + return nil + } + newSignerBlockTimeOffset := *newSignerBlockTimeOffsetPtr + if oc.signerBlockTimeOffset != newSignerBlockTimeOffset { + oc.logger.Info(). + Dur("offset", newSignerBlockTimeOffset). + Msg("block time offset updated") + oc.signerBlockTimeOffset = newSignerBlockTimeOffset + } + + return nil +} diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 8637a47e17..2a961a43fa 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -3,6 +3,7 @@ package orchestrator import ( "context" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" @@ -14,6 +15,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/testutil/sample" crosschainkeeper "github.com/zeta-chain/node/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" @@ -540,6 +542,58 @@ func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { } } +func TestSyncObserverOperationalFlags(t *testing.T) { + ctx := context.Background() + t.Run("no flags set", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{}, nil) + orchestrator := mockOrchestrator(t, client) + err := orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Zero( + t, + orchestrator.signerBlockTimeOffset, + "block time offset should be zero if operational flags not set", + ) + }) + + t.Run("flags set", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + expectedDuration := time.Second + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(expectedDuration), + }, nil) + orchestrator := mockOrchestrator(t, client) + err := orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Equal(t, expectedDuration, orchestrator.signerBlockTimeOffset) + }) + + t.Run("flags updated", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + expectedDuration := time.Second + mock := client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(expectedDuration), + }, nil) + orchestrator := mockOrchestrator(t, client) + err := orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Equal(t, expectedDuration, orchestrator.signerBlockTimeOffset) + + mock.Unset() + expectedDuration = time.Second * 2 + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(expectedDuration), + }, nil) + err = orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Equal(t, expectedDuration, orchestrator.signerBlockTimeOffset) + }) +} + func mockOrchestrator(t *testing.T, zetaClient interfaces.ZetacoreClient, chainsOrParams ...any) *Orchestrator { supportedChains, obsParams := parseChainsWithParams(t, chainsOrParams...) From 80ca9214bf531668b0b18199f0743fa81957f3ca Mon Sep 17 00:00:00 2001 From: cuiweiyuan Date: Fri, 3 Jan 2025 01:36:38 +0800 Subject: [PATCH 04/10] chore: fix function name in comment (#3327) Signed-off-by: cuiweiyuan Co-authored-by: Alex Gartner --- zetaclient/chains/bitcoin/rpc/rpc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/rpc/rpc.go b/zetaclient/chains/bitcoin/rpc/rpc.go index 48182c8726..d29291c582 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc.go +++ b/zetaclient/chains/bitcoin/rpc/rpc.go @@ -61,7 +61,7 @@ func GetTxResultByHash( return hash, txResult, nil } -// GetTXRawResultByHash gets the raw transaction by hash +// GetRawTxByHash gets the raw transaction by hash func GetRawTxByHash(rpcClient interfaces.BTCRPCClient, txID string) (*btcutil.Tx, error) { hash, err := chainhash.NewHashFromStr(txID) if err != nil { From ef4c1d4eb87ff6fa6acba379f083e6b90393d187 Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Mon, 6 Jan 2025 10:14:29 +0100 Subject: [PATCH 05/10] fix: make crosschain-call with invalid withdraw revert (#3321) * withdrawer contract * deposit and withdraw dust test * add changelog * add test * don't commit state and revert when process withdraw fails * fix tests * add more assertions * update contract * update contract 2 * add back bitcoin tests * Update e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go Co-authored-by: skosito * Update x/crosschain/keeper/evm_deposit.go Co-authored-by: skosito * add comment --------- Co-authored-by: skosito --- changelog.md | 1 + cmd/zetae2e/local/local.go | 1 + e2e/contracts/withdrawer/Withdrawer.abi | 116 +++++++ e2e/contracts/withdrawer/Withdrawer.bin | 1 + e2e/contracts/withdrawer/Withdrawer.go | 283 ++++++++++++++++++ e2e/contracts/withdrawer/Withdrawer.json | 119 ++++++++ e2e/contracts/withdrawer/Withdrawer.sol | 37 +++ e2e/contracts/withdrawer/bindings.go | 6 + e2e/e2etests/e2etests.go | 13 +- ..._bitcoin_deposit_and_withdraw_with_dust.go | 58 ++++ x/crosschain/keeper/evm_deposit.go | 27 +- x/crosschain/keeper/evm_deposit_test.go | 22 +- x/crosschain/keeper/evm_hooks.go | 6 +- x/crosschain/keeper/evm_hooks_test.go | 16 +- 14 files changed, 676 insertions(+), 30 deletions(-) create mode 100644 e2e/contracts/withdrawer/Withdrawer.abi create mode 100644 e2e/contracts/withdrawer/Withdrawer.bin create mode 100644 e2e/contracts/withdrawer/Withdrawer.go create mode 100644 e2e/contracts/withdrawer/Withdrawer.json create mode 100644 e2e/contracts/withdrawer/Withdrawer.sol create mode 100644 e2e/contracts/withdrawer/bindings.go create mode 100644 e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go diff --git a/changelog.md b/changelog.md index 72d1ea2c59..5bb11d40cf 100644 --- a/changelog.md +++ b/changelog.md @@ -32,6 +32,7 @@ * [3278](https://github.com/zeta-chain/node/pull/3278) - enforce checksum format for asset address in ZRC20 * [3289](https://github.com/zeta-chain/node/pull/3289) - remove all dynamic peer discovery (zetaclient) * [3314](https://github.com/zeta-chain/node/pull/3314) - update `last_scanned_block_number` metrics more frequently for Solana chain +* [3321](https://github.com/zeta-chain/node/pull/3321) - make crosschain-call with invalid withdraw revert ## v24.0.0 diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 65b5fdcf44..8368e0d0bc 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -303,6 +303,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { bitcoinDepositTestsAdvanced := []string{ e2etests.TestBitcoinDepositAndCallRevertWithDustName, e2etests.TestBitcoinStdMemoDepositAndCallRevertOtherAddressName, + e2etests.TestBitcoinDepositAndWithdrawWithDustName, } bitcoinWithdrawTests := []string{ e2etests.TestBitcoinWithdrawSegWitName, diff --git a/e2e/contracts/withdrawer/Withdrawer.abi b/e2e/contracts/withdrawer/Withdrawer.abi new file mode 100644 index 0000000000..9d462137f0 --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.abi @@ -0,0 +1,116 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_withdrawAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/e2e/contracts/withdrawer/Withdrawer.bin b/e2e/contracts/withdrawer/Withdrawer.bin new file mode 100644 index 0000000000..7f0200971d --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.bin @@ -0,0 +1 @@ +60a0604052348015600f57600080fd5b506040516107f63803806107f68339818101604052810190602f91906072565b806080818152505050609a565b600080fd5b6000819050919050565b6052816041565b8114605c57600080fd5b50565b600081519050606c81604b565b92915050565b6000602082840312156085576084603c565b5b6000609184828501605f565b91505092915050565b6080516107346100c260003960008181609e0152818161018d01526102e201526107346000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063534844a2146100465780635bcfd61614610064578063de43156e14610080575b600080fd5b61004e61009c565b60405161005b9190610383565b60405180910390f35b61007e600480360381019061007991906104bb565b6100c0565b005b61009a600480360381019061009591906104bb565b610215565b005b7f000000000000000000000000000000000000000000000000000000000000000081565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161011b92919061056e565b6020604051808303816000875af115801561013a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061015e91906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c701262686806000019061018b919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b81526004016101ca939291906106cc565b6020604051808303816000875af11580156101e9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061020d91906105cf565b505050505050565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161027092919061056e565b6020604051808303816000875af115801561028f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102b391906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c70126268680600001906102e0919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b815260040161031f939291906106cc565b6020604051808303816000875af115801561033e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061036291906105cf565b505050505050565b6000819050919050565b61037d8161036a565b82525050565b60006020820190506103986000830184610374565b92915050565b600080fd5b600080fd5b600080fd5b6000606082840312156103c3576103c26103a8565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103f7826103cc565b9050919050565b610407816103ec565b811461041257600080fd5b50565b600081359050610424816103fe565b92915050565b6104338161036a565b811461043e57600080fd5b50565b6000813590506104508161042a565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261047b5761047a610456565b5b8235905067ffffffffffffffff8111156104985761049761045b565b5b6020830191508360018202830111156104b4576104b3610460565b5b9250929050565b6000806000806000608086880312156104d7576104d661039e565b5b600086013567ffffffffffffffff8111156104f5576104f46103a3565b5b610501888289016103ad565b955050602061051288828901610415565b945050604061052388828901610441565b935050606086013567ffffffffffffffff811115610544576105436103a3565b5b61055088828901610465565b92509250509295509295909350565b610568816103ec565b82525050565b6000604082019050610583600083018561055f565b6105906020830184610374565b9392505050565b60008115159050919050565b6105ac81610597565b81146105b757600080fd5b50565b6000815190506105c9816105a3565b92915050565b6000602082840312156105e5576105e461039e565b5b60006105f3848285016105ba565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083356001602003843603038112610628576106276105fc565b5b80840192508235915067ffffffffffffffff82111561064a57610649610601565b5b60208301925060018202360383131561066657610665610606565b5b509250929050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006106ab838561066e565b93506106b883858461067f565b6106c18361068e565b840190509392505050565b600060408201905081810360008301526106e781858761069f565b90506106f66020830184610374565b94935050505056fea2646970667358221220eb0d0178243bc765ecffd41945dfc69d032eaf9e1347d0b6ee2ec8408676acd564736f6c634300081a0033 diff --git a/e2e/contracts/withdrawer/Withdrawer.go b/e2e/contracts/withdrawer/Withdrawer.go new file mode 100644 index 0000000000..99bd5fdfe3 --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.go @@ -0,0 +1,283 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package withdrawer + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// Context is an auto generated low-level Go binding around an user-defined struct. +type Context struct { + Origin []byte + Sender common.Address + ChainID *big.Int +} + +// WithdrawerMetaData contains all meta data concerning the Withdrawer contract. +var WithdrawerMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_withdrawAmount\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"origin\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"chainID\",\"type\":\"uint256\"}],\"internalType\":\"structContext\",\"name\":\"context\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"name\":\"onCall\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"origin\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"chainID\",\"type\":\"uint256\"}],\"internalType\":\"structContext\",\"name\":\"context\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"name\":\"onCrossChainCall\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"withdrawAmount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + Bin: "0x60a0604052348015600f57600080fd5b506040516107f63803806107f68339818101604052810190602f91906072565b806080818152505050609a565b600080fd5b6000819050919050565b6052816041565b8114605c57600080fd5b50565b600081519050606c81604b565b92915050565b6000602082840312156085576084603c565b5b6000609184828501605f565b91505092915050565b6080516107346100c260003960008181609e0152818161018d01526102e201526107346000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063534844a2146100465780635bcfd61614610064578063de43156e14610080575b600080fd5b61004e61009c565b60405161005b9190610383565b60405180910390f35b61007e600480360381019061007991906104bb565b6100c0565b005b61009a600480360381019061009591906104bb565b610215565b005b7f000000000000000000000000000000000000000000000000000000000000000081565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161011b92919061056e565b6020604051808303816000875af115801561013a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061015e91906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c701262686806000019061018b919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b81526004016101ca939291906106cc565b6020604051808303816000875af11580156101e9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061020d91906105cf565b505050505050565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161027092919061056e565b6020604051808303816000875af115801561028f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102b391906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c70126268680600001906102e0919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b815260040161031f939291906106cc565b6020604051808303816000875af115801561033e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061036291906105cf565b505050505050565b6000819050919050565b61037d8161036a565b82525050565b60006020820190506103986000830184610374565b92915050565b600080fd5b600080fd5b600080fd5b6000606082840312156103c3576103c26103a8565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103f7826103cc565b9050919050565b610407816103ec565b811461041257600080fd5b50565b600081359050610424816103fe565b92915050565b6104338161036a565b811461043e57600080fd5b50565b6000813590506104508161042a565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261047b5761047a610456565b5b8235905067ffffffffffffffff8111156104985761049761045b565b5b6020830191508360018202830111156104b4576104b3610460565b5b9250929050565b6000806000806000608086880312156104d7576104d661039e565b5b600086013567ffffffffffffffff8111156104f5576104f46103a3565b5b610501888289016103ad565b955050602061051288828901610415565b945050604061052388828901610441565b935050606086013567ffffffffffffffff811115610544576105436103a3565b5b61055088828901610465565b92509250509295509295909350565b610568816103ec565b82525050565b6000604082019050610583600083018561055f565b6105906020830184610374565b9392505050565b60008115159050919050565b6105ac81610597565b81146105b757600080fd5b50565b6000815190506105c9816105a3565b92915050565b6000602082840312156105e5576105e461039e565b5b60006105f3848285016105ba565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083356001602003843603038112610628576106276105fc565b5b80840192508235915067ffffffffffffffff82111561064a57610649610601565b5b60208301925060018202360383131561066657610665610606565b5b509250929050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006106ab838561066e565b93506106b883858461067f565b6106c18361068e565b840190509392505050565b600060408201905081810360008301526106e781858761069f565b90506106f66020830184610374565b94935050505056fea2646970667358221220eb0d0178243bc765ecffd41945dfc69d032eaf9e1347d0b6ee2ec8408676acd564736f6c634300081a0033", +} + +// WithdrawerABI is the input ABI used to generate the binding from. +// Deprecated: Use WithdrawerMetaData.ABI instead. +var WithdrawerABI = WithdrawerMetaData.ABI + +// WithdrawerBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use WithdrawerMetaData.Bin instead. +var WithdrawerBin = WithdrawerMetaData.Bin + +// DeployWithdrawer deploys a new Ethereum contract, binding an instance of Withdrawer to it. +func DeployWithdrawer(auth *bind.TransactOpts, backend bind.ContractBackend, _withdrawAmount *big.Int) (common.Address, *types.Transaction, *Withdrawer, error) { + parsed, err := WithdrawerMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(WithdrawerBin), backend, _withdrawAmount) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Withdrawer{WithdrawerCaller: WithdrawerCaller{contract: contract}, WithdrawerTransactor: WithdrawerTransactor{contract: contract}, WithdrawerFilterer: WithdrawerFilterer{contract: contract}}, nil +} + +// Withdrawer is an auto generated Go binding around an Ethereum contract. +type Withdrawer struct { + WithdrawerCaller // Read-only binding to the contract + WithdrawerTransactor // Write-only binding to the contract + WithdrawerFilterer // Log filterer for contract events +} + +// WithdrawerCaller is an auto generated read-only Go binding around an Ethereum contract. +type WithdrawerCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// WithdrawerTransactor is an auto generated write-only Go binding around an Ethereum contract. +type WithdrawerTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// WithdrawerFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type WithdrawerFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// WithdrawerSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type WithdrawerSession struct { + Contract *Withdrawer // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// WithdrawerCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type WithdrawerCallerSession struct { + Contract *WithdrawerCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// WithdrawerTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type WithdrawerTransactorSession struct { + Contract *WithdrawerTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// WithdrawerRaw is an auto generated low-level Go binding around an Ethereum contract. +type WithdrawerRaw struct { + Contract *Withdrawer // Generic contract binding to access the raw methods on +} + +// WithdrawerCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type WithdrawerCallerRaw struct { + Contract *WithdrawerCaller // Generic read-only contract binding to access the raw methods on +} + +// WithdrawerTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type WithdrawerTransactorRaw struct { + Contract *WithdrawerTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewWithdrawer creates a new instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawer(address common.Address, backend bind.ContractBackend) (*Withdrawer, error) { + contract, err := bindWithdrawer(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Withdrawer{WithdrawerCaller: WithdrawerCaller{contract: contract}, WithdrawerTransactor: WithdrawerTransactor{contract: contract}, WithdrawerFilterer: WithdrawerFilterer{contract: contract}}, nil +} + +// NewWithdrawerCaller creates a new read-only instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawerCaller(address common.Address, caller bind.ContractCaller) (*WithdrawerCaller, error) { + contract, err := bindWithdrawer(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &WithdrawerCaller{contract: contract}, nil +} + +// NewWithdrawerTransactor creates a new write-only instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawerTransactor(address common.Address, transactor bind.ContractTransactor) (*WithdrawerTransactor, error) { + contract, err := bindWithdrawer(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &WithdrawerTransactor{contract: contract}, nil +} + +// NewWithdrawerFilterer creates a new log filterer instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawerFilterer(address common.Address, filterer bind.ContractFilterer) (*WithdrawerFilterer, error) { + contract, err := bindWithdrawer(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &WithdrawerFilterer{contract: contract}, nil +} + +// bindWithdrawer binds a generic wrapper to an already deployed contract. +func bindWithdrawer(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := WithdrawerMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Withdrawer *WithdrawerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Withdrawer.Contract.WithdrawerCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Withdrawer *WithdrawerRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Withdrawer.Contract.WithdrawerTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Withdrawer *WithdrawerRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Withdrawer.Contract.WithdrawerTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Withdrawer *WithdrawerCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Withdrawer.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Withdrawer *WithdrawerTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Withdrawer.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Withdrawer *WithdrawerTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Withdrawer.Contract.contract.Transact(opts, method, params...) +} + +// WithdrawAmount is a free data retrieval call binding the contract method 0x534844a2. +// +// Solidity: function withdrawAmount() view returns(uint256) +func (_Withdrawer *WithdrawerCaller) WithdrawAmount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Withdrawer.contract.Call(opts, &out, "withdrawAmount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// WithdrawAmount is a free data retrieval call binding the contract method 0x534844a2. +// +// Solidity: function withdrawAmount() view returns(uint256) +func (_Withdrawer *WithdrawerSession) WithdrawAmount() (*big.Int, error) { + return _Withdrawer.Contract.WithdrawAmount(&_Withdrawer.CallOpts) +} + +// WithdrawAmount is a free data retrieval call binding the contract method 0x534844a2. +// +// Solidity: function withdrawAmount() view returns(uint256) +func (_Withdrawer *WithdrawerCallerSession) WithdrawAmount() (*big.Int, error) { + return _Withdrawer.Contract.WithdrawAmount(&_Withdrawer.CallOpts) +} + +// OnCall is a paid mutator transaction binding the contract method 0x5bcfd616. +// +// Solidity: function onCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactor) OnCall(opts *bind.TransactOpts, context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.contract.Transact(opts, "onCall", context, zrc20, arg2, arg3) +} + +// OnCall is a paid mutator transaction binding the contract method 0x5bcfd616. +// +// Solidity: function onCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerSession) OnCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} + +// OnCall is a paid mutator transaction binding the contract method 0x5bcfd616. +// +// Solidity: function onCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactorSession) OnCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactor) OnCrossChainCall(opts *bind.TransactOpts, context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.contract.Transact(opts, "onCrossChainCall", context, zrc20, arg2, arg3) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerSession) OnCrossChainCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCrossChainCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactorSession) OnCrossChainCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCrossChainCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} diff --git a/e2e/contracts/withdrawer/Withdrawer.json b/e2e/contracts/withdrawer/Withdrawer.json new file mode 100644 index 0000000000..26bfe6df7b --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.json @@ -0,0 +1,119 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_withdrawAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bin": "60a0604052348015600f57600080fd5b506040516107f63803806107f68339818101604052810190602f91906072565b806080818152505050609a565b600080fd5b6000819050919050565b6052816041565b8114605c57600080fd5b50565b600081519050606c81604b565b92915050565b6000602082840312156085576084603c565b5b6000609184828501605f565b91505092915050565b6080516107346100c260003960008181609e0152818161018d01526102e201526107346000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063534844a2146100465780635bcfd61614610064578063de43156e14610080575b600080fd5b61004e61009c565b60405161005b9190610383565b60405180910390f35b61007e600480360381019061007991906104bb565b6100c0565b005b61009a600480360381019061009591906104bb565b610215565b005b7f000000000000000000000000000000000000000000000000000000000000000081565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161011b92919061056e565b6020604051808303816000875af115801561013a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061015e91906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c701262686806000019061018b919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b81526004016101ca939291906106cc565b6020604051808303816000875af11580156101e9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061020d91906105cf565b505050505050565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161027092919061056e565b6020604051808303816000875af115801561028f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102b391906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c70126268680600001906102e0919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b815260040161031f939291906106cc565b6020604051808303816000875af115801561033e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061036291906105cf565b505050505050565b6000819050919050565b61037d8161036a565b82525050565b60006020820190506103986000830184610374565b92915050565b600080fd5b600080fd5b600080fd5b6000606082840312156103c3576103c26103a8565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103f7826103cc565b9050919050565b610407816103ec565b811461041257600080fd5b50565b600081359050610424816103fe565b92915050565b6104338161036a565b811461043e57600080fd5b50565b6000813590506104508161042a565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261047b5761047a610456565b5b8235905067ffffffffffffffff8111156104985761049761045b565b5b6020830191508360018202830111156104b4576104b3610460565b5b9250929050565b6000806000806000608086880312156104d7576104d661039e565b5b600086013567ffffffffffffffff8111156104f5576104f46103a3565b5b610501888289016103ad565b955050602061051288828901610415565b945050604061052388828901610441565b935050606086013567ffffffffffffffff811115610544576105436103a3565b5b61055088828901610465565b92509250509295509295909350565b610568816103ec565b82525050565b6000604082019050610583600083018561055f565b6105906020830184610374565b9392505050565b60008115159050919050565b6105ac81610597565b81146105b757600080fd5b50565b6000815190506105c9816105a3565b92915050565b6000602082840312156105e5576105e461039e565b5b60006105f3848285016105ba565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083356001602003843603038112610628576106276105fc565b5b80840192508235915067ffffffffffffffff82111561064a57610649610601565b5b60208301925060018202360383131561066657610665610606565b5b509250929050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006106ab838561066e565b93506106b883858461067f565b6106c18361068e565b840190509392505050565b600060408201905081810360008301526106e781858761069f565b90506106f66020830184610374565b94935050505056fea2646970667358221220eb0d0178243bc765ecffd41945dfc69d032eaf9e1347d0b6ee2ec8408676acd564736f6c634300081a0033" +} diff --git a/e2e/contracts/withdrawer/Withdrawer.sol b/e2e/contracts/withdrawer/Withdrawer.sol new file mode 100644 index 0000000000..13ac900ace --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +struct Context { + bytes origin; + address sender; + uint256 chainID; +} + +interface IZRC20 { + function approve(address spender, uint256 amount) external returns (bool); + function withdraw(bytes memory to, uint256 amount) external returns (bool); +} + +// Withdrawer is a simple contract performing a withdraw of deposited ZRC20 +// The amount to withdraw can be set during the contract deployment, it also to tests some edge cases like withdrawing BTC dust amount +contract Withdrawer { + uint256 immutable public withdrawAmount; + + constructor(uint256 _withdrawAmount) { + withdrawAmount = _withdrawAmount; + } + + // perform a withdraw on cross chain call + function onCrossChainCall(Context calldata context, address zrc20, uint256, bytes calldata) external { + // perform withdrawal with the target token + IZRC20(zrc20).approve(address(zrc20), type(uint256).max); + IZRC20(zrc20).withdraw(context.origin, withdrawAmount); + } + + // perform a withdraw on cross chain call, v2 + function onCall(Context calldata context, address zrc20, uint256, bytes calldata) external { + // perform withdrawal with the target token + IZRC20(zrc20).approve(address(zrc20), type(uint256).max); + IZRC20(zrc20).withdraw(context.origin, withdrawAmount); + } +} \ No newline at end of file diff --git a/e2e/contracts/withdrawer/bindings.go b/e2e/contracts/withdrawer/bindings.go new file mode 100644 index 0000000000..0ec483e439 --- /dev/null +++ b/e2e/contracts/withdrawer/bindings.go @@ -0,0 +1,6 @@ +//go:generate sh -c "solc --evm-version paris Withdrawer.sol --combined-json abi,bin --allow-paths .. | jq '.contracts.\"Withdrawer.sol:Withdrawer\"' > Withdrawer.json" +//go:generate sh -c "cat Withdrawer.json | jq .abi > Withdrawer.abi" +//go:generate sh -c "cat Withdrawer.json | jq .bin | tr -d '\"' > Withdrawer.bin" +//go:generate sh -c "abigen --abi Withdrawer.abi --bin Withdrawer.bin --pkg withdrawer --type Withdrawer --out Withdrawer.go" + +package withdrawer diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 1b9f3ef419..884573d2fa 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -75,6 +75,7 @@ const ( TestBitcoinDepositAndCallName = "bitcoin_deposit_and_call" TestBitcoinDepositAndCallRevertName = "bitcoin_deposit_and_call_revert" TestBitcoinDepositAndCallRevertWithDustName = "bitcoin_deposit_and_call_revert_with_dust" + TestBitcoinDepositAndWithdrawWithDustName = "bitcoin_deposit_and_withdraw_with_dust" TestBitcoinDonationName = "bitcoin_donation" TestBitcoinStdMemoDepositName = "bitcoin_std_memo_deposit" TestBitcoinStdMemoDepositAndCallName = "bitcoin_std_memo_deposit_and_call" @@ -587,16 +588,24 @@ var AllE2ETests = []runner.E2ETest{ ), runner.NewE2ETest( TestBitcoinDepositAndCallRevertName, - "deposit Bitcoin into ZEVM; expect refund", []runner.ArgDefinition{ + "deposit Bitcoin into ZEVM; expect refund", + []runner.ArgDefinition{ {Description: "amount in btc", DefaultValue: "0.1"}, }, TestBitcoinDepositAndCallRevert, ), runner.NewE2ETest( TestBitcoinDepositAndCallRevertWithDustName, - "deposit Bitcoin into ZEVM; revert with dust amount that aborts the CCTX", []runner.ArgDefinition{}, + "deposit Bitcoin into ZEVM; revert with dust amount that aborts the CCTX", + []runner.ArgDefinition{}, TestBitcoinDepositAndCallRevertWithDust, ), + runner.NewE2ETest( + TestBitcoinDepositAndWithdrawWithDustName, + "deposit Bitcoin into ZEVM and withdraw with dust amount that fails the CCTX", + []runner.ArgDefinition{}, + TestBitcoinDepositAndWithdrawWithDust, + ), runner.NewE2ETest( TestBitcoinStdMemoDepositName, "deposit Bitcoin into ZEVM with standard memo", diff --git a/e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go b/e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go new file mode 100644 index 0000000000..8b108f2103 --- /dev/null +++ b/e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go @@ -0,0 +1,58 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/contracts/withdrawer" + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + +// TestBitcoinDepositAndWithdrawWithDust deposits Bitcoin and call a smart contract that withdraw dust amount +// It tests the edge case where during a cross-chain call, a invaild withdraw is initiated (processLogs fails) +func TestBitcoinDepositAndWithdrawWithDust(r *runner.E2ERunner, args []string) { + // Given "Live" BTC network + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + require.Len(r, args, 0) + + // ARRANGE + + // Deploy the withdrawer contract on ZetaChain with a withdraw amount of 100 satoshis (dust amount is 1000 satoshis) + withdrawerAddr, tx, _, err := withdrawer.DeployWithdrawer(r.ZEVMAuth, r.ZEVMClient, big.NewInt(100)) + require.NoError(r, err) + + // Wait for the transaction to be mined + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.Equal(r, receipt.Status, uint64(1)) + + // Given a list of UTXOs + utxos, err := r.ListDeployerUTXOs() + require.NoError(r, err) + require.NotEmpty(r, utxos) + + // ACT + // Deposit 0.01 BTC to withdrawer, this is an arbitrary amount, must be greater than dust amount + txHash, err := r.SendToTSSFromDeployerWithMemo(0.01, utxos, withdrawerAddr.Bytes()) + require.NoError(r, err) + require.NotEmpty(r, txHash) + + // ASSERT + // Now we want to make sure the cctx is reverted with expected error message + + // cctx status would be pending revert if using v21 or before + cctx := utils.WaitCctxRevertedByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient) + require.Equal(r, crosschaintypes.CctxStatus_Reverted, cctx.CctxStatus.Status) + require.Contains(r, cctx.CctxStatus.ErrorMessage, crosschaintypes.ErrCannotProcessWithdrawal.Error()) + + // check the contract has no BTC balance, this would mean the contract call state transition is not reverted + // get BTC ZRC20 balance of the withdrawer contract + bal, err := r.BTCZRC20.BalanceOf(&bind.CallOpts{}, withdrawerAddr) + require.NoError(r, err) + require.Zero(r, bal.Uint64()) +} diff --git a/x/crosschain/keeper/evm_deposit.go b/x/crosschain/keeper/evm_deposit.go index e57dc16d1b..d6c8a77a66 100644 --- a/x/crosschain/keeper/evm_deposit.go +++ b/x/crosschain/keeper/evm_deposit.go @@ -100,8 +100,13 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo return false, fmt.Errorf("HandleEVMDeposit: unable to decode address: %w", err) } + // use a temporary context to not commit any state change in case of error + // note: ZRC20DepositAndCallContract is solely responsible for calling the contract and depositing tokens if needed + // and does not include any other side effects or any logic that modifies the state directly + tmpCtx, commit := ctx.CacheContext() + evmTxResponse, contractCall, err := k.fungibleKeeper.ZRC20DepositAndCallContract( - ctx, + tmpCtx, from, to, inboundAmount, @@ -113,8 +118,11 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo cctx.InboundParams.IsCrossChainCall, ) if fungibletypes.IsContractReverted(evmTxResponse, err) || errShouldRevertCctx(err) { + // this is a contract revert, we commit the state to save the emitted logs related to revert + commit() return true, err } else if err != nil { + // this should not happen and we don't commit the state to avoid inconsistent state return false, err } @@ -123,18 +131,21 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo if !evmTxResponse.Failed() && contractCall { logs := evmtypes.LogsToEthereum(evmTxResponse.Logs) if len(logs) > 0 { - ctx = ctx.WithValue(InCCTXIndexKey, cctx.Index) + tmpCtx = tmpCtx.WithValue(InCCTXIndexKey, cctx.Index) txOrigin := cctx.InboundParams.TxOrigin if txOrigin == "" { txOrigin = inboundSender } - err = k.ProcessLogs(ctx, logs, to, txOrigin) + // process logs to process cctx events initiated during the contract call + err = k.ProcessLogs(tmpCtx, logs, to, txOrigin) if err != nil { - // ProcessLogs should not error; error indicates exception, should abort - return false, errors.Wrap(types.ErrCannotProcessWithdrawal, err.Error()) + // this happens if the cctx events are not processed correctly with invalid withdrawals + // in this situation we want the CCTX to be reverted, we don't commit the state so the contract call is not persisted + // the contract call is considered as reverted + return true, errors.Wrap(types.ErrCannotProcessWithdrawal, err.Error()) } - ctx.EventManager().EmitEvent( + tmpCtx.EventManager().EmitEvent( sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute("action", "DepositZRC20AndCallContract"), @@ -145,7 +156,11 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo ) } } + + // commit state change from the deposit and eventual cctx events + commit() } + return false, nil } diff --git a/x/crosschain/keeper/evm_deposit_test.go b/x/crosschain/keeper/evm_deposit_test.go index 45c0a0aecc..b65071a13f 100644 --- a/x/crosschain/keeper/evm_deposit_test.go +++ b/x/crosschain/keeper/evm_deposit_test.go @@ -98,7 +98,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -145,7 +145,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -187,7 +187,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { cctx, ) require.Error(t, err) - require.False(t, reverted) + require.True(t, reverted) fungibleMock.AssertExpectations(t) }, ) @@ -209,7 +209,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -300,7 +300,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { errDeposit := errors.New("deposit failed") fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -346,7 +346,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { errDeposit := errors.New("deposit failed") fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -391,7 +391,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -436,7 +436,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -481,7 +481,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -552,7 +552,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ctx = ctx.WithTxBytes(b) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -596,7 +596,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { require.NoError(t, err) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, diff --git a/x/crosschain/keeper/evm_hooks.go b/x/crosschain/keeper/evm_hooks.go index 38726c287a..d09f771fbb 100644 --- a/x/crosschain/keeper/evm_hooks.go +++ b/x/crosschain/keeper/evm_hooks.go @@ -133,7 +133,7 @@ func (k Keeper) ProcessZEVMInboundV1( // If Validation fails, we will not process the event and return and error. This condition means that the event was correct, and emitted from a registered ZRC20 contract // But the information entered by the user is incorrect. In this case we can return an error and roll back the transaction - if err := k.ValidateZrc20WithdrawEvent(ctx, eventZRC20Withdrawal, coin.ForeignChainId); err != nil { + if err := k.ValidateZRC20WithdrawEvent(ctx, eventZRC20Withdrawal, coin.ForeignChainId); err != nil { return err } // If the event is valid, we will process it and create a new CCTX @@ -303,9 +303,9 @@ func (k Keeper) ProcessZetaSentEvent( return nil } -// ValidateZrc20WithdrawEvent checks if the ZRC20Withdrawal event is valid +// ValidateZRC20WithdrawEvent checks if the ZRC20Withdrawal event is valid // It verifies event information for BTC chains and returns an error if the event is invalid -func (k Keeper) ValidateZrc20WithdrawEvent(ctx sdk.Context, event *zrc20.ZRC20Withdrawal, chainID int64) error { +func (k Keeper) ValidateZRC20WithdrawEvent(ctx sdk.Context, event *zrc20.ZRC20Withdrawal, chainID int64) error { // The event was parsed; that means the user has deposited tokens to the contract. return k.validateZRC20Withdrawal(ctx, chainID, event.Value, event.To) } diff --git a/x/crosschain/keeper/evm_hooks_test.go b/x/crosschain/keeper/evm_hooks_test.go index fff7e31f02..e9b4b49b0c 100644 --- a/x/crosschain/keeper/evm_hooks_test.go +++ b/x/crosschain/keeper/evm_hooks_test.go @@ -161,7 +161,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { *sample.ValidZRC20WithdrawToBTCReceipt(t).Logs[3], ) require.NoError(t, err) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.NoError(t, err) }) @@ -174,12 +174,12 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { // 1000 satoshis is the minimum amount that can be withdrawn btcMainNetWithdrawalEvent.Value = big.NewInt(constant.BTCWithdrawalDustAmount) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.NoError(t, err) // 999 satoshis cannot be withdrawn btcMainNetWithdrawalEvent.Value = big.NewInt(constant.BTCWithdrawalDustAmount - 1) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.ErrorContains(t, err, "less than dust amount") }) @@ -189,7 +189,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { *sample.ValidZRC20WithdrawToBTCReceipt(t).Logs[3], ) require.NoError(t, err) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinTestnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinTestnet.ChainId) require.ErrorContains(t, err, "invalid address") }) @@ -201,7 +201,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { require.NoError(t, err) btcMainNetWithdrawalEvent.To = []byte("04b2891ba8cb491828db3ebc8a780d43b169e7b3974114e6e50f9bab6ec" + "63c2f20f6d31b2025377d05c2a704d3bd799d0d56f3a8543d79a01ab6084a1cb204f260") - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.ErrorContains(t, err, "unsupported address") }) @@ -213,7 +213,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { value := big.NewInt(constant.SolanaWalletRentExempt) solWithdrawalEvent := sample.ZRC20Withdrawal(to, value) - err := k.ValidateZrc20WithdrawEvent(ctx, solWithdrawalEvent, chains.SolanaMainnet.ChainId) + err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chains.SolanaMainnet.ChainId) require.ErrorContains(t, err, "invalid address") }) @@ -227,12 +227,12 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { solWithdrawalEvent := sample.ZRC20Withdrawal(to, value) // 1000000 lamports can be withdrawn - err := k.ValidateZrc20WithdrawEvent(ctx, solWithdrawalEvent, chainID) + err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID) require.NoError(t, err) // 999999 lamports cannot be withdrawn solWithdrawalEvent.Value = big.NewInt(constant.SolanaWalletRentExempt - 1) - err = k.ValidateZrc20WithdrawEvent(ctx, solWithdrawalEvent, chainID) + err = k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID) require.ErrorContains(t, err, "less than rent exempt") }) } From 51e3f9751d02bad584b5aec939cbc293c5e12361 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 8 Jan 2025 06:46:24 -0800 Subject: [PATCH 06/10] chore: remove main Dockerfile (#3329) --- .../build-docker-images-generic/action.yml | 71 ---- .../actions/build-docker-images/action.yml | 54 --- .github/workflows/docker-build-and-push.yml | 86 ----- Dockerfile | 66 ---- Makefile | 43 --- contrib/docker-scripts/download_binaries.py | 58 --- contrib/docker-scripts/install_cosmovisor.py | 69 ---- contrib/docker-scripts/start.sh | 335 ------------------ contrib/rpc/zetacored/docker-compose.yml | 36 -- contrib/rpc/zetacored/init_docker_compose.sh | 65 ---- contrib/rpc/zetacored/kill_docker_compose.sh | 14 - contrib/rpc/zetacored/networks/.athens3 | 14 - .../zetacored/networks/.athens3-localbuild | 14 - contrib/rpc/zetacored/networks/.mainnet | 14 - .../zetacored/networks/.mainnet-localbuild | 14 - 15 files changed, 953 deletions(-) delete mode 100644 .github/actions/build-docker-images-generic/action.yml delete mode 100644 .github/actions/build-docker-images/action.yml delete mode 100644 .github/workflows/docker-build-and-push.yml delete mode 100644 Dockerfile delete mode 100644 contrib/docker-scripts/download_binaries.py delete mode 100644 contrib/docker-scripts/install_cosmovisor.py delete mode 100644 contrib/docker-scripts/start.sh delete mode 100644 contrib/rpc/zetacored/docker-compose.yml delete mode 100644 contrib/rpc/zetacored/init_docker_compose.sh delete mode 100644 contrib/rpc/zetacored/kill_docker_compose.sh delete mode 100644 contrib/rpc/zetacored/networks/.athens3 delete mode 100644 contrib/rpc/zetacored/networks/.athens3-localbuild delete mode 100644 contrib/rpc/zetacored/networks/.mainnet delete mode 100644 contrib/rpc/zetacored/networks/.mainnet-localbuild diff --git a/.github/actions/build-docker-images-generic/action.yml b/.github/actions/build-docker-images-generic/action.yml deleted file mode 100644 index 245ac8a631..0000000000 --- a/.github/actions/build-docker-images-generic/action.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: 'Build Docker Images' -description: 'Builds Docker images and pushes them to any repository.' -inputs: - DOCKER_FILENAME: - description: 'Name of the docker file to use for the build' - required: true - REPOSITORY_NAME: - description: 'Name of the Repository' - required: true - IMAGE_TAG: - description: 'Image Tag' - required: true - REGISTRY: - description: 'Docker or ORG you want to push to.' - required: true - DOCKER_ORG: - description: 'Docker ORG you want to push to.' - required: false - USERNAME: - description: 'Username for GitHub Container Registry' - required: true - TOKEN: - description: 'Token for GitHub Container Registry' - required: true - DOCKER_FILE_DIRECTORY: - description: 'Directory for your Dockerfile' - required: true - DOCKER_BUILD_KIT: - description: "whether or not to use docker build kit." - required: true - TAG_LATEST: - description: "should the pipeline tag latest" - required: true -runs: - using: "composite" - - steps: - - name: Set Environment Variables" - run: | - echo "DOCKER_BUILDKIT=${{ inputs.DOCKER_BUILD_KIT }}" >> $GITHUB_ENV - shell: bash - - - name: Log in to the Docker Registry - uses: docker/login-action@v2 - with: - registry: ${{ inputs.REGISTRY }} - username: ${{ inputs.USERNAME }} - password: ${{ inputs.TOKEN }} - - - name: Build, tag, and push images - shell: bash - working-directory: ${{ inputs.DOCKER_FILE_DIRECTORY }} - run: | - if [ ! -z "${{ inputs.DOCKER_ORG }}" ]; then - echo "DOCKER ORG SPECIFIED SO USE DOCKER HUB" - docker build -f ${{ inputs.DOCKER_FILENAME }} -t ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} . - docker push ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} - - if [ "${{ inputs.TAG_LATEST }}" == "true" ]; then - docker tag ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:latest - docker push ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:latest - fi - else - echo "DOCKER REGISTRY SPECIFIED WITH NO DOCKER_ORG USE NON ORG REGISTRIES" - docker build -f ${{ inputs.DOCKER_FILENAME }} -t ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} . - docker push ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} - if [ "${{ inputs.TAG_LATEST }}" == "true" ]; then - docker tag ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:latest - docker push ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:latest - fi - fi diff --git a/.github/actions/build-docker-images/action.yml b/.github/actions/build-docker-images/action.yml deleted file mode 100644 index b68efe7cbb..0000000000 --- a/.github/actions/build-docker-images/action.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: 'Build Docker Images' -description: 'Builds Docker images and pushes them to ECR and GHCR' -inputs: - DOCKER_FILENAME: - description: 'Name of the docker file to use for the build' - required: true - REPOSITORY_NAME: - description: 'Name of the Repository' - required: true - IMAGE_TAG: - description: 'Image Tag' - required: true - GHCR_USERNAME: - description: 'Username for GitHub Container Registry' - required: true - GHCR_TOKEN: - description: 'Token for GitHub Container Registry' - required: true - -runs: - using: "composite" - - steps: - - name: Set Environment Variables" - run: | - echo "DOCKER_FILENAME=${{ inputs.DOCKER_FILENAME }}" >> $GITHUB_ENV - echo "REPOSITORY_NAME=${{ inputs.REPOSITORY_NAME }}" >> $GITHUB_ENV - echo "IMAGE_TAG=${{ inputs.IMAGE_TAG }}" >> $GITHUB_ENV - echo "GHCR_USERNAME=${{ inputs.GHCR_USERNAME }}" >> $GITHUB_ENV - echo "GHCR_TOKEN=${{ inputs.GHCR_TOKEN }}" >> $GITHUB_ENV - echo "DOCKER_BUILDKIT=1" >> $GITHUB_ENV - shell: bash - - - name: Log in to the GitHub Container Registry - id: login-ghcr - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ env.GHCR_USERNAME }} - password: ${{ env.GHCR_TOKEN }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push images - shell: bash - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - GHCR_REGISTRY: ghcr.io/zeta-chain - run: | - docker build -f $DOCKER_FILENAME -t $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG -t $GHCR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG . - # docker push $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG - docker push $GHCR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG \ No newline at end of file diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml deleted file mode 100644 index c2def3200d..0000000000 --- a/.github/workflows/docker-build-and-push.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Zetacored-Docker-Build - -on: - release: - types: - - created - workflow_dispatch: - inputs: - version: - description: 'Docker Tag Version For Manual Execution' - required: false - default: '' - -concurrency: - group: Zetacored-Docker-Build - cancel-in-progress: false - -env: - DOCKER_REPO: "zetacored" - DOCKER_ORG: "zetachain" - DOCKER_REGISTRY: "https://index.docker.io/v1/" - -jobs: - docker_build_ubuntu: - runs-on: ubuntu-22.04 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set Version from the release title. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.release.name }}" >> $GITHUB_ENV - - - name: Set Version for Hotfix Release from Input. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.inputs.version }}" >> ${GITHUB_ENV} - - - name: "BUILD:PUSH:MONITORING:DOCKER:IMAGE" - uses: ./.github/actions/build-docker-images-generic - with: - DOCKER_FILENAME: "Dockerfile" - REPOSITORY_NAME: "${{ env.DOCKER_REPO }}" - IMAGE_TAG: "ubuntu-${{ env.GITHUB_TAG_MAJOR_VERSION }}" - REGISTRY: "${{ env.DOCKER_REGISTRY }}" - DOCKER_ORG: "${{ env.DOCKER_ORG }}" - USERNAME: "${{ secrets.DOCKER_HUB_USERNAME }}" - TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" - DOCKER_FILE_DIRECTORY: "./" - DOCKER_BUILD_KIT: "0" - TAG_LATEST: "true" - - docker_build_arm: - runs-on: buildjet-4vcpu-ubuntu-2204-arm - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set Version from the release title. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.release.name }}" >> $GITHUB_ENV - - - name: Set Version for Hotfix Release from Input. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.inputs.version }}" >> ${GITHUB_ENV} - - - name: "BUILD:PUSH:MONITORING:DOCKER:IMAGE" - uses: ./.github/actions/build-docker-images-generic - with: - DOCKER_FILENAME: "Dockerfile" - REPOSITORY_NAME: "${{ env.DOCKER_REPO }}" - IMAGE_TAG: "arm-${{ env.GITHUB_TAG_MAJOR_VERSION }}" - REGISTRY: "${{ env.DOCKER_REGISTRY }}" - DOCKER_ORG: "${{ env.DOCKER_ORG }}" - USERNAME: "${{ secrets.DOCKER_HUB_USERNAME }}" - TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" - DOCKER_FILE_DIRECTORY: "./" - DOCKER_BUILD_KIT: "0" - TAG_LATEST: "false" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0f9804d087..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -# Build Stage -FROM golang:1.22-alpine3.18 AS builder - -ENV GOPATH /go -ENV GOOS=linux -ENV CGO_ENABLED=1 - -# Install build dependencies -RUN apk --no-cache add git make build-base jq openssh libusb-dev linux-headers bash curl python3 py3-pip - -# Set the working directory -WORKDIR /go/delivery/zeta-node - -# Copy module files and download dependencies -COPY go.mod . -COPY go.sum . - -RUN go mod download - -# Copy the rest of the source code and build the application -COPY . . - -RUN expected_major_version=$(grep 'const releaseVersion = ' app/setup_handlers.go | awk -F'"' '{print $2}') && \ - make install VERSION="${expected_major_version}" && \ - git_hash=$(git rev-parse --short HEAD) && \ - echo -n "${expected_major_version}-${git_hash}" > /go/delivery/zeta-node/expected_major_version - -# Run Stage -FROM alpine:3.18 - -ENV COSMOVISOR_CHECKSUM="626dfc58c266b85f84a7ed8e2fe0e2346c15be98cfb9f9b88576ba899ed78cdc" -ENV COSMOVISOR_VERSION="v1.5.0" -# Copy Start Script Helpers -COPY contrib/docker-scripts/* /scripts/ - -# Install runtime dependencies -RUN apk --no-cache add git jq bash curl nano vim tmux python3 libusb-dev linux-headers make build-base bind-tools psmisc coreutils wget py3-pip qemu-img qemu-system-x86_64 && \ - pip install requests && \ - chmod a+x -R /scripts && \ - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.31-r0/glibc-2.31-r0.apk && \ - apk add --force-overwrite --allow-untrusted glibc-2.31-r0.apk && \ - curl https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz > /tmp/google-cloud-sdk.tar.gz && \ - mkdir -p /usr/local/gcloud && \ - tar -C /usr/local/gcloud -xvf /tmp/google-cloud-sdk.tar.gz && \ - /usr/local/gcloud/google-cloud-sdk/install.sh --quiet && \ - ln -s /usr/local/gcloud/google-cloud-sdk/bin/gcloud /usr/bin/gcloud && \ - python /scripts/install_cosmovisor.py - -# Copy the binaries from the build stage -COPY --from=builder /go/bin/zetaclientd /usr/local/bin/zetaclientd -COPY --from=builder /go/bin/zetacored /usr/local/bin/zetacored -COPY --from=builder /go/delivery/zeta-node/expected_major_version /scripts/expected_major_version - -# Set the working directory -WORKDIR /usr/local/bin - -# Set the default shell -ENV SHELL /bin/bash - -EXPOSE 26656 -EXPOSE 1317 -EXPOSE 8545 -EXPOSE 8546 -EXPOSE 9090 -EXPOSE 26657 -EXPOSE 9091 \ No newline at end of file diff --git a/Makefile b/Makefile index 1ecd3bc599..344ae150e4 100644 --- a/Makefile +++ b/Makefile @@ -482,49 +482,6 @@ stop-eth-node-mainnet: clean-eth-node-mainnet: cd contrib/rpc/ethereum && DOCKER_TAG=$(DOCKER_TAG) docker-compose down -v -#ZETA - -#FULL-NODE-RPC-FROM-BUILT-IMAGE -start-zetacored-rpc-mainnet: - cd contrib/rpc/zetacored && bash init_docker_compose.sh mainnet image $(DOCKER_TAG) - -stop-zetacored-rpc-mainnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet false - -clean-zetacored-rpc-mainnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet true - -#FULL-NODE-RPC-FROM-BUILT-IMAGE -start-zetacored-rpc-testnet: - cd contrib/rpc/zetacored && bash init_docker_compose.sh athens3 image $(DOCKER_TAG) - -stop-zetacored-rpc-testnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 false - -clean-zetacored-rpc-testnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 true - -#FULL-NODE-RPC-FROM-LOCAL-BUILD -start-zetacored-rpc-mainnet-localbuild: - cd contrib/rpc/zetacored && bash init_docker_compose.sh mainnet localbuild $(DOCKER_TAG) - -stop-zetacored-rpc-mainnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet false - -clean-zetacored-rpc-mainnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet true - -#FULL-NODE-RPC-FROM-LOCAL-BUILD -start-zetacored-rpc-testnet-localbuild: - cd contrib/rpc/zetacored && bash init_docker_compose.sh athens3 localbuild $(DOCKER_TAG) - -stop-zetacored-rpc-testnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 false - -clean-zetacored-rpc-testnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 true - - ############################################################################### ### Debug Tools ### ############################################################################### diff --git a/contrib/docker-scripts/download_binaries.py b/contrib/docker-scripts/download_binaries.py deleted file mode 100644 index 99063a40f0..0000000000 --- a/contrib/docker-scripts/download_binaries.py +++ /dev/null @@ -1,58 +0,0 @@ -import requests -import os -import json -import logging -import sys - -# Logger class for easier logging setup -class Logger: - def __init__(self): - self.log = logging.getLogger() - self.log.setLevel(logging.INFO) - self.handler = logging.StreamHandler(sys.stdout) - self.handler.setLevel(logging.DEBUG) - self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - self.handler.setFormatter(self.formatter) - self.log.addHandler(self.handler) - - -# Initialize logger instance -logger = Logger() - -# Parse JSON from an environment variable to get binary download information -info = json.loads(os.environ["DOWNLOAD_BINARIES"]) - -try: - # Iterate over binaries to download - for binary in info["binaries"]: - download_link = binary["download_url"] - binary_location = f'{os.environ["DAEMON_HOME"]}/{binary["binary_location"]}' - binary_directory = os.path.dirname(binary_location) - # Log download link - logger.log.info(f"DOWNLOAD LINK: {download_link}") - split_download_link = download_link.split("/") - # Log split download link parts - logger.log.info(f"SPLIT DOWNLOAD LINK: {split_download_link}") - # Extract binary name and version from the download link - binary_name = download_link.split("/")[8] - # Check if binary already exists - logger.log.info(f"CHECKING / DOWNLOADING {binary_location}") - - if os.path.exists(binary_location): - # If binary exists, log and do nothing - logger.log.info(f"BINARY EXISTS ALREADY: {binary_location}") - else: - # If binary doesn't exist, download and save it - logger.log.info("BINARY DOES NOT EXIST.") - os.makedirs(binary_directory, exist_ok=True) - response = requests.get(download_link) - if response.status_code == 200: - with open(binary_location, "wb") as f: - f.write(response.content) - os.chmod(binary_location, 0o755) - logger.log.info("BINARY DOWNLOADED SUCCESSFULLY.") - else: - logger.log.info("FAILED TO DOWNLOAD BINARY. Status code:", response.status_code) - logger.log.info("BINARIES DOWNLOAD FINISHED...") -except Exception as e: - logger.log.error(str(e)) diff --git a/contrib/docker-scripts/install_cosmovisor.py b/contrib/docker-scripts/install_cosmovisor.py deleted file mode 100644 index 568c9bbe8e..0000000000 --- a/contrib/docker-scripts/install_cosmovisor.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -import hashlib -import logging -import os -import requests -import sys - -# Constants defining the binary name, version, expected checksum, download URL, and installation path -BINARY_NAME = "cosmovisor" -BINARY_VERSION = os.getenv("COSMOVISOR_VERSION") # Get the cosmovisor version from environment variable -EXPECTED_CHECKSUM = os.getenv("COSMOVISOR_CHECKSUM") # Get the expected checksum from environment variable -BINARY_URL = f"https://binary-pickup.zetachain.com/cosmovisor-{BINARY_VERSION}-linux-amd64" # Construct the binary download URL -INSTALL_PATH = f"/usr/local/bin/{BINARY_NAME}" # Define the installation path for the binary - -# Check if necessary environment variables are set; exit if not -if not BINARY_VERSION or not EXPECTED_CHECKSUM: - logging.error("Environment variables COSMOVISOR_VERSION and COSMOVISOR_CHECKSUM must be set.") - sys.exit(1) - -# Configure logging to both stdout and a file -logging.basicConfig( - level=logging.INFO, # Set logging level to INFO - format="%(levelname)s: %(message)s", # Define log message format - handlers=[ - logging.StreamHandler(sys.stdout), # Log to stdout - logging.FileHandler("/var/log/update_cosmovisor.log") # Log to a file - ] -) - - -# Function to calculate the SHA-256 checksum of the downloaded binary -def calculate_checksum(file_path): - sha256 = hashlib.sha256() # Create a new SHA-256 hash object - with open(file_path, "rb") as f: # Open the binary file in binary read mode - for byte_block in iter(lambda: f.read(4096), - b""): # Read the file in chunks to avoid loading it all into memory - sha256.update(byte_block) # Update the hash object with the chunk - return sha256.hexdigest() # Return the hexadecimal digest of the hash object - - -# Function to download the binary and update it if the checksum matches -def download_and_update_binary(): - try: - response = requests.get(BINARY_URL) # Attempt to download the binary - response.raise_for_status() # Check if the download was successful, raises exception on failure - logging.info("Binary downloaded successfully.") - except requests.exceptions.RequestException as e: - logging.error(f"Failed to download the binary: {e}") # Log any error during download - sys.exit(1) # Exit the script on download failure - - with open(INSTALL_PATH, "wb") as f: # Open the installation path file in binary write mode - f.write(response.content) # Write the downloaded binary content to the file - - actual_checksum = calculate_checksum(INSTALL_PATH) # Calculate the checksum of the downloaded binary - if actual_checksum == EXPECTED_CHECKSUM: # Compare the actual checksum with the expected checksum - logging.info("Cosmovisor binary checksum verified.") # Log success if checksums match - os.chmod(INSTALL_PATH, 0o755) # Make the binary executable - logging.info("Cosmovisor binary updated successfully.") - else: - logging.error( - "Checksums do not match. Possible corrupted download. Deleting the downloaded binary.") # Log failure if checksums do not match - os.remove(INSTALL_PATH) # Remove the potentially corrupted binary - sys.exit(1) # Exit the script due to checksum mismatch - - -# Main script execution starts here -logging.info( - f"Downloading the {BINARY_NAME} binary (version {BINARY_VERSION})...") # Log the start of the download process -download_and_update_binary() # Call the function to download and update the binary diff --git a/contrib/docker-scripts/start.sh b/contrib/docker-scripts/start.sh deleted file mode 100644 index bb151a49fe..0000000000 --- a/contrib/docker-scripts/start.sh +++ /dev/null @@ -1,335 +0,0 @@ -#!/bin/bash - -logt() { - echo "$(date '+%Y-%m-%d %H:%M:%S') $1" -} - - -function load_defaults { - #DEFAULT: Mainnet Statesync. - export DAEMON_HOME=${DAEMON_HOME:=/root/.zetacored} - export NETWORK=${NETWORK:=mainnet} - export RESTORE_TYPE=${RESTORE_TYPE:=statesync} - export SNAPSHOT_API=${SNAPSHOT_API:=https://snapshots.rpc.zetachain.com} - export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC:=40000} - export COSMOVISOR_VERSION=${COSMOVISOR_VERSION:=v1.5.0} - export CHAIN_ID=${CHAIN_ID:=zetachain_7000-1} - export COSMOVISOR_CHECKSUM=${COSMOVISOR_CHECKSUM:=626dfc58c266b85f84a7ed8e2fe0e2346c15be98cfb9f9b88576ba899ed78cdc} - export VISOR_NAME=${VISOR_NAME:=cosmovisor} - export DAEMON_NAME=${DAEMON_NAME:=zetacored} - export DAEMON_ALLOW_DOWNLOAD_BINARIES=${DAEMON_ALLOW_DOWNLOAD_BINARIES:=false} - export DAEMON_RESTART_AFTER_UPGRADE=${DAEMON_RESTART_AFTER_UPGRADE:=true} - export UNSAFE_SKIP_BACKUP=${UNSAFE_SKIP_BACKUP:=true} - export CLIENT_DAEMON_NAME=${CLIENT_DAEMON_NAME:=zetaclientd} - export CLIENT_DAEMON_ARGS=${CLIENT_DAEMON_ARGS:""} - export CLIENT_SKIP_UPGRADE=${CLIENT_SKIP_UPGRADE:=true} - export CLIENT_START_PROCESS=${CLIENT_START_PROCESS:=false} - export MONIKER=${MONIKER:=local-test} - export RE_DO_START_SEQUENCE=${RE_DO_START_SEQUENCE:=false} - - #ATHENS3 - export BINARY_LIST_ATHENS3=${BINARY_LIST_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/binary_list.json} - export STATE_SYNC_RPC_NODE_FILE_ATHENS3=${STATE_SYNC_RPC_NODE_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/state_sync_node} - export RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3=${RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/rpc_state_sync_nodes} - export APP_TOML_FILE_ATHENS3=${APP_TOML_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/app.toml} - export CONFIG_TOML_FILE_ATHENS3=${CONFIG_TOML_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/config.toml} - export CLIENT_TOML_FILE_ATHENS3=${CLIENT_TOML_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/client.toml} - export GENESIS_FILE_ATHENS3=${GENESIS_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/genesis.json} - - #MAINNET - export BINARY_LIST_MAINNET=${BINARY_LIST_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/binary_list.json} - export STATE_SYNC_RPC_NODE_FILE_MAINNET=${STATE_SYNC_RPC_NODE_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/state_sync_node} - export RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET=${RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/rpc_state_sync_nodes} - export APP_TOML_FILE_MAINNET=${APP_TOML_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/app.toml} - export CONFIG_TOML_FILE_MAINNET=${CONFIG_TOML_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/config.toml} - export CLIENT_TOML_FILE_MAINNET=${CLIENT_TOML_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/client.toml} - export GENESIS_FILE_MAINNET=${GENESIS_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/genesis.json} - -} - -function init_chain { - if [ -d "${DAEMON_HOME}/config" ]; then - logt "${DAEMON_NAME} home directory already initialized." - else - logt "${DAEMON_NAME} home directory not initialized." - logt "MONIKER: ${MONIKER}" - logt "DAEMON_HOME: ${DAEMON_HOME}" - ${DAEMON_NAME} init ${MONIKER} --home ${DAEMON_HOME} --chain-id ${CHAIN_ID} - fi -} - -function download_configs { - if [ "${NETWORK}" == "mainnet" ]; then - wget -q ${APP_TOML_FILE_MAINNET} -O ${DAEMON_HOME}/config/app.toml - wget -q ${CONFIG_TOML_FILE_MAINNET} -O ${DAEMON_HOME}/config/config.toml - wget -q ${CLIENT_TOML_FILE_MAINNET} -O ${DAEMON_HOME}/config/client.toml - wget -q ${GENESIS_FILE_MAINNET} -O ${DAEMON_HOME}/config/genesis.json - wget -q ${BINARY_LIST_MAINNET} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - elif [ "${NETWORK}" == "athens3" ]; then - wget -q ${APP_TOML_FILE_ATHENS3} -O ${DAEMON_HOME}/config/app.toml - wget -q ${CONFIG_TOML_FILE_ATHENS3} -O ${DAEMON_HOME}/config/config.toml - wget -q ${CLIENT_TOML_FILE_ATHENS3} -O ${DAEMON_HOME}/config/client.toml - wget -q ${GENESIS_FILE_ATHENS3} -O ${DAEMON_HOME}/config/genesis.json - wget -q ${BINARY_LIST_ATHENS3} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - else - logt "Initialize for Localnet." - fi -} - -function setup_restore_type { - if [ "${RESTORE_TYPE}" == "statesync" ]; then - logt "Statesync restore. Download state sync rpc address from network-config" - if [ "${NETWORK}" == "mainnet" ]; then - logt "MAINNET STATE SYNC" - logt "STATE_SYNC_RPC_NODE_FILE_MAINNET: ${STATE_SYNC_RPC_NODE_FILE_MAINNET}" - logt "RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET: ${RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET}" - wget -q ${STATE_SYNC_RPC_NODE_FILE_MAINNET} - wget -q ${RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET} - export STATE_SYNC_SERVER=$(cat state_sync_node) - export RPC_STATE_SYNC_SERVERS=$(cat rpc_state_sync_nodes) - rm -rf state_sync_node - rm -rf rpc_state_sync_nodes - elif [ "${NETWORK}" == "athens3" ]; then - logt "ATHENS STATE SYNC" - logt "STATE_SYNC_RPC_NODE_FILE_MAINNET: ${STATE_SYNC_RPC_NODE_FILE_ATHENS3}" - logt "RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET: ${RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3}" - wget -q ${STATE_SYNC_RPC_NODE_FILE_ATHENS3} - wget -q ${RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3} - export STATE_SYNC_SERVER=$(cat state_sync_node) - export RPC_STATE_SYNC_SERVERS=$(cat rpc_state_sync_nodes) - rm -rf state_sync_node - rm -rf rpc_state_sync_nodes - fi - elif [ "${RESTORE_TYPE}" == "snapshot" ]; then - if [ "${NETWORK}" == "mainnet" ]; then - logt "Get Latest Snapshot URL" - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/mainnet/fullnode/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/mainnet/fullnode/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - elif [ "${NETWORK}" == "athens3" ]; then - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/testnet/fullnode/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/testnet/fullnode/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - fi - elif [ "${RESTORE_TYPE}" == "snapshot-archive" ]; then - if [ "${NETWORK}" == "mainnet" ]; then - logt "Get Latest Snapshot URL" - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/mainnet/archive/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/mainnet/archive/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - elif [ "${NETWORK}" == "athens3" ]; then - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/testnet/archive/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/testnet/archive/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - fi - else - logt "Initialize for Localnet." - fi -} - -function change_config_values { - if [ "${RESTORE_TYPE}" == "statesync" ]; then - export STATE_SYNC_SERVER="${STATE_SYNC_SERVER}" - export TRUST_HEIGHT=$(curl -s ${STATE_SYNC_SERVER}/block | jq -r '.result.block.header.height') - export HEIGHT=$((TRUST_HEIGHT-${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC})) - export TRUST_HASH=$(curl -s "${STATE_SYNC_SERVER}/block?height=${HEIGHT}" | jq -r '.result.block_id.hash') - export RPC_STATE_SYNC_SERVERS="${RPC_STATE_SYNC_SERVERS}" - export EXTERNAL_IP=$(curl -4 icanhazip.com) - - logt "******* DEBUG STATE SYNC VALUES *******" - logt "STATE_SYNC_SERVER: ${STATE_SYNC_SERVER}" - logt "RPC_STATE_SYNC_SERVERS: ${RPC_STATE_SYNC_SERVERS}" - logt "TRUST_HEIGHT: ${TRUST_HEIGHT}" - logt "TRUST_HASH: ${TRUST_HASH}" - logt "HEIGHT: ${HEIGHT}" - logt "EXTERNAL_IP: ${EXTERNAL_IP}" - - logt "SED Change Config Files." - sed -i -e "s/^enable = .*/enable = \"true\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^rpc_servers = .*/rpc_servers = \"${RPC_STATE_SYNC_SERVERS}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^trust_height = .*/trust_height = \"${HEIGHT}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^trust_hash = .*/trust_hash = \"${TRUST_HASH}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^moniker = .*/moniker = \"${MONIKER}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^external_address = .*/external_address = \"${EXTERNAL_IP}:26656\"/" ${DAEMON_HOME}/config/config.toml - else - export EXTERNAL_IP=$(curl -4 icanhazip.com) - logt "******* DEBUG STATE SYNC VALUES *******" - logt "EXTERNAL_IP: ${EXTERNAL_IP}" - logt "SED Change Config Files." - sed -i -e "s/^enable = .*/enable = \"true\"/" ${DAEMON_HOME}/config/config.toml - sed '/^\[statesync\]/,/^\[/ s/enable = "true"/enable = "false"/' ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^moniker = .*/moniker = \"${MONIKER}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^external_address = .*/external_address = \"${EXTERNAL_IP}:26656\"/" ${DAEMON_HOME}/config/config.toml - fi -} - -function setup_basic_keyring { - if ${DAEMON_NAME} keys show "$MONIKER" --keyring-backend test > /dev/null 2>&1; then - echo "Key $MONIKER already exists." - else - ${DAEMON_NAME} keys add "$MONIKER" --keyring-backend test - echo "Key $MONIKER created." - fi -} - -function download_binary_version { - if [ "${NETWORK}" == "mainnet" ]; then - wget -q ${BINARY_LIST_MAINNET} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - elif [ "${NETWORK}" == "athens3" ]; then - wget -q ${BINARY_LIST_ATHENS3} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - fi - python3 /scripts/download_binaries.py -} - -function move_zetacored_binaries { - mkdir -p ${DAEMON_HOME}/cosmovisor || logt "Directory already exists ${DAEMON_HOME}/cosmovisor" - mkdir -p ${DAEMON_HOME}/cosmovisor/genesis || logt "Directory already exists ${DAEMON_HOME}/cosmovisor/genesis" - mkdir -p ${DAEMON_HOME}/cosmovisor/genesis/bin || logt "Directory already exists ${DAEMON_HOME}/cosmovisor/genesis/bin" - cp /usr/local/bin/zetacored ${DAEMON_HOME}/cosmovisor/genesis/bin/zetacored - - if [ "${RESTORE_TYPE}" == "statesync" ]; then - logt "Its statesync so cosmosvisor won't know which binary to start from so make sure it starts from the latest version reported in ABCI_INFO from statesync server rpc." - export VERSION_CHECK=$(curl -s ${STATE_SYNC_SERVER}/abci_info | jq -r '.result.response.version') - logt "CURRENT VERSION_CHECK: ${VERSION_CHECK}" - cp ${DAEMON_HOME}/cosmovisor/upgrades/v${VERSION_CHECK}/bin/zetacored ${DAEMON_HOME}/cosmovisor/genesis/bin/zetacored - fi -} - -function start_network { - if [ "${IS_LOCAL_DEVELOPMENT}" == "true" ]; then - cp /usr/local/bin/zetacored ${DAEMON_HOME}/cosmovisor/genesis/bin/zetacored - find /root/.zetacored/cosmovisor/upgrades/ -type f -path "*/bin/zetacored" -exec cp /usr/local/bin/zetacored {} \; - fi - expected_major_version=$(cat /scripts/expected_major_version | cut -d '-' -f 1) - VISOR_VERSION=v$(${VISOR_NAME} version | tail -n 1 | tr -d '(devel)' | tr -d '\n') - DAEMON_VERSION=$(${DAEMON_NAME} version) - VISOR_MAJOR_VERSION=$(echo $VISOR_VERSION | grep -o '^v[0-9]*') - DAEMON_MAJOR_VERSION=$(echo $DAEMON_VERSION | grep -o '^v[0-9]*') - - logt "EXPECTED_VERSION_WITH_HASH: $(cat /scripts/expected_major_version | cut -d '-' -f 1)" - logt "EXPECTED_MAJOR_VERSION: ${expected_major_version}" - logt "VISOR_VERSION: ${VISOR_VERSION}" - logt "DAEMON_VERSION: ${DAEMON_VERSION}" - logt "VISOR_MAJOR_VERSION: ${VISOR_MAJOR_VERSION}" - logt "DAEMON_MAJOR_VERSION: ${DAEMON_MAJOR_VERSION}" - - if [ "$VISOR_MAJOR_VERSION" != "$expected_major_version" ] || [ "$DAEMON_MAJOR_VERSION" != "$expected_major_version" ]; then - logt "One or both versions don't match the expected major release version: $expected_major_version" - else - logt "Both versions match the expected major release version: $expected_major_version" - fi - - if [ "$VISOR_VERSION" != "$DAEMON_VERSION" ]; then - logt "cosmovisor version doesn't appear to match your daemon version. Start ${DAEMON_NAME}" - else - logt "cosmovisor version match your daemon version. Start ${VISOR_NAME}" - fi - - ${VISOR_NAME} run start --home ${DAEMON_HOME} \ - --log_level info \ - --moniker ${MONIKER} \ - --rpc.laddr tcp://0.0.0.0:26657 \ - --minimum-gas-prices 1.0azeta "--grpc.enable=true" -} - -logt "Load Default Values for ENV Vars if not set." -load_defaults - -if [[ -f "${DAEMON_HOME}/start_sequence_status" ]] && grep -q "START_SEQUENCE_COMPLETE" "${DAEMON_HOME}/start_sequence_status" && [[ "$RE_DO_START_SEQUENCE" != "true" ]]; then - logt "The start sequence is complete and no redo is required." - - logt "Download Configs" - download_configs - - logt "Download Historical Binaries" - download_binary_version - - if [ "${RESTORE_TYPE}" == "statesync" ]; then - logt "Setup Restore Type: ${RESTORE_TYPE}" - logt "During restarts, we re-do this to ensure to update the configs with valid values. When you call change config the stuff that gets set in this function for statesync needs to be set. Doesn't effect to re-set this." - setup_restore_type - fi - - logt "Modify Chain Configs" - change_config_values - - logt "Move Zetacored Binaries." - move_zetacored_binaries - - logt "Start sequence has completed, echo into file so on restart it doesn't download snapshots again." - echo "START_SEQUENCE_COMPLETE" >> ${DAEMON_HOME}/start_sequence_status - - logt "Start Network" - start_network -else - logt "START_SEQUENCE_COMPLETE is not true, or RE_DO_START_SEQUENCE is set to true." - - if [[ "$RE_DO_START_SEQUENCE" == "true" ]]; then - logt "Clean any files that may exist in: ${DAEMON_HOME}" - rm -rf ${DAEMON_HOME}/* || logt "directory doesn't exist." - fi - - logt "Init Chain" - init_chain - - logt "Download Configs" - download_configs - - logt "Download Historical Binaries" - download_binary_version - - logt "Setup Restore Type: ${RESTORE_TYPE}" - setup_restore_type - - logt "Modify Chain Configs" - change_config_values - - logt "Move root binaries to current" - move_zetacored_binaries - - logt "Start sequence has completed, echo into file so on restart it doesn't download snapshots again." - echo "START_SEQUENCE_COMPLETE" >> ${DAEMON_HOME}/start_sequence_status - - logt "Start Network" - start_network -fi \ No newline at end of file diff --git a/contrib/rpc/zetacored/docker-compose.yml b/contrib/rpc/zetacored/docker-compose.yml deleted file mode 100644 index 50dbd0a731..0000000000 --- a/contrib/rpc/zetacored/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: '3.8' -services: - -=name=-: - platform: linux/amd64 - -=image_block=- - container_name: "zetachain_${NETWORK:-mainnet}_rpc" - environment: - DAEMON_HOME: "${DAEMON_HOME:-/root/.zetacored}" - NETWORK: ${NETWORK:-mainnet} - RESTORE_TYPE: "${RESTORE_TYPE:-snapshot}" - SNAPSHOT_API: ${SNAPSHOT_API:-https://snapshots.rpc.zetachain.com} - TRUST_HEIGHT_DIFFERENCE_STATE_SYNC: ${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC:-40000} - CHAIN_ID: "${CHAIN_ID:-zetachain_7000-1}" - VISOR_NAME: "${VISOR_NAME:-cosmovisor}" - DAEMON_NAME: "${DAEMON_NAME:-zetacored}" - DAEMON_ALLOW_DOWNLOAD_BINARIES: "${DAEMON_ALLOW_DOWNLOAD_BINARIES:-false}" - DAEMON_RESTART_AFTER_UPGRADE: "${DAEMON_RESTART_AFTER_UPGRADE:-true}" - UNSAFE_SKIP_BACKUP: "${UNSAFE_SKIP_BACKUP:-true}" - MONIKER: ${MONIKER:-mainnet-docker-rpc} - #If this is true it will erase everything and start over from scratch. - RE_DO_START_SEQUENCE: "${RE_DO_START_SEQUENCE:-false}" - #If this is true it will build the dockerfile and use binary from built docker file instead of remote docker image for local development testing on non-governance upgrades. - IS_LOCAL_DEVELOPMENT: "${IS_LOCAL_DEVELOPMENT:-false}" - ports: - - "26656:26656" - - "1317:1317" - - "8545:8545" - - "8546:8546" - - "26657:26657" - - "9090:9090" - - "9091:9091" - volumes: - - -=name=-:/root/.zetacored/ - entrypoint: bash /scripts/start.sh -volumes: - -=name=-: diff --git a/contrib/rpc/zetacored/init_docker_compose.sh b/contrib/rpc/zetacored/init_docker_compose.sh deleted file mode 100644 index 04e9f6857c..0000000000 --- a/contrib/rpc/zetacored/init_docker_compose.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -NETWORK=${1} -TYPE=${2} -DOCKER_TAG=${3} - -if [ "$TYPE" == "image" ]; then - echo "Source Environment File." - SOURCE_FILE_NAME="networks/.${NETWORK}" - if [ ! -f "$SOURCE_FILE_NAME" ]; then - echo "Environment file $SOURCE_FILE_NAME does not exist." - exit 1 - fi - source ${SOURCE_FILE_NAME} -elif [ "$TYPE" == "localbuild" ]; then - echo "Source Environment File." - SOURCE_FILE_NAME="networks/.${NETWORK}-localbuild" - if [ ! -f "$SOURCE_FILE_NAME" ]; then - echo "Environment file $SOURCE_FILE_NAME does not exist." - exit 1 - fi - source ${SOURCE_FILE_NAME} -fi - -# Define the path to the Docker Compose file -FILE_PATH="${NETWORK}-docker-compose.yml" -cp docker-compose.yml ${FILE_PATH} - -# Determine the appropriate Docker Compose configuration based on TYPE -if [ "$TYPE" == "image" ]; then - IMAGE_BLOCK="image: zetachain/zetacored:\${DOCKER_TAG:-ubuntu-v14.0.1.0}" - NAME="zetacored-rpc-${NETWORK}" -elif [ "$TYPE" == "localbuild" ]; then - IMAGE_BLOCK=$(cat << 'EOF' -build: - context: ../../.. - dockerfile: Dockerfile -EOF -) - NAME="zetacored-rpc-${NETWORK}-localbuild" -else - echo "Invalid TYPE. Please specify 'image' or 'localbuild'." - exit 1 -fi - -IMAGE_BLOCK_ESCAPED=$(echo "$IMAGE_BLOCK" | sed 's/[&/]/\\&/g; s/$/\\/') -IMAGE_BLOCK_ESCAPED=${IMAGE_BLOCK_ESCAPED%?} - -# Replace placeholders in the Docker Compose file -sed -i '' "s|-=name=-|$NAME|g" $FILE_PATH -sed -i '' "s|-=image_block=-|$IMAGE_BLOCK_ESCAPED|g" $FILE_PATH - -echo "DEBUG ENV VARS" -printenv -echo "================" - -echo "Placeholders have been replaced in $FILE_PATH." -cat $FILE_PATH -echo "================" - -if [ "$TYPE" == "image" ]; then - docker-compose -f ${FILE_PATH} up -elif [ "$TYPE" == "localbuild" ]; then - docker-compose -f ${FILE_PATH} up --build -fi diff --git a/contrib/rpc/zetacored/kill_docker_compose.sh b/contrib/rpc/zetacored/kill_docker_compose.sh deleted file mode 100644 index 5d6a2c192d..0000000000 --- a/contrib/rpc/zetacored/kill_docker_compose.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -NETWORK=${1} -CLEAN=${2} -FILE_PATH="${NETWORK}-docker-compose.yml" - -if [ "${CLEAN}" == "true" ]; then - docker-compose -f ${FILE_PATH} down -v - rm -rf ${FILE_PATH} -else - docker-compose -f ${FILE_PATH} down - rm -rf ${FILE_PATH} -fi - diff --git a/contrib/rpc/zetacored/networks/.athens3 b/contrib/rpc/zetacored/networks/.athens3 deleted file mode 100644 index 7de4cede3b..0000000000 --- a/contrib/rpc/zetacored/networks/.athens3 +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=athens3 -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="athens_7001-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="true" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=testnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="false" diff --git a/contrib/rpc/zetacored/networks/.athens3-localbuild b/contrib/rpc/zetacored/networks/.athens3-localbuild deleted file mode 100644 index b79c14c220..0000000000 --- a/contrib/rpc/zetacored/networks/.athens3-localbuild +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=athens3 -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="athens_7001-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="false" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=testnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="true" diff --git a/contrib/rpc/zetacored/networks/.mainnet b/contrib/rpc/zetacored/networks/.mainnet deleted file mode 100644 index ff260bb5ca..0000000000 --- a/contrib/rpc/zetacored/networks/.mainnet +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=mainnet -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="zetachain_7000-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="true" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=mainnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="false" diff --git a/contrib/rpc/zetacored/networks/.mainnet-localbuild b/contrib/rpc/zetacored/networks/.mainnet-localbuild deleted file mode 100644 index 381c34bd6d..0000000000 --- a/contrib/rpc/zetacored/networks/.mainnet-localbuild +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=mainnet -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="zetachain_7000-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="false" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=mainnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="true" From 148935c27a5d70a9402b2f5b103d1853b2c7a31f Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Wed, 8 Jan 2025 18:15:58 +0100 Subject: [PATCH 07/10] chore: prepare v25 (#3338) * disable precompiles * complete changelog * set v24 as base for upgrade tests * lint error --- Makefile | 4 ++-- changelog.md | 5 ++--- cmd/zetae2e/local/local.go | 28 +++++++++++++++------------- precompiles/precompiles.go | 6 +++--- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 344ae150e4..02ac397d51 100644 --- a/Makefile +++ b/Makefile @@ -327,7 +327,7 @@ ifdef UPGRADE_TEST_FROM_SOURCE zetanode-upgrade: e2e-images @echo "Building zetanode-upgrade from source" $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime-source \ - --build-arg OLD_VERSION='release/v23' \ + --build-arg OLD_VERSION='release/v24' \ --build-arg NODE_VERSION=$(NODE_VERSION) \ --build-arg NODE_COMMIT=$(NODE_COMMIT) . @@ -336,7 +336,7 @@ else zetanode-upgrade: e2e-images @echo "Building zetanode-upgrade from binaries" $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime \ - --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v23.1.5' \ + --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v24.0.0' \ --build-arg NODE_VERSION=$(NODE_VERSION) \ --build-arg NODE_COMMIT=$(NODE_COMMIT) \ . diff --git a/changelog.md b/changelog.md index 5bb11d40cf..f878b67b3c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,12 @@ # CHANGELOG -## Unreleased +## v25.0.0 ### Features * [3235](https://github.com/zeta-chain/node/pull/3235) - add /systemtime telemetry endpoint (zetaclient) * [3317](https://github.com/zeta-chain/node/pull/3317) - add configurable signer latency correction (zetaclient) - ### Tests * [3205](https://github.com/zeta-chain/node/issues/3205) - move Bitcoin revert address test to advanced group to avoid upgrade test failure @@ -15,7 +14,7 @@ * [3095](https://github.com/zeta-chain/node/pull/3095) - initialize simulation tests for custom zetachain modules * [3276](https://github.com/zeta-chain/node/pull/3276) - add Solana E2E performance tests and improve Solana outbounds performance -## Refactor +### Refactor * [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient * [3291](https://github.com/zeta-chain/node/pull/3291) - revamp zetaclient initialization (+ graceful shutdown) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 8368e0d0bc..e32bfc0d77 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -338,23 +338,25 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipPrecompiles { precompiledContractTests := []string{ - e2etests.TestPrecompilesPrototypeName, - e2etests.TestPrecompilesPrototypeThroughContractName, - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - // e2etests.TestPrecompilesStakingThroughContractName, - e2etests.TestPrecompilesBankName, - e2etests.TestPrecompilesBankFailName, - e2etests.TestPrecompilesBankThroughContractName, + //e2etests.TestPrecompilesPrototypeName, + //e2etests.TestPrecompilesPrototypeThroughContractName, + //// Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + //// e2etests.TestPrecompilesStakingThroughContractName, + //e2etests.TestPrecompilesBankName, + //e2etests.TestPrecompilesBankFailName, + //e2etests.TestPrecompilesBankThroughContractName, } if e2eStartHeight < 100 { // these tests require a clean system // since unstaking has an unbonding period - precompiledContractTests = append(precompiledContractTests, - e2etests.TestPrecompilesStakingName, - e2etests.TestPrecompilesDistributeName, - e2etests.TestPrecompilesDistributeNonZRC20Name, - e2etests.TestPrecompilesDistributeThroughContractName, - ) + //precompiledContractTests = append(precompiledContractTests, + // e2etests.TestPrecompilesStakingName, + // e2etests.TestPrecompilesDistributeName, + // e2etests.TestPrecompilesDistributeNonZRC20Name, + // e2etests.TestPrecompilesDistributeThroughContractName, + //) + // prevent lint error + _ = precompiledContractTests } else { logger.Print("⚠️ partial precompiled run (unclean state)") } diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index 3c9a066812..557d880ab4 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -22,9 +22,9 @@ import ( // This is useful for listing and reading from other packages, such as BlockedAddrs() function. // Setting to false a contract here will disable it, not being included in the blockchain. var EnabledStatefulContracts = map[common.Address]bool{ - prototype.ContractAddress: true, - staking.ContractAddress: true, - bank.ContractAddress: true, + prototype.ContractAddress: false, + staking.ContractAddress: false, + bank.ContractAddress: false, } // StatefulContracts returns all the registered precompiled contracts. From 47c9444bbd115176a2736f31be8cb7d23b0059e7 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 8 Jan 2025 09:45:22 -0800 Subject: [PATCH 08/10] feat: add zetaclient minimum version check (#3320) * feat: add minimum version check * review feedback * changelog --- changelog.md | 1 + cmd/zetaclientd/start.go | 27 ++++-- docs/openapi/openapi.swagger.yaml | 6 ++ .../zetacore/observer/operational.proto | 5 + .../zetacore/observer/operational_pb.d.ts | 9 ++ x/observer/types/errors.go | 5 + x/observer/types/operational.go | 4 + x/observer/types/operational.pb.go | 94 +++++++++++++++---- x/observer/types/operational_test.go | 18 ++++ zetaclient/maintenance/shutdown_listener.go | 65 +++++++++++-- .../maintenance/shutdown_listener_test.go | 59 +++++++++++- 11 files changed, 256 insertions(+), 37 deletions(-) diff --git a/changelog.md b/changelog.md index f878b67b3c..4442a0cbba 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ * [3235](https://github.com/zeta-chain/node/pull/3235) - add /systemtime telemetry endpoint (zetaclient) * [3317](https://github.com/zeta-chain/node/pull/3317) - add configurable signer latency correction (zetaclient) +* [3320](https://github.com/zeta-chain/node/pull/3320) - add zetaclient minimum version check ### Tests diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 2a46dabdbd..ae7da7e6ac 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -83,6 +83,20 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to resolve observer pub key bech32") } + isObserver, err := isObserverNode(ctx, zetacoreClient) + switch { + case err != nil: + return errors.Wrap(err, "unable to check if observer node") + case !isObserver: + logger.Std.Warn().Msg("This node is not an observer node. Exit 0") + return nil + } + + shutdownListener := maintenance.NewShutdownListener(zetacoreClient, logger.Std) + if err := shutdownListener.RunPreStartCheck(ctx); err != nil { + return errors.Wrap(err, "pre start check failed") + } + tssSetupProps := zetatss.SetupProps{ Config: cfg, Zetacore: zetacoreClient, @@ -94,20 +108,13 @@ func Start(_ *cobra.Command, _ []string) error { Telemetry: telemetry, } + // This will start p2p communication so it should only happen after + // preflight checks have completed tss, err := zetatss.Setup(ctx, tssSetupProps, logger.Std) if err != nil { return errors.Wrap(err, "unable to setup TSS service") } - isObserver, err := isObserverNode(ctx, zetacoreClient) - switch { - case err != nil: - return errors.Wrap(err, "unable to check if observer node") - case !isObserver: - logger.Std.Warn().Msg("This node is not an observer node. Exit 0") - return nil - } - // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, logger.Std).Listen(ctx, func() { @@ -115,7 +122,7 @@ func Start(_ *cobra.Command, _ []string) error { graceful.ShutdownNow() }) - maintenance.NewShutdownListener(zetacoreClient, logger.Std).Listen(ctx, func() { + shutdownListener.Listen(ctx, func() { logger.Std.Info().Msg("Shutdown listener received an action to shutdown zetaclientd.") graceful.ShutdownNow() }) diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index 694f839db6..11756a1c7b 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -58115,6 +58115,12 @@ definitions: description: |- Offset from the zetacore block time to initiate signing. Should be calculated and set based on max(zetaclient_core_block_latency). + minimum_version: + type: string + description: |- + Minimum version of zetaclient that is allowed to run. This must be either + a valid semver string (v23.0.1) or empty. If empty, all versions are + allowed. description: Flags for the top-level operation of zetaclient. observerPendingNonces: type: object diff --git a/proto/zetachain/zetacore/observer/operational.proto b/proto/zetachain/zetacore/observer/operational.proto index 62ff737074..05096a57c3 100644 --- a/proto/zetachain/zetacore/observer/operational.proto +++ b/proto/zetachain/zetacore/observer/operational.proto @@ -16,4 +16,9 @@ message OperationalFlags { // Should be calculated and set based on max(zetaclient_core_block_latency). google.protobuf.Duration signer_block_time_offset = 2 [ (gogoproto.stdduration) = true ]; + + // Minimum version of zetaclient that is allowed to run. This must be either + // a valid semver string (v23.0.1) or empty. If empty, all versions are + // allowed. + string minimum_version = 3; } \ No newline at end of file diff --git a/typescript/zetachain/zetacore/observer/operational_pb.d.ts b/typescript/zetachain/zetacore/observer/operational_pb.d.ts index c9fc213927..4f18ce91a0 100644 --- a/typescript/zetachain/zetacore/observer/operational_pb.d.ts +++ b/typescript/zetachain/zetacore/observer/operational_pb.d.ts @@ -28,6 +28,15 @@ export declare class OperationalFlags extends Message { */ signerBlockTimeOffset?: Duration; + /** + * Minimum version of zetaclient that is allowed to run. This must be either + * a valid semver string (v23.0.1) or empty. If empty, all versions are + * allowed. + * + * @generated from field: string minimum_version = 3; + */ + minimumVersion: string; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; diff --git a/x/observer/types/errors.go b/x/observer/types/errors.go index fbc6261d76..aaebb297b4 100644 --- a/x/observer/types/errors.go +++ b/x/observer/types/errors.go @@ -71,4 +71,9 @@ var ( 1140, "signer block time offset exceeds limit", ) + ErrOperationalFlagsInvalidMinimumVersion = errorsmod.Register( + ModuleName, + 1141, + "minimum version is not a valid semver string", + ) ) diff --git a/x/observer/types/operational.go b/x/observer/types/operational.go index 4da766c53a..e90415acdf 100644 --- a/x/observer/types/operational.go +++ b/x/observer/types/operational.go @@ -4,6 +4,7 @@ import ( "time" cosmoserrors "cosmossdk.io/errors" + "golang.org/x/mod/semver" ) const ( @@ -23,5 +24,8 @@ func (f *OperationalFlags) Validate() error { return cosmoserrors.Wrapf(ErrOperationalFlagsSignerBlockTimeOffsetLimit, "(%s)", signerBlockTimeOffset) } } + if f.MinimumVersion != "" && !semver.IsValid(f.MinimumVersion) { + return ErrOperationalFlagsInvalidMinimumVersion + } return nil } diff --git a/x/observer/types/operational.pb.go b/x/observer/types/operational.pb.go index e5fb9c3281..a23aed758b 100644 --- a/x/observer/types/operational.pb.go +++ b/x/observer/types/operational.pb.go @@ -35,6 +35,10 @@ type OperationalFlags struct { // Offset from the zetacore block time to initiate signing. // Should be calculated and set based on max(zetaclient_core_block_latency). SignerBlockTimeOffset *time.Duration `protobuf:"bytes,2,opt,name=signer_block_time_offset,json=signerBlockTimeOffset,proto3,stdduration" json:"signer_block_time_offset,omitempty"` + // Minimum version of zetaclient that is allowed to run. This must be either + // a valid semver string (v23.0.1) or empty. If empty, all versions are + // allowed. + MinimumVersion string `protobuf:"bytes,3,opt,name=minimum_version,json=minimumVersion,proto3" json:"minimum_version,omitempty"` } func (m *OperationalFlags) Reset() { *m = OperationalFlags{} } @@ -84,6 +88,13 @@ func (m *OperationalFlags) GetSignerBlockTimeOffset() *time.Duration { return nil } +func (m *OperationalFlags) GetMinimumVersion() string { + if m != nil { + return m.MinimumVersion + } + return "" +} + func init() { proto.RegisterType((*OperationalFlags)(nil), "zetachain.zetacore.observer.OperationalFlags") } @@ -93,25 +104,27 @@ func init() { } var fileDescriptor_ea3eed2ec55093b5 = []byte{ - // 282 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0xc1, 0x4a, 0xec, 0x30, - 0x14, 0x86, 0x27, 0xf7, 0x8a, 0x8b, 0x8a, 0x22, 0x83, 0x42, 0x1d, 0x21, 0x0e, 0x82, 0x30, 0x20, - 0x93, 0x80, 0xbe, 0x41, 0x51, 0x71, 0x37, 0x30, 0xb8, 0x10, 0x37, 0x25, 0xed, 0x9c, 0xa6, 0xc1, - 0xb6, 0xa7, 0x24, 0xa9, 0xa8, 0x4f, 0x21, 0xb8, 0xf1, 0x91, 0x5c, 0xce, 0xd2, 0x9d, 0xd2, 0xbe, - 0x88, 0x4c, 0x32, 0x75, 0x76, 0x27, 0x27, 0xdf, 0x7f, 0x3e, 0xf8, 0x83, 0xe9, 0x2b, 0x58, 0x91, - 0xe6, 0x42, 0x55, 0xdc, 0x4d, 0xa8, 0x81, 0x63, 0x62, 0x40, 0x3f, 0x81, 0xe6, 0x58, 0x83, 0x16, - 0x56, 0x61, 0x25, 0x0a, 0x56, 0x6b, 0xb4, 0x38, 0x3c, 0xfe, 0xc3, 0x59, 0x8f, 0xb3, 0x1e, 0x1f, - 0x1d, 0x48, 0x94, 0xe8, 0x38, 0xbe, 0x9a, 0x7c, 0x64, 0x44, 0x25, 0xa2, 0x2c, 0x80, 0xbb, 0x57, - 0xd2, 0x64, 0x7c, 0xd1, 0xf8, 0xa3, 0xfe, 0xff, 0xf4, 0x9d, 0x04, 0xfb, 0xb3, 0x8d, 0xe8, 0xa6, - 0x10, 0xd2, 0x0c, 0xcf, 0x82, 0x3d, 0x0d, 0xc6, 0x0a, 0x6d, 0xe3, 0x1c, 0x94, 0xcc, 0x6d, 0x48, - 0xc6, 0x64, 0xf2, 0x7f, 0xbe, 0xbb, 0xde, 0xde, 0xba, 0xe5, 0xf0, 0x3e, 0x08, 0x8d, 0x92, 0x15, - 0xe8, 0x38, 0x29, 0x30, 0x7d, 0x8c, 0xad, 0x2a, 0x21, 0xc6, 0x2c, 0x33, 0x60, 0xc3, 0x7f, 0x63, - 0x32, 0xd9, 0xb9, 0x38, 0x62, 0x5e, 0xcf, 0x7a, 0x3d, 0xbb, 0x5a, 0xeb, 0xa3, 0xad, 0x8f, 0xef, - 0x13, 0x32, 0x3f, 0xf4, 0x07, 0xa2, 0x55, 0xfe, 0x4e, 0x95, 0x30, 0x73, 0xe9, 0xe8, 0xfa, 0xb3, - 0xa5, 0x64, 0xd9, 0x52, 0xf2, 0xd3, 0x52, 0xf2, 0xd6, 0xd1, 0xc1, 0xb2, 0xa3, 0x83, 0xaf, 0x8e, - 0x0e, 0x1e, 0xce, 0xa5, 0xb2, 0x79, 0x93, 0xb0, 0x14, 0x4b, 0x57, 0xd9, 0xd4, 0xb7, 0x57, 0xe1, - 0x02, 0xf8, 0xf3, 0xa6, 0x3b, 0xfb, 0x52, 0x83, 0x49, 0xb6, 0x9d, 0xf6, 0xf2, 0x37, 0x00, 0x00, - 0xff, 0xff, 0xf0, 0xe0, 0x3c, 0x60, 0x67, 0x01, 0x00, 0x00, + // 308 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0x41, 0x4a, 0x03, 0x31, + 0x14, 0x86, 0x1b, 0x2b, 0x82, 0x23, 0x56, 0x19, 0x14, 0xc6, 0x0a, 0xb1, 0x08, 0x62, 0x41, 0x9a, + 0x80, 0xde, 0xa0, 0xa8, 0xb8, 0x2b, 0x14, 0x11, 0x71, 0x33, 0xcc, 0xb4, 0xaf, 0x99, 0xe0, 0x24, + 0xaf, 0x24, 0x99, 0xa2, 0x9e, 0xc2, 0xa5, 0x87, 0xf1, 0x00, 0x2e, 0xbb, 0x74, 0xa7, 0xb4, 0x17, + 0x91, 0x26, 0xad, 0xdd, 0xbd, 0xfc, 0xf9, 0xff, 0xf7, 0x3d, 0xfe, 0xa8, 0xf3, 0x06, 0x2e, 0x1b, + 0x14, 0x99, 0xd4, 0xdc, 0x4f, 0x68, 0x80, 0x63, 0x6e, 0xc1, 0x4c, 0xc0, 0x70, 0x1c, 0x83, 0xc9, + 0x9c, 0x44, 0x9d, 0x95, 0x6c, 0x6c, 0xd0, 0x61, 0x7c, 0xfc, 0x6f, 0x67, 0x2b, 0x3b, 0x5b, 0xd9, + 0x9b, 0x07, 0x02, 0x05, 0x7a, 0x1f, 0x5f, 0x4c, 0x21, 0xd2, 0xa4, 0x02, 0x51, 0x94, 0xc0, 0xfd, + 0x2b, 0xaf, 0x46, 0x7c, 0x58, 0x85, 0xa5, 0xe1, 0xff, 0xf4, 0x93, 0x44, 0xfb, 0xbd, 0x35, 0xe8, + 0xb6, 0xcc, 0x84, 0x8d, 0xcf, 0xa2, 0x86, 0x01, 0xeb, 0x32, 0xe3, 0xd2, 0x02, 0xa4, 0x28, 0x5c, + 0x42, 0x5a, 0xa4, 0x5d, 0xef, 0xef, 0x2e, 0xd5, 0x3b, 0x2f, 0xc6, 0x8f, 0x51, 0x62, 0xa5, 0xd0, + 0x60, 0xd2, 0xbc, 0xc4, 0xc1, 0x73, 0xea, 0xa4, 0x82, 0x14, 0x47, 0x23, 0x0b, 0x2e, 0xd9, 0x68, + 0x91, 0xf6, 0xce, 0xe5, 0x11, 0x0b, 0x78, 0xb6, 0xc2, 0xb3, 0xeb, 0x25, 0xbe, 0xbb, 0xf9, 0xf1, + 0x73, 0x42, 0xfa, 0x87, 0x61, 0x41, 0x77, 0x91, 0xbf, 0x97, 0x0a, 0x7a, 0x3e, 0x1d, 0x9f, 0x47, + 0x7b, 0x4a, 0x6a, 0xa9, 0x2a, 0x95, 0x4e, 0xc0, 0x58, 0x89, 0x3a, 0xa9, 0xb7, 0x48, 0x7b, 0xbb, + 0xdf, 0x58, 0xca, 0x0f, 0x41, 0xed, 0xde, 0x7c, 0xcd, 0x28, 0x99, 0xce, 0x28, 0xf9, 0x9d, 0x51, + 0xf2, 0x3e, 0xa7, 0xb5, 0xe9, 0x9c, 0xd6, 0xbe, 0xe7, 0xb4, 0xf6, 0x74, 0x21, 0xa4, 0x2b, 0xaa, + 0x9c, 0x0d, 0x50, 0xf9, 0x6e, 0x3b, 0xa1, 0x66, 0x8d, 0x43, 0xe0, 0x2f, 0xeb, 0x92, 0xdd, 0xeb, + 0x18, 0x6c, 0xbe, 0xe5, 0xef, 0xbb, 0xfa, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x88, 0xe2, 0xfd, + 0x90, 0x01, 0x00, 0x00, } func (m *OperationalFlags) Marshal() (dAtA []byte, err error) { @@ -134,6 +147,13 @@ func (m *OperationalFlags) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.MinimumVersion) > 0 { + i -= len(m.MinimumVersion) + copy(dAtA[i:], m.MinimumVersion) + i = encodeVarintOperational(dAtA, i, uint64(len(m.MinimumVersion))) + i-- + dAtA[i] = 0x1a + } if m.SignerBlockTimeOffset != nil { n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.SignerBlockTimeOffset, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset):]) if err1 != nil { @@ -176,6 +196,10 @@ func (m *OperationalFlags) Size() (n int) { l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset) n += 1 + l + sovOperational(uint64(l)) } + l = len(m.MinimumVersion) + if l > 0 { + n += 1 + l + sovOperational(uint64(l)) + } return n } @@ -269,6 +293,38 @@ func (m *OperationalFlags) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MinimumVersion", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowOperational + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthOperational + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthOperational + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MinimumVersion = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOperational(dAtA[iNdEx:]) diff --git a/x/observer/types/operational_test.go b/x/observer/types/operational_test.go index 79c8caf0dc..ca0e6056b8 100644 --- a/x/observer/types/operational_test.go +++ b/x/observer/types/operational_test.go @@ -15,6 +15,10 @@ func TestOperationalFlags_Validate(t *testing.T) { of types.OperationalFlags errContains string }{ + { + name: "empty is valid", + of: types.OperationalFlags{}, + }, { name: "invalid restart height", of: types.OperationalFlags{ @@ -48,11 +52,25 @@ func TestOperationalFlags_Validate(t *testing.T) { }, errContains: types.ErrOperationalFlagsSignerBlockTimeOffsetLimit.Error(), }, + { + name: "minimum version valid", + of: types.OperationalFlags{ + MinimumVersion: "v1.1.1", + }, + }, + { + name: "minimum version invalid", + of: types.OperationalFlags{ + MinimumVersion: "asdf", + }, + errContains: types.ErrOperationalFlagsInvalidMinimumVersion.Error(), + }, { name: "all flags valid", of: types.OperationalFlags{ RestartHeight: 1, SignerBlockTimeOffset: ptr.Ptr(time.Second), + MinimumVersion: "v1.1.1", }, }, } diff --git a/zetaclient/maintenance/shutdown_listener.go b/zetaclient/maintenance/shutdown_listener.go index 0ac3b18d75..edbd93bedd 100644 --- a/zetaclient/maintenance/shutdown_listener.go +++ b/zetaclient/maintenance/shutdown_listener.go @@ -2,12 +2,16 @@ package maintenance import ( "context" + "fmt" + "strings" "time" "cosmossdk.io/errors" "github.com/rs/zerolog" + "golang.org/x/mod/semver" "github.com/zeta-chain/node/pkg/bg" + "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/retry" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -22,17 +26,30 @@ type ShutdownListener struct { logger zerolog.Logger lastRestartHeightMissed int64 + // get the current version of zetaclient + getVersion func() string } // NewShutdownListener creates a new ShutdownListener. func NewShutdownListener(client interfaces.ZetacoreClient, logger zerolog.Logger) *ShutdownListener { log := logger.With().Str("module", "shutdown_listener").Logger() return &ShutdownListener{ - client: client, - logger: log, + client: client, + logger: log, + getVersion: getVersionDefault, } } +// RunPreStartCheck runs any checks that must run before fully starting zetaclient. +// Specifically this should be run before any TSS P2P is started. +func (o *ShutdownListener) RunPreStartCheck(ctx context.Context) error { + operationalFlags, err := o.getOperationalFlagsWithRetry(ctx) + if err != nil { + return errors.Wrap(err, "unable to get initial operational flags") + } + return o.checkMinimumVersion(operationalFlags) +} + func (o *ShutdownListener) Listen(ctx context.Context, action func()) { var ( withLogger = bg.WithLogger(o.logger) @@ -43,12 +60,9 @@ func (o *ShutdownListener) Listen(ctx context.Context, action func()) { } func (o *ShutdownListener) waitForUpdate(ctx context.Context) error { - operationalFlags, err := retry.DoTypedWithBackoffAndRetry( - func() (observertypes.OperationalFlags, error) { return o.client.GetOperationalFlags(ctx) }, - retry.DefaultConstantBackoff(), - ) + operationalFlags, err := o.getOperationalFlagsWithRetry(ctx) if err != nil { - return errors.Wrap(err, "unable to get initial operational flags") + return errors.Wrap(err, "get initial operational flags") } if o.handleNewFlags(ctx, operationalFlags) { return nil @@ -74,8 +88,19 @@ func (o *ShutdownListener) waitForUpdate(ctx context.Context) error { } } +func (o *ShutdownListener) getOperationalFlagsWithRetry(ctx context.Context) (observertypes.OperationalFlags, error) { + return retry.DoTypedWithBackoffAndRetry( + func() (observertypes.OperationalFlags, error) { return o.client.GetOperationalFlags(ctx) }, + retry.DefaultConstantBackoff(), + ) +} + // handleNewFlags processes the flags and returns true if a shutdown should be signaled func (o *ShutdownListener) handleNewFlags(ctx context.Context, f observertypes.OperationalFlags) bool { + if err := o.checkMinimumVersion(f); err != nil { + o.logger.Error().Err(err).Any("operational_flags", f).Msg("minimum version check") + return true + } if f.RestartHeight < 1 { return false } @@ -123,3 +148,29 @@ func (o *ShutdownListener) handleNewFlags(ctx context.Context, f observertypes.O } } } + +func (o *ShutdownListener) checkMinimumVersion(f observertypes.OperationalFlags) error { + if f.MinimumVersion != "" { + // we typically store the version without the required v prefix + currentVersion := ensurePrefix(o.getVersion(), "v") + if semver.Compare(currentVersion, f.MinimumVersion) == -1 { + return fmt.Errorf( + "current version (%s) is less than minimum version (%s)", + currentVersion, + f.MinimumVersion, + ) + } + } + return nil +} + +func getVersionDefault() string { + return constant.Version +} + +func ensurePrefix(s, prefix string) string { + if !strings.HasPrefix(s, prefix) { + return prefix + s + } + return s +} diff --git a/zetaclient/maintenance/shutdown_listener_test.go b/zetaclient/maintenance/shutdown_listener_test.go index 2b2c9128d1..c85c70cde0 100644 --- a/zetaclient/maintenance/shutdown_listener_test.go +++ b/zetaclient/maintenance/shutdown_listener_test.go @@ -100,8 +100,65 @@ func TestShutdownListener(t *testing.T) { return len(client.Calls) == 2 }, time.Second, time.Millisecond) assertChannelNotClosed(t, complete) - cancel() }) + + t.Run("minimum version ok", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + listener := NewShutdownListener(client, logger) + listener.getVersion = func() string { + return "1.1.2" + } + + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + MinimumVersion: "v1.1.1", + }, nil) + + // pre start checks passed + err := listener.RunPreStartCheck(ctx) + require.NoError(t, err) + + // listener also does not shutdown + complete := make(chan interface{}) + listener.Listen(ctx, func() { + close(complete) + }) + + require.Eventually(t, func() bool { + return len(client.Calls) == 2 + }, time.Second, time.Millisecond) + assertChannelNotClosed(t, complete) + }) + + t.Run("minimum version failed", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + listener := NewShutdownListener(client, logger) + listener.getVersion = func() string { + return "1.1.1" + } + + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + MinimumVersion: "v1.1.2", + }, nil) + + // pre start checks would return error + err := listener.RunPreStartCheck(ctx) + require.Error(t, err) + + // listener would also shutdown + complete := make(chan interface{}) + listener.Listen(ctx, func() { + close(complete) + }) + + require.Eventually(t, func() bool { + return len(client.Calls) == 2 + }, time.Second, time.Millisecond) + <-complete + }) + // avoid Log in goroutine after TestShutdownListener has completed + cancel() time.Sleep(time.Millisecond * 100) } From 2edfa9c42f4d9e2fa4d86daaf12b316acdbc85d2 Mon Sep 17 00:00:00 2001 From: Tanmay Date: Fri, 10 Jan 2025 11:25:13 -0500 Subject: [PATCH 09/10] test: add crosschain and observer operations (#3207) * add crosschain decoders * decoders test * add oeprations * add fungible deploy contracts * add fungible deploy contracts * add simulation for observer module * add comments for crosschain operations * add comments for observer and fungible operations * generate files * debug import export test * fix import export tests * fix import export tests * fix app determinism test * update codecov.yml * reduce weight for DeployedSystemContracts operation * add new function to generate eth address from provided randomness * Update x/fungible/simulation/decoders.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update testutil/sample/sample.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * add validations * add validations * add randmoness to tss * make requested changes 1 * update state.go file * update state.go file * change chains.IsEVMChain to chains.IsEthereumChain in deposit tests * add basic structure for outbound vote message * add randomised outbound message * add cointype randomisation to inbound message * add cointype gas * add cointype erc20 * add outbound tracker * improve outbound tracker nonce selection * remove block limit for outbound tracker * add operation remove outbound tracker * add operation whitelist erc20 * fix unit tests * fix unit tests * fix Abort CCTX test * add operation refund aborted cctx * add operation update rate limiter flags * add operation UpdateErc20PauseStatus * add additional checks to whitelist erc20 * add default gas prices to the state * add default gas prices to the state * update zeta accounting genesis * update operation whitelist erc20 to not try whitelisting duplicate assets * add check for aborted status when finalizing an outbound * add check for aborted status when finalizing an outbound * reduce errors for RefundAbortedCCTX * reduce errors for RefundAbortedCCTX * reafactor update TSS to use existing cctx * remove setting nonce to cctx twice * set observe count for even is observer set is empty * add updte keygen message * add update chain params * add SimulateMsgResetChainNonces * add more operations from observer module * add helper functions * format code * add additional check to voting messages to for existing ballots * add msg vote tss * add msg vote tss * remove isPending check for aborted cctx * fix unit tests * add comments to simulation_test.go * add comments and unit tests * add comments to operations * refactor based on comments * fix formating * improve formating for decoders.go * remove overflow check when not needed * resolve comments 2 * add protocol v2 * generate files 2 * reduce weight of message enable CCTX * add solana address from rand * make changes based on feedback * refacctor updateCrossChainState to updateCrosschainState * rename Maxed to MaxReached * change test names for tests under TestCoinType_SupportsRefund * rename functions updateState to extractState * rebase develop * revert format for makefile * generate files --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Makefile | 8 +- changelog.md | 1 + pkg/chains/chain_filters.go | 5 + pkg/chains/chain_filters_test.go | 33 +- pkg/coin/coin.go | 4 + pkg/coin/coin_test.go | 42 +- pkg/memo/memo_test.go | 25 + simulation/simulation_test.go | 40 +- simulation/state.go | 217 +++++--- testutil/keeper/mocks/crosschain/authority.go | 32 +- testutil/keeper/mocks/observer/staking.go | 20 + testutil/sample/crosschain.go | 169 ++++++- testutil/sample/crypto.go | 56 +++ testutil/sample/observer.go | 54 +- testutil/sample/sample.go | 10 + x/crosschain/genesis.go | 22 +- x/crosschain/keeper/cctx_utils.go | 1 - .../keeper/msg_server_add_outbound_tracker.go | 4 +- .../msg_server_add_outbound_tracker_test.go | 4 +- .../keeper/msg_server_refund_aborted_tx.go | 1 - .../keeper/msg_server_vote_inbound_tx.go | 1 + .../keeper/msg_server_vote_inbound_tx_test.go | 8 +- .../keeper/msg_server_whitelist_erc20.go | 1 + .../keeper/msg_server_whitelist_erc20_test.go | 11 + x/crosschain/keeper/refund.go | 2 +- x/crosschain/simulation/decoders.go | 54 +- x/crosschain/simulation/decoders_test.go | 39 +- .../simulation/operation_abort_stuck_cctx.go | 128 +++++ .../operation_add_inbound_tracker.go | 72 +++ .../operation_add_outbound_tracker.go | 152 ++++++ .../simulation/operation_gas_price_voter.go | 78 +++ .../operation_refund_aborted_cctx.go | 84 ++++ .../operation_remove_outbound_tracker.go | 72 +++ .../operation_update_erc20_pause_status.go | 120 +++++ .../operation_update_rate_limiter_flags.go | 60 +++ .../operation_update_tss_address.go | 129 +++++ .../simulation/operation_vote_inbound.go | 190 +++++++ .../simulation/operation_vote_outbound.go | 226 +++++++++ .../simulation/operation_whitelist_erc20.go | 147 ++++++ x/crosschain/simulation/operations.go | 464 +++++++----------- x/crosschain/types/expected_keepers.go | 2 + x/crosschain/types/keys.go | 3 +- x/crosschain/types/outbound_tracker.go | 6 + x/crosschain/types/outbound_tracker_test.go | 48 ++ x/fungible/keeper/deposits.go | 1 + x/fungible/simulation/operations.go | 6 +- x/observer/genesis.go | 18 +- .../keeper/msg_server_update_observer.go | 8 +- x/observer/keeper/msg_server_vote_tss.go | 5 +- x/observer/simulation/decoders.go | 63 ++- x/observer/simulation/decoders_test.go | 53 +- .../simulation/operation_add_observer.go | 95 ++++ .../operation_add_observer_node_account.go | 110 +++++ .../simulation/operation_disable_cctx.go | 55 +++ .../simulation/operation_enable_cctx.go | 55 +++ .../operation_remove_chain_params.go | 83 ++++ .../operation_reset_chain_nonces.go | 87 ++++ .../operation_update_chain_params.go | 67 +++ ...eration_update_gas_price_increase_flags.go | 58 +++ .../simulation/operation_update_keygen.go | 66 +++ .../simulation/operation_update_observer.go | 118 +++++ x/observer/simulation/operation_vote_tss.go | 177 +++++++ x/observer/simulation/operations.go | 410 ++++++++++++++-- x/observer/types/expected_keepers.go | 1 + x/observer/types/keys.go | 4 + zetaclient/chains/evm/observer/outbound.go | 3 +- 66 files changed, 3841 insertions(+), 547 deletions(-) create mode 100644 x/crosschain/simulation/operation_abort_stuck_cctx.go create mode 100644 x/crosschain/simulation/operation_add_inbound_tracker.go create mode 100644 x/crosschain/simulation/operation_add_outbound_tracker.go create mode 100644 x/crosschain/simulation/operation_gas_price_voter.go create mode 100644 x/crosschain/simulation/operation_refund_aborted_cctx.go create mode 100644 x/crosschain/simulation/operation_remove_outbound_tracker.go create mode 100644 x/crosschain/simulation/operation_update_erc20_pause_status.go create mode 100644 x/crosschain/simulation/operation_update_rate_limiter_flags.go create mode 100644 x/crosschain/simulation/operation_update_tss_address.go create mode 100644 x/crosschain/simulation/operation_vote_inbound.go create mode 100644 x/crosschain/simulation/operation_vote_outbound.go create mode 100644 x/crosschain/simulation/operation_whitelist_erc20.go create mode 100644 x/crosschain/types/outbound_tracker.go create mode 100644 x/crosschain/types/outbound_tracker_test.go create mode 100644 x/observer/simulation/operation_add_observer.go create mode 100644 x/observer/simulation/operation_add_observer_node_account.go create mode 100644 x/observer/simulation/operation_disable_cctx.go create mode 100644 x/observer/simulation/operation_enable_cctx.go create mode 100644 x/observer/simulation/operation_remove_chain_params.go create mode 100644 x/observer/simulation/operation_reset_chain_nonces.go create mode 100644 x/observer/simulation/operation_update_chain_params.go create mode 100644 x/observer/simulation/operation_update_gas_price_increase_flags.go create mode 100644 x/observer/simulation/operation_update_keygen.go create mode 100644 x/observer/simulation/operation_update_observer.go create mode 100644 x/observer/simulation/operation_vote_tss.go diff --git a/Makefile b/Makefile index 02ac397d51..89a42ab4e9 100644 --- a/Makefile +++ b/Makefile @@ -409,7 +409,7 @@ test-sim-fullappsimulation: $(call run-sim-test,"TestFullAppSimulation",TestFullAppSimulation,100,200,30m) test-sim-import-export: - $(call run-sim-test,"test-import-export",TestAppImportExport,50,100,30m) + $(call run-sim-test,"test-import-export",TestAppImportExport,100,200,30m) test-sim-after-import: $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,100,200,30m) @@ -430,6 +430,12 @@ test-sim-after-import-long: runsim @echo "Running application simulation-after-import. This may take several minute" @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 500 50 TestAppSimulationAfterImport +# Use to run all simulation tests quickly (for example, before a creating a PR) +test-sim-quick: + $(call run-sim-test,"test-full-app-sim",TestFullAppSimulation,10,20,30m) + $(call run-sim-test,"test-import-export",TestAppImportExport,10,20,30m) + $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,10,20,30m) + .PHONY: \ test-sim-nondeterminism \ test-sim-fullappsimulation \ diff --git a/changelog.md b/changelog.md index 4442a0cbba..44c0a2765c 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,7 @@ * [3254](https://github.com/zeta-chain/node/pull/3254) - rename v2 E2E tests as evm tests and rename old evm tests as legacy * [3095](https://github.com/zeta-chain/node/pull/3095) - initialize simulation tests for custom zetachain modules * [3276](https://github.com/zeta-chain/node/pull/3276) - add Solana E2E performance tests and improve Solana outbounds performance +* [3207](https://github.com/zeta-chain/node/pull/3207) - add simulation test operations for all messages in crosschain and observer module ### Refactor diff --git a/pkg/chains/chain_filters.go b/pkg/chains/chain_filters.go index 3235fb98b1..c7961860e1 100644 --- a/pkg/chains/chain_filters.go +++ b/pkg/chains/chain_filters.go @@ -18,6 +18,11 @@ func FilterByConsensus(cs Consensus) ChainFilter { return func(chain Chain) bool { return chain.Consensus == cs } } +// FilterByVM filters chains by VM type +func FilterByVM(vm Vm) ChainFilter { + return func(chain Chain) bool { return chain.Vm == vm } +} + // FilterChains applies a list of filters to a list of chains func FilterChains(chainList []Chain, filters ...ChainFilter) []Chain { // Apply each filter to the list of supported chains diff --git a/pkg/chains/chain_filters_test.go b/pkg/chains/chain_filters_test.go index 6254600452..d1fc057572 100644 --- a/pkg/chains/chain_filters_test.go +++ b/pkg/chains/chain_filters_test.go @@ -14,42 +14,57 @@ func TestFilterChains(t *testing.T) { expected func() []chains.Chain }{ { - name: "Filter external chains", + name: "filter external chains", filters: []chains.ChainFilter{chains.FilterExternalChains}, expected: func() []chains.Chain { return chains.ExternalChainList([]chains.Chain{}) }, }, { - name: "Filter gateway observer chains", + name: "filter gateway observer chains", filters: []chains.ChainFilter{chains.FilterByGateway(chains.CCTXGateway_observers)}, expected: func() []chains.Chain { return chains.ChainListByGateway(chains.CCTXGateway_observers, []chains.Chain{}) }, }, { - name: "Filter consensus ethereum chains", + name: "filter consensus ethereum chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_ethereum)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_ethereum, []chains.Chain{}) }, }, { - name: "Filter consensus bitcoin chains", + name: "filter consensus bitcoin chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_bitcoin)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_bitcoin, []chains.Chain{}) }, }, { - name: "Filter consensus solana chains", + name: "filter consensus solana chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_solana_consensus)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_solana_consensus, []chains.Chain{}) }, }, { - name: "Apply multiple filters external chains and gateway observer", + name: "filter evm chains", + filters: []chains.ChainFilter{ + chains.FilterByVM(chains.Vm_evm), + }, + expected: func() []chains.Chain { + var chainList []chains.Chain + for _, chain := range chains.ExternalChainList([]chains.Chain{}) { + if chain.Vm == chains.Vm_evm { + chainList = append(chainList, chain) + } + } + return chainList + }, + }, + { + name: "apply multiple filters external chains and gateway observer", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -66,7 +81,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Apply multiple filters external chains with gateway observer and consensus ethereum", + name: "apply multiple filters external chains with gateway observer and consensus ethereum", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -85,7 +100,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Apply multiple filters external chains with gateway observer and consensus bitcoin", + name: "apply multiple filters external chains with gateway observer and consensus bitcoin", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -116,7 +131,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Test multiple filters in random order", + name: "test multiple filters in random order", filters: []chains.ChainFilter{ chains.FilterByGateway(chains.CCTXGateway_observers), chains.FilterByConsensus(chains.Consensus_ethereum), diff --git a/pkg/coin/coin.go b/pkg/coin/coin.go index 223c2cbed9..e66095ebad 100644 --- a/pkg/coin/coin.go +++ b/pkg/coin/coin.go @@ -34,3 +34,7 @@ func GetAzetaDecFromAmountInZeta(zetaAmount string) (sdk.Dec, error) { zetaToAzetaConvertionFactor := sdk.NewDecFromInt(sdk.NewInt(1000000000000000000)) return zetaDec.Mul(zetaToAzetaConvertionFactor), nil } + +func (c CoinType) SupportsRefund() bool { + return c == CoinType_ERC20 || c == CoinType_Gas || c == CoinType_Zeta +} diff --git a/pkg/coin/coin_test.go b/pkg/coin/coin_test.go index 9f930039f7..504a743b2c 100644 --- a/pkg/coin/coin_test.go +++ b/pkg/coin/coin_test.go @@ -1,14 +1,15 @@ -package coin +package coin_test import ( "testing" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/coin" ) func Test_AzetaPerZeta(t *testing.T) { - require.Equal(t, sdk.NewDec(1e18), AzetaPerZeta()) + require.Equal(t, sdk.NewDec(1e18), coin.AzetaPerZeta()) } func Test_GetAzetaDecFromAmountInZeta(t *testing.T) { @@ -57,7 +58,7 @@ func Test_GetAzetaDecFromAmountInZeta(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - azeta, err := GetAzetaDecFromAmountInZeta(tc.zetaAmount) + azeta, err := coin.GetAzetaDecFromAmountInZeta(tc.zetaAmount) tc.err(t, err) if err == nil { require.Equal(t, tc.azetaAmount, azeta) @@ -71,31 +72,31 @@ func TestGetCoinType(t *testing.T) { tests := []struct { name string coin string - want CoinType + want coin.CoinType wantErr bool }{ { name: "valid coin type 0", coin: "0", - want: CoinType(0), + want: coin.CoinType(0), wantErr: false, }, { name: "valid coin type 1", coin: "1", - want: CoinType(1), + want: coin.CoinType(1), wantErr: false, }, { name: "valid coin type 2", coin: "2", - want: CoinType(2), + want: coin.CoinType(2), wantErr: false, }, { name: "valid coin type 3", coin: "3", - want: CoinType(3), + want: coin.CoinType(3), wantErr: false, }, { @@ -106,7 +107,7 @@ func TestGetCoinType(t *testing.T) { { name: "invalid coin type large number", coin: "4", - want: CoinType(4), + want: coin.CoinType(4), }, { name: "invalid coin type non-integer", @@ -117,7 +118,7 @@ func TestGetCoinType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetCoinType(tt.coin) + got, err := coin.GetCoinType(tt.coin) if tt.wantErr { require.Error(t, err) } else { @@ -127,3 +128,24 @@ func TestGetCoinType(t *testing.T) { }) } } + +func TestCoinType_SupportsRefund(t *testing.T) { + tests := []struct { + name string + c coin.CoinType + want bool + }{ + {"should support refund for ERC20", coin.CoinType_ERC20, true}, + {"should support refund forGas", coin.CoinType_Gas, true}, + {"should support refund forZeta", coin.CoinType_Zeta, true}, + {"should not support refund forCmd", coin.CoinType_Cmd, false}, + {"should not support refund forUnknown", coin.CoinType(100), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.SupportsRefund(); got != tt.want { + t.Errorf("CoinType.SupportsRefund() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/memo/memo_test.go b/pkg/memo/memo_test.go index 4eabd6a18f..4979b29d41 100644 --- a/pkg/memo/memo_test.go +++ b/pkg/memo/memo_test.go @@ -2,11 +2,13 @@ package memo_test import ( "encoding/hex" + mathrand "math/rand" "testing" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/testutil/sample" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) @@ -306,6 +308,9 @@ func Test_Memo_DecodeFromBytes(t *testing.T) { func Test_DecodeLegacyMemoHex(t *testing.T) { expectedShortMsgResult, err := hex.DecodeString("1a2b3c4d5e6f708192a3b4c5d6e7f808") + r := mathrand.New(mathrand.NewSource(42)) + address, data, memoHex := sample.MemoFromRand(r) + require.NoError(t, err) tests := []struct { name string @@ -324,6 +329,8 @@ func Test_DecodeLegacyMemoHex(t *testing.T) { {"empty msg", "", common.Address{}, nil, false}, {"invalid hex", "invalidHex", common.Address{}, nil, true}, {"short msg", "1a2b3c4d5e6f708192a3b4c5d6e7f808", common.Address{}, expectedShortMsgResult, false}, + {"random message", sample.EthAddress().String(), common.Address{}, nil, true}, + {"random message with hex encoding", memoHex, address, data, false}, } for _, tt := range tests { @@ -339,3 +346,21 @@ func Test_DecodeLegacyMemoHex(t *testing.T) { }) } } + +func Test_DecodeLegacyMemoHex_Random(t *testing.T) { + r := mathrand.New(mathrand.NewSource(42)) + + // Generate a random memo hex + randomMemo := common.BytesToAddress([]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}). + Hex() + randomData := []byte(sample.StringRandom(r, 10)) + randomMemoHex := hex.EncodeToString(append(common.FromHex(randomMemo), randomData...)) + + // Decode the random memo hex + addr, data, err := memo.DecodeLegacyMemoHex(randomMemoHex) + + // Validate the results + require.NoError(t, err) + require.Equal(t, common.HexToAddress(randomMemo), addr) + require.Equal(t, randomData, data) +} diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 818250cbaf..6309753011 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -42,8 +42,6 @@ func init() { zetasimulation.GetSimulatorFlags() } -// StoreKeysPrefixes defines a struct used in comparing two keys for two different stores -// SkipPrefixes is used to skip certain prefixes when comparing the stores type StoreKeysPrefixes struct { A storetypes.StoreKey B storetypes.StoreKey @@ -66,6 +64,12 @@ func interBlockCacheOpt() func(*baseapp.BaseApp) { } // TestAppStateDeterminism runs a full application simulation , and produces multiple blocks as per the config +// It does the following +// 1. It runs the simulation multiple times with the same seed value +// 2. It checks the apphash at the end of each run +// 3. It compares the apphash at the end of each run to check for determinism +// 4. Repeat steps 1-3 for multiple seeds + // It checks the determinism of the application by comparing the apphash at the end of each run to other runs // The following test certifies that , for the same set of operations ( irrespective of what the operations are ) , // we would reach the same final state if the initial state is the same @@ -179,7 +183,10 @@ func TestAppStateDeterminism(t *testing.T) { } // TestFullAppSimulation runs a full simApp simulation with the provided configuration. -// At the end of the run it tries to export the genesis state to make sure the export works. +// This test does the following +// 1. It runs a full simulation with the provided configuration +// 2. It exports the state and validators +// 3. Verifies that the run and export were successful func TestFullAppSimulation(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -252,6 +259,15 @@ func TestFullAppSimulation(t *testing.T) { zetasimulation.PrintStats(db) } +// TestAppImportExport tests the application simulation after importing the state exported from a previous.At a high level,it does the following +// 1. It runs a full simulation and exports the state +// 2. It creates a new app, and db +// 3. It imports the exported state into the new app +// 4. It compares the key value pairs for the two apps.The comparison function takes a list of keys to skip as an input as well +// a. First app which ran the simulation +// b. Second app which imported the state + +// This can verify the export and import process do not modify the state in anyway irrespective of the operations performed func TestAppImportExport(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -270,7 +286,6 @@ func TestAppImportExport(t *testing.T) { t.Skip("skipping application simulation") } require.NoError(t, err, "simulation setup failed") - t.Cleanup(func() { if err := db.Close(); err != nil { require.NoError(t, err, "Error closing new database") @@ -375,7 +390,6 @@ func TestAppImportExport(t *testing.T) { ChainID: SimAppChainID, }) - t.Log("initializing genesis for the new app using exported genesis state") // Use genesis state from the first app to initialize the second app newSimApp.ModuleManager().InitGenesis(ctxNewSimApp, newSimApp.AppCodec(), genesisState) newSimApp.StoreConsensusParams(ctxNewSimApp, exported.ConsensusParams) @@ -390,7 +404,7 @@ func TestAppImportExport(t *testing.T) { // We will need to explore this further to find a definitive answer // TODO:https://github.com/zeta-chain/node/issues/3263 - // {simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, + //{simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, { simApp.GetKey(stakingtypes.StoreKey), newSimApp.GetKey(stakingtypes.StoreKey), [][]byte{ @@ -439,6 +453,12 @@ func TestAppImportExport(t *testing.T) { } } +// TestAppSimulationAfterImport tests the application simulation after importing the state exported from a previous simulation run. +// It does the following steps +// 1. It runs a full simulation and exports the state +// 2. It creates a new app, and db +// 3. It imports the exported state into the new app +// 4. It runs a simulation on the new app and verifies that there is no error in the second simulation func TestAppSimulationAfterImport(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -516,6 +536,7 @@ func TestAppSimulationAfterImport(t *testing.T) { exported, err := simApp.ExportAppStateAndValidators(true, []string{}, []string{}) require.NoError(t, err) + // Setup a new app with new database and directory newDB, newDir, _, _, err := cosmossimutils.SetupSimulation( config, SimDBBackend+"_new", @@ -523,9 +544,7 @@ func TestAppSimulationAfterImport(t *testing.T) { zetasimulation.FlagVerboseValue, zetasimulation.FlagEnabledValue, ) - require.NoError(t, err, "simulation setup failed") - t.Cleanup(func() { if err := newDB.Close(); err != nil { require.NoError(t, err, "Error closing new database") @@ -534,7 +553,6 @@ func TestAppSimulationAfterImport(t *testing.T) { require.NoError(t, err, "Error removing directory") } }) - newSimApp, err := zetasimulation.NewSimApp( logger, newDB, @@ -544,12 +562,14 @@ func TestAppSimulationAfterImport(t *testing.T) { ) require.NoError(t, err) + // Initialize the new app with the exported genesis state of the first run t.Log("Importing genesis into the new app") newSimApp.InitChain(abci.RequestInitChain{ ChainId: SimAppChainID, AppStateBytes: exported.AppState, }) + // Run simulation on the new app stopEarly, simParams, simErr = simulation.SimulateFromSeed( t, os.Stdout, @@ -567,5 +587,5 @@ func TestAppSimulationAfterImport(t *testing.T) { config, simApp.AppCodec(), ) - require.NoError(t, err) + require.NoError(t, simErr) } diff --git a/simulation/state.go b/simulation/state.go index 9bf2c24949..4e76f7d623 100644 --- a/simulation/state.go +++ b/simulation/state.go @@ -3,17 +3,12 @@ package simulation import ( "encoding/json" "fmt" - "io" "math/rand" - "os" "testing" "time" "cosmossdk.io/math" - cmtjson "github.com/cometbft/cometbft/libs/json" - tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -23,9 +18,12 @@ import ( "github.com/stretchr/testify/require" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" - zetaapp "github.com/zeta-chain/node/app" + zetachains "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/crypto" "github.com/zeta-chain/node/testutil/sample" authoritytypes "github.com/zeta-chain/node/x/authority/types" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -36,7 +34,10 @@ const ( InitiallyBondedValidators = "initially_bonded_validators" ) -func updateBankState( +// extractBankGenesisState extracts and updates the bank genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool +func extractBankGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -67,7 +68,9 @@ func updateBankState( return bankState } -func updateEVMState( +// extractEVMGenesisState extracts and updates the evm genesis state. +// It replaces the EvmDenom with BondDenom +func extractEVMGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -85,7 +88,11 @@ func updateEVMState( return evmState } -func updateStakingState( +// extractStakingGenesisState extracts and updates the staking genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool +// It additionally returns the non-bonded coins as well +func extractStakingGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -112,7 +119,16 @@ func updateStakingState( return stakingState, notBondedCoins } -func updateObserverState( +// extractObserverGenesisState extracts and updates the observer genesis state. +// It adds the following +// - A random observer set which is a subset of the current validator set +// - A randomised node account for each observer +// - A random TSS +// - A TSS history for the TSS created +// - Chain nonces for each chain +// - Pending nonces for each chain +// - Crosschain flags, inbound and outbound enabled +func extractObserverGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -125,6 +141,7 @@ func updateObserverState( observerState := new(observertypes.GenesisState) cdc.MustUnmarshalJSON(observerStateBz, observerState) + // Create an observer set as a subset of the current validator set observers := make([]string, 0) for _, validator := range validators { accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) @@ -138,24 +155,67 @@ func updateObserverState( observers[i], observers[j] = observers[j], observers[i] }) - numObservers := r.Intn(11) + 5 + numObservers := r.Intn(21) + 5 if numObservers > len(observers) { numObservers = len(observers) } observers = observers[:numObservers] + // Create node account list for the observers set + nodeAccounts := make([]*observertypes.NodeAccount, len(observers)) + for i, observer := range observers { + nodeAccounts[i] = &observertypes.NodeAccount{ + Operator: observer, + GranteeAddress: observer, + GranteePubkey: &crypto.PubKeySet{}, + NodeStatus: observertypes.NodeStatus_Active, + } + } + // Create a random tss + tss, err := sample.TSSFromRand(r) + require.NoError(t, err) + tss.OperatorAddressList = observers + + // Create a tss history + tssHistory := make([]observertypes.TSS, 0) + tssHistory = append(tssHistory, tss) + + // Create chainnonces and pendingnonces + chains := zetachains.DefaultChainsList() + chainsNonces := make([]observertypes.ChainNonces, 0) + pendingNonces := make([]observertypes.PendingNonces, 0) + for _, chain := range chains { + chainNonce := observertypes.ChainNonces{ + ChainId: chain.ChainId, + Nonce: 0, + } + chainsNonces = append(chainsNonces, chainNonce) + pendingNonce := observertypes.PendingNonces{ + NonceLow: 0, + NonceHigh: 0, + ChainId: chain.ChainId, + Tss: tss.TssPubkey, + } + pendingNonces = append(pendingNonces, pendingNonce) + } + + observerState.Tss = &tss observerState.Observers.ObserverList = observers + observerState.NodeAccountList = nodeAccounts observerState.CrosschainFlags.IsInboundEnabled = true observerState.CrosschainFlags.IsOutboundEnabled = true - - tss := sample.TSSFromRand(t, r) - tss.OperatorAddressList = observers - observerState.Tss = &tss + observerState.ChainNonces = chainsNonces + observerState.PendingNonces = pendingNonces + observerState.TssHistory = tssHistory return observerState } -func updateAuthorityState( +// extractAuthorityGenesisState extracts and updates the authority genesis state. +// It adds the following +// - A policy for each policy type; +// the address is a random account address selected from the simulation accounts list +func extractAuthorityGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -190,7 +250,40 @@ func updateAuthorityState( return authorityState } -func updateFungibleState( +// extractCrosschainGenesisState extracts and updates the crosschain genesis state. +// It adds the following +// - A gas price list for each chain +func extractCrosschainGenesisState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + r *rand.Rand, +) *crosschaintypes.GenesisState { + crossChainStateBz, ok := rawState[crosschaintypes.ModuleName] + require.True(t, ok, "crosschain genesis state is missing") + + crossChainState := new(crosschaintypes.GenesisState) + cdc.MustUnmarshalJSON(crossChainStateBz, crossChainState) + + // Add a gasprice for each chain + chains := zetachains.DefaultChainsList() + gasPriceList := make([]*crosschaintypes.GasPrice, len(chains)) + for i, chain := range chains { + gasPriceList[i] = sample.GasPriceFromRand(r, chain.ChainId) + } + + crossChainState.GasPriceList = gasPriceList + + return crossChainState +} + +// extractFungibleGenesisState extracts and updates the fungible genesis state. +// It adds the following +// - A random system contract address +// - A random connector zevm address +// - A random gateway address +// - A foreign coin for each chain under the default chain list. +func extractFungibleGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -207,9 +300,28 @@ func updateFungibleState( Gateway: sample.EthAddressFromRand(r).String(), } + foreignCoins := make([]fungibletypes.ForeignCoins, 0) + chains := zetachains.DefaultChainsList() + + for _, chain := range chains { + foreignCoin := fungibletypes.ForeignCoins{ + ForeignChainId: chain.ChainId, + Asset: sample.EthAddressFromRand(r).String(), + Zrc20ContractAddress: sample.EthAddressFromRand(r).String(), + Decimals: 18, + Paused: false, + CoinType: coin.CoinType_Gas, + LiquidityCap: math.ZeroUint(), + } + foreignCoins = append(foreignCoins, foreignCoin) + } + fungibleState.ForeignCoinsList = foreignCoins + return fungibleState } +// updateRawState updates the raw genesis state for the application. +// This is used to inject values needed to run the simulation tests. func updateRawState( t *testing.T, rawState map[string]json.RawMessage, @@ -217,12 +329,12 @@ func updateRawState( r *rand.Rand, accs []simtypes.Account, ) { - stakingState, notBondedCoins := updateStakingState(t, rawState, cdc) - bankState := updateBankState(t, rawState, cdc, notBondedCoins) - evmState := updateEVMState(t, rawState, cdc, stakingState.Params.BondDenom) - observerState := updateObserverState(t, rawState, cdc, r, stakingState.Validators) - authorityState := updateAuthorityState(t, rawState, cdc, r, accs) - fungibleState := updateFungibleState(t, rawState, cdc, r) + stakingState, notBondedCoins := extractStakingGenesisState(t, rawState, cdc) + bankState := extractBankGenesisState(t, rawState, cdc, notBondedCoins) + evmState := extractEVMGenesisState(t, rawState, cdc, stakingState.Params.BondDenom) + observerState := extractObserverGenesisState(t, rawState, cdc, r, stakingState.Validators) + authorityState := extractAuthorityGenesisState(t, rawState, cdc, r, accs) + fungibleState := extractFungibleGenesisState(t, rawState, cdc, r) rawState[stakingtypes.ModuleName] = cdc.MustMarshalJSON(stakingState) rawState[banktypes.ModuleName] = cdc.MustMarshalJSON(bankState) @@ -230,6 +342,7 @@ func updateRawState( rawState[observertypes.ModuleName] = cdc.MustMarshalJSON(observerState) rawState[authoritytypes.ModuleName] = cdc.MustMarshalJSON(authorityState) rawState[fungibletypes.ModuleName] = cdc.MustMarshalJSON(fungibleState) + rawState[crosschaintypes.ModuleName] = cdc.MustMarshalJSON(extractCrosschainGenesisState(t, rawState, cdc, r)) } // AppStateFn returns the initial application state using a genesis or the simulation parameters. @@ -253,7 +366,7 @@ func AppStateFn( chainID = config.ChainID - // if exported state is provided then use it + // if exported state is provided, then use it if exportedState != nil { return exportedState, accs, chainID, genesisTimestamp } @@ -342,61 +455,5 @@ func AppStateRandomizedFn( if err != nil { panic(err) } - return appState, accs } - -// AppStateFromGenesisFileFn util function to generate the genesis AppState -// from a genesis.json file. -func AppStateFromGenesisFileFn( - r io.Reader, - cdc codec.JSONCodec, - genesisFile string, -) (tmtypes.GenesisDoc, []simtypes.Account, error) { - bytes, err := os.ReadFile(genesisFile) // #nosec G304 -- genesisFile value is controlled - if err != nil { - panic(err) - } - - var genesis tmtypes.GenesisDoc - // NOTE: Comet uses a custom JSON decoder for GenesisDoc - err = cmtjson.Unmarshal(bytes, &genesis) - if err != nil { - panic(err) - } - - var appState zetaapp.GenesisState - err = json.Unmarshal(genesis.AppState, &appState) - if err != nil { - panic(err) - } - - var authGenesis authtypes.GenesisState - if appState[authtypes.ModuleName] != nil { - cdc.MustUnmarshalJSON(appState[authtypes.ModuleName], &authGenesis) - } - - newAccs := make([]simtypes.Account, len(authGenesis.Accounts)) - for i, acc := range authGenesis.Accounts { - // Pick a random private key, since we don't know the actual key - // This should be fine as it's only used for mock Tendermint validators - // and these keys are never actually used to sign by mock Tendermint. - privkeySeed := make([]byte, 15) - if _, err := r.Read(privkeySeed); err != nil { - panic(err) - } - - privKey := secp256k1.GenPrivKeyFromSecret(privkeySeed) - - a, ok := acc.GetCachedValue().(authtypes.AccountI) - if !ok { - return genesis, nil, fmt.Errorf("expected account") - } - - // create simulator accounts - simAcc := simtypes.Account{PrivKey: privKey, PubKey: privKey.PubKey(), Address: a.GetAddress()} - newAccs[i] = simAcc - } - - return genesis, newAccs, nil -} diff --git a/testutil/keeper/mocks/crosschain/authority.go b/testutil/keeper/mocks/crosschain/authority.go index 7dafb1331b..9f2a9a8837 100644 --- a/testutil/keeper/mocks/crosschain/authority.go +++ b/testutil/keeper/mocks/crosschain/authority.go @@ -3,8 +3,10 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" chains "github.com/zeta-chain/node/pkg/chains" + authoritytypes "github.com/zeta-chain/node/x/authority/types" + + mock "github.com/stretchr/testify/mock" types "github.com/cosmos/cosmos-sdk/types" ) @@ -52,6 +54,34 @@ func (_m *CrosschainAuthorityKeeper) GetAdditionalChainList(ctx types.Context) [ return r0 } +// GetPolicies provides a mock function with given fields: ctx +func (_m *CrosschainAuthorityKeeper) GetPolicies(ctx types.Context) (authoritytypes.Policies, bool) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetPolicies") + } + + var r0 authoritytypes.Policies + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context) (authoritytypes.Policies, bool)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) authoritytypes.Policies); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(authoritytypes.Policies) + } + + if rf, ok := ret.Get(1).(func(types.Context) bool); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // NewCrosschainAuthorityKeeper creates a new instance of CrosschainAuthorityKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCrosschainAuthorityKeeper(t interface { diff --git a/testutil/keeper/mocks/observer/staking.go b/testutil/keeper/mocks/observer/staking.go index 90007b6c35..af2537bac0 100644 --- a/testutil/keeper/mocks/observer/staking.go +++ b/testutil/keeper/mocks/observer/staking.go @@ -15,6 +15,26 @@ type ObserverStakingKeeper struct { mock.Mock } +// GetAllValidators provides a mock function with given fields: ctx +func (_m *ObserverStakingKeeper) GetAllValidators(ctx types.Context) []stakingtypes.Validator { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetAllValidators") + } + + var r0 []stakingtypes.Validator + if rf, ok := ret.Get(0).(func(types.Context) []stakingtypes.Validator); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]stakingtypes.Validator) + } + } + + return r0 +} + // GetDelegation provides a mock function with given fields: ctx, delAddr, valAddr func (_m *ObserverStakingKeeper) GetDelegation(ctx types.Context, delAddr types.AccAddress, valAddr types.ValAddress) (stakingtypes.Delegation, bool) { ret := _m.Called(ctx, delAddr, valAddr) diff --git a/testutil/sample/crosschain.go b/testutil/sample/crosschain.go index 82e683604a..b3ed7d5ee8 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -2,6 +2,7 @@ package sample import ( "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "math/big" @@ -11,7 +12,7 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" @@ -54,6 +55,36 @@ func RateLimiterFlags() types.RateLimiterFlags { } } +func RateLimiterFlagsFromRand(r *rand.Rand) types.RateLimiterFlags { + return types.RateLimiterFlags{ + Enabled: true, + Window: r.Int63(), + Rate: sdk.NewUint(r.Uint64()), + Conversions: []types.Conversion{ + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + }, + } +} + // CustomAssetRate creates a custom asset rate with the given parameters func CustomAssetRate( chainID int64, @@ -118,6 +149,25 @@ func GasPriceWithChainID(t *testing.T, chainID int64) types.GasPrice { } } +func GasPriceFromRand(r *rand.Rand, chainID int64) *types.GasPrice { + var price uint64 + for price == 0 { + maxGasPrice := uint64(1000 * 1e9) // 1000 Gwei + price = uint64(1e9) + r.Uint64()%maxGasPrice + } + // Select priority fee between 0 and price + priorityFee := r.Uint64() % price + return &types.GasPrice{ + Creator: "", + ChainId: chainID, + Signers: []string{AccAddressFromRand(r)}, + BlockNums: []uint64{r.Uint64()}, + Prices: []uint64{price}, + MedianIndex: 0, + PriorityFees: []uint64{priorityFee}, + } +} + func InboundParams(r *rand.Rand) *types.InboundParams { return &types.InboundParams{ Sender: EthAddress().String(), @@ -314,8 +364,10 @@ func InboundVote(coinType coin.CoinType, from, to int64) types.MsgVoteInbound { } // InboundVoteFromRand creates a simulated inbound vote message. This function uses the provided source of randomness to generate the vote -func InboundVoteFromRand(coinType coin.CoinType, from, to int64, r *rand.Rand) types.MsgVoteInbound { - EthAddress() +func InboundVoteFromRand(from, to int64, r *rand.Rand, asset string) types.MsgVoteInbound { + coinType := CoinTypeFromRand(r) + _, _, memo := MemoFromRand(r) + return types.MsgVoteInbound{ Creator: "", Sender: EthAddressFromRand(r).String(), @@ -323,19 +375,118 @@ func InboundVoteFromRand(coinType coin.CoinType, from, to int64, r *rand.Rand) t Receiver: EthAddressFromRand(r).String(), ReceiverChain: to, Amount: math.NewUint(r.Uint64()), - Message: base64.StdEncoding.EncodeToString(RandomBytes(r)), + Message: memo, InboundBlockHeight: r.Uint64(), CallOptions: &types.CallOptions{ GasLimit: 1000000000, }, - InboundHash: ethcommon.BytesToHash(RandomBytes(r)).String(), - CoinType: coinType, - TxOrigin: EthAddressFromRand(r).String(), - Asset: StringRandom(r, 32), - EventIndex: r.Uint64(), + InboundHash: common.BytesToHash(RandomBytes(r)).String(), + CoinType: coinType, + TxOrigin: EthAddressFromRand(r).String(), + Asset: asset, + EventIndex: r.Uint64(), + ProtocolContractVersion: ProtocolVersionFromRand(r), } } +func ProtocolVersionFromRand(r *rand.Rand) types.ProtocolContractVersion { + versions := []types.ProtocolContractVersion{types.ProtocolContractVersion_V1, types.ProtocolContractVersion_V2} + return versions[r.Intn(len(versions))] +} + +func CoinTypeFromRand(r *rand.Rand) coin.CoinType { + coinTypes := []coin.CoinType{coin.CoinType_Gas, coin.CoinType_ERC20, coin.CoinType_Zeta} + coinType := coinTypes[r.Intn(len(coinTypes))] + return coinType +} + +func MemoFromRand(r *rand.Rand) (common.Address, []byte, string) { + randomMemo := common.BytesToAddress([]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}). + Hex() + randomData := []byte(StringRandom(r, 10)) + memoHex := hex.EncodeToString(append(common.FromHex(randomMemo), randomData...)) + return common.HexToAddress(randomMemo), randomData, memoHex +} + +func CCTXfromRand(r *rand.Rand, + creator string, + index string, + to int64, + from int64, + tssPubkey string, + asset string, +) types.CrossChainTx { + coinType := CoinTypeFromRand(r) + + amount := math.NewUint(uint64(r.Int63())) + inbound := &types.InboundParams{ + Sender: EthAddressFromRand(r).String(), + SenderChainId: from, + TxOrigin: EthAddressFromRand(r).String(), + CoinType: coinType, + Asset: asset, + Amount: amount, + ObservedHash: StringRandom(r, 32), + ObservedExternalHeight: r.Uint64(), + BallotIndex: StringRandom(r, 32), + FinalizedZetaHeight: r.Uint64(), + } + + outbound := &types.OutboundParams{ + Receiver: EthAddressFromRand(r).String(), + ReceiverChainId: to, + CoinType: coinType, + Amount: math.NewUint(uint64(r.Int63())), + TssNonce: 0, + TssPubkey: tssPubkey, + CallOptions: &types.CallOptions{ + GasLimit: r.Uint64(), + }, + GasPrice: math.NewUint(uint64(r.Int63())).String(), + Hash: StringRandom(r, 32), + BallotIndex: StringRandom(r, 32), + ObservedExternalHeight: r.Uint64(), + GasUsed: 100, + EffectiveGasPrice: math.NewInt(r.Int63()), + EffectiveGasLimit: 100, + } + + cctx := types.CrossChainTx{ + Creator: creator, + Index: index, + ZetaFees: sdk.NewUint(1), + RelayedMessage: base64.StdEncoding.EncodeToString(RandomBytes(r)), + CctxStatus: &types.Status{ + IsAbortRefunded: false, + Status: types.CctxStatus_PendingOutbound, + }, + InboundParams: inbound, + OutboundParams: []*types.OutboundParams{outbound}, + ProtocolContractVersion: ProtocolVersionFromRand(r), + } + return cctx +} + +func OutboundVoteSim(r *rand.Rand, + cctx types.CrossChainTx, +) (types.CrossChainTx, types.MsgVoteOutbound) { + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: chains.ReceiveStatus_success, + Creator: cctx.Creator, + ObservedOutboundHash: common.BytesToHash(EthAddressFromRand(r).Bytes()).String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: cctx.GetCurrentOutboundParam().ObservedExternalHeight, + ObservedOutboundEffectiveGasPrice: cctx.GetCurrentOutboundParam().EffectiveGasPrice, + ObservedOutboundGasUsed: cctx.GetCurrentOutboundParam().GasUsed, + CoinType: cctx.InboundParams.CoinType, + } + + return cctx, msg +} + func ZRC20Withdrawal(to []byte, value *big.Int) *zrc20.ZRC20Withdrawal { return &zrc20.ZRC20Withdrawal{ From: EthAddress(), diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index 9e643fa123..783ffa4a8d 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -2,6 +2,7 @@ package sample import ( "crypto/ecdsa" + cryptoed25519 "crypto/ed25519" "math/big" "math/rand" "strconv" @@ -33,6 +34,15 @@ func PubKeySet() *crypto.PubKeySet { return &pubKeySet } +func Ed25519PrivateKeyFromRand(r *rand.Rand) (*ed25519.PrivKey, error) { + randomBytes := make([]byte, 32) + _, err := r.Read(randomBytes) + if err != nil { + return nil, err + } + return ed25519.GenPrivKeyFromSecret(randomBytes), nil +} + // PubKeyString returns a sample public key string func PubKeyString() string { priKey := ed25519.GenPrivKey() @@ -47,6 +57,22 @@ func PubKeyString() string { return pubkey.String() } +func PubkeyStringFromRand(r *rand.Rand) (string, error) { + priKey, err := Ed25519PrivateKeyFromRand(r) + if err != nil { + return "", err + } + s, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, priKey.PubKey()) + if err != nil { + return "", err + } + pubkey, err := crypto.NewPubKey(s) + if err != nil { + return "", err + } + return pubkey.String(), nil +} + // PrivKeyAddressPair returns a private key, address pair func PrivKeyAddressPair() (*ed25519.PrivKey, sdk.AccAddress) { privKey := ed25519.GenPrivKey() @@ -83,6 +109,16 @@ func SolanaPrivateKey(t *testing.T) solana.PrivateKey { return privKey } +func SolanaPrivateKeyFromRand(r *rand.Rand) (solana.PrivateKey, error) { + pub, priv, err := cryptoed25519.GenerateKey(r) + if err != nil { + return nil, err + } + var publicKey cryptoed25519.PublicKey + copy(publicKey[:], pub) + return solana.PrivateKey(priv), nil +} + // SolanaAddress returns a sample solana address func SolanaAddress(t *testing.T) string { privKey, err := solana.NewRandomPrivateKey() @@ -90,6 +126,14 @@ func SolanaAddress(t *testing.T) string { return privKey.PublicKey().String() } +func SolanaAddressFromRand(r *rand.Rand) (string, error) { + privKey, err := SolanaPrivateKeyFromRand(r) + if err != nil { + return "", err + } + return privKey.PublicKey().String(), nil +} + // SolanaSignature returns a sample solana signature func SolanaSignature(t *testing.T) solana.Signature { // Generate a random keypair @@ -113,6 +157,11 @@ func Hash() ethcommon.Hash { return ethcommon.BytesToHash(EthAddress().Bytes()) } +// Hash returns a sample hash +func HashFromRand(r *rand.Rand) ethcommon.Hash { + return ethcommon.BytesToHash(EthAddressFromRand(r).Bytes()) +} + // BtcHash returns a sample btc hash func BtcHash() chainhash.Hash { return chainhash.Hash(Hash()) @@ -138,6 +187,13 @@ func AccAddress() string { return sdk.AccAddress(addr).String() } +// AccAddressFromRand returns a sample account address in string +func AccAddressFromRand(r *rand.Rand) string { + pk := PubKey(r) + addr := pk.Address() + return sdk.AccAddress(addr).String() +} + // ValAddress returns a sample validator operator address func ValAddress(r *rand.Rand) sdk.ValAddress { return sdk.ValAddress(PubKey(r).Address()) diff --git a/testutil/sample/observer.go b/testutil/sample/observer.go index 1a0ed8f2ba..5ee8c6f648 100644 --- a/testutil/sample/observer.go +++ b/testutil/sample/observer.go @@ -10,7 +10,6 @@ import ( "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/cosmos" @@ -72,6 +71,15 @@ func Keygen(t *testing.T) *types.Keygen { } } +func KeygenFromRand(r *rand.Rand) types.Keygen { + pubkey := PubKey(r) + return types.Keygen{ + Status: types.KeygenStatus_KeyGenSuccess, + GranteePubkeys: []string{pubkey.String()}, + BlockNumber: r.Int63(), + } +} + func LastObserverCount(lastChangeHeight int64) *types.LastObserverCount { r := newRandFromSeed(lastChangeHeight) @@ -108,6 +116,27 @@ func ChainParams(chainID int64) *types.ChainParams { } } +func ChainParamsFromRand(r *rand.Rand, chainID int64) *types.ChainParams { + fiftyPercent := sdk.MustNewDecFromStr("0.5") + return &types.ChainParams{ + ChainId: chainID, + ConfirmationCount: r.Uint64(), + + GasPriceTicker: Uint64InRangeFromRand(r, 1, 300), + InboundTicker: Uint64InRangeFromRand(r, 1, 300), + OutboundTicker: Uint64InRangeFromRand(r, 1, 300), + WatchUtxoTicker: Uint64InRangeFromRand(r, 1, 300), + ZetaTokenContractAddress: EthAddressFromRand(r).String(), + ConnectorContractAddress: EthAddressFromRand(r).String(), + Erc20CustodyContractAddress: EthAddressFromRand(r).String(), + OutboundScheduleInterval: Int64InRangeFromRand(r, 1, 100), + OutboundScheduleLookahead: Int64InRangeFromRand(r, 1, 500), + BallotThreshold: fiftyPercent, + MinObserverDelegation: sdk.NewDec(r.Int63()), + IsSupported: true, + } +} + func ChainParamsSupported(chainID int64) *types.ChainParams { cp := ChainParams(chainID) cp.IsSupported = true @@ -124,12 +153,16 @@ func ChainParamsList() (cpl types.ChainParamsList) { } // TSSFromRand returns a random TSS,it uses the randomness provided as a parameter -func TSSFromRand(t *testing.T, r *rand.Rand) types.TSS { +func TSSFromRand(r *rand.Rand) (types.TSS, error) { pubKey := PubKey(r) spk, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pubKey) - require.NoError(t, err) + if err != nil { + return types.TSS{}, err + } pk, err := zetacrypto.NewPubKey(spk) - require.NoError(t, err) + if err != nil { + return types.TSS{}, err + } pubkeyString := pk.String() return types.TSS{ TssPubkey: pubkeyString, @@ -137,7 +170,7 @@ func TSSFromRand(t *testing.T, r *rand.Rand) types.TSS { OperatorAddressList: []string{}, FinalizedZetaHeight: r.Int63(), KeyGenZetaHeight: r.Int63(), - } + }, nil } // TODO: rename to TSS @@ -287,6 +320,17 @@ func GasPriceIncreaseFlags() types.GasPriceIncreaseFlags { } } +func GasPriceIncreaseFlagsFromRand(r *rand.Rand) types.GasPriceIncreaseFlags { + minValue := 1 + maxValue := 100 + return types.GasPriceIncreaseFlags{ + EpochLength: int64(r.Intn(maxValue-minValue) + minValue), + RetryInterval: time.Duration(r.Intn(maxValue-minValue) + minValue), + GasPriceIncreasePercent: 1, + MaxPendingCctxs: 100, + } +} + func OperationalFlags() types.OperationalFlags { return types.OperationalFlags{ RestartHeight: 1, diff --git a/testutil/sample/sample.go b/testutil/sample/sample.go index 3f0390ddad..c9f416a088 100644 --- a/testutil/sample/sample.go +++ b/testutil/sample/sample.go @@ -93,12 +93,22 @@ func Uint64InRange(low, high uint64) uint64 { return r.Uint64()%(high-low) + low } +// Uint64InRange returns a sample uint64 in the given ranges +func Uint64InRangeFromRand(r *rand.Rand, low, high uint64) uint64 { + return r.Uint64()%(high-low) + low +} + // Int64InRange returns a sample int64 in the given ranges func Int64InRange(low, high int64) int64 { r := newRandFromSeed(low) return r.Int63()%(high-low) + low } +// Int64InRangeFromRand returns a sample int64 in the given ranges +func Int64InRangeFromRand(r *rand.Rand, low, high int64) int64 { + return r.Int63()%(high-low) + low +} + func UintInRange(low, high uint64) sdkmath.Uint { u := Uint64InRange(low, high) return sdkmath.NewUint(u) diff --git a/x/crosschain/genesis.go b/x/crosschain/genesis.go index 8ea365e059..0f6856d559 100644 --- a/x/crosschain/genesis.go +++ b/x/crosschain/genesis.go @@ -1,7 +1,6 @@ package crosschain import ( - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/node/x/crosschain/keeper" @@ -11,9 +10,8 @@ import ( // InitGenesis initializes the crosschain module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - // Always set the zeta accounting to zero at genesis. - // ZetaAccounting value is build by iterating through all the cctxs and adding the amount to the zeta accounting. - k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: sdkmath.ZeroUint()}) + k.SetZetaAccounting(ctx, genState.ZetaAccounting) + // Set all the outbound tracker for _, elem := range genState.OutboundTrackerList { k.SetOutboundTracker(ctx, elem) @@ -36,8 +34,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the chain nonces - // Set all the last block heights for _, elem := range genState.LastBlockHeightList { if elem != nil { @@ -45,15 +41,15 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the cross-chain txs - tss, found := k.GetObserverKeeper().GetTSS(ctx) - if found { - for _, elem := range genState.CrossChainTxs { - if elem != nil { - k.SaveCCTXUpdate(ctx, *elem, tss.TssPubkey) - } + // Set the cross-chain transactions only, + // We don't need to call SaveCCTXUpdate as the other fields are being set already + for _, elem := range genState.CrossChainTxs { + if elem != nil { + cctx := *elem + k.SetCrossChainTx(ctx, cctx) } } + for _, elem := range genState.FinalizedInbounds { k.SetFinalizedInbound(ctx, elem) } diff --git a/x/crosschain/keeper/cctx_utils.go b/x/crosschain/keeper/cctx_utils.go index 3f62202cb7..3fedf03cef 100644 --- a/x/crosschain/keeper/cctx_utils.go +++ b/x/crosschain/keeper/cctx_utils.go @@ -30,7 +30,6 @@ func (k Keeper) SetObserverOutboundInfo(ctx sdk.Context, receiveChainID int64, c "identifiers: %s (chain %q)", cctx.LogIdentifierForCCTX(), chain.Name, ) } - // SET nonce cctx.GetCurrentOutboundParam().TssNonce = nonce.Nonce tss, found := k.GetObserverKeeper().GetTSS(ctx) diff --git a/x/crosschain/keeper/msg_server_add_outbound_tracker.go b/x/crosschain/keeper/msg_server_add_outbound_tracker.go index 9beffffd1e..360a8f8301 100644 --- a/x/crosschain/keeper/msg_server_add_outbound_tracker.go +++ b/x/crosschain/keeper/msg_server_add_outbound_tracker.go @@ -13,7 +13,6 @@ import ( ) // MaxOutboundTrackerHashes is the maximum number of hashes that can be stored in the outbound transaction tracker -const MaxOutboundTrackerHashes = 5 // AddOutboundTracker adds a new record to the outbound transaction tracker. // only the admin policy account and the observer validators are authorized to broadcast this message without proof. @@ -45,7 +44,6 @@ func (k msgServer) AddOutboundTracker( msg.Nonce, ) } - // tracker submission is only allowed when the cctx is pending if !IsPending(cctx.CrossChainTx) { // garbage tracker (for any reason) is harmful to outTx observation and should be removed if it exists @@ -87,7 +85,7 @@ func (k msgServer) AddOutboundTracker( } // check if max hashes are reached - if len(tracker.HashList) >= MaxOutboundTrackerHashes { + if tracker.MaxReached() { return nil, types.ErrMaxTxOutTrackerHashesReached.Wrapf( "max hashes reached for chain %d, nonce %d, hash number: %d", msg.ChainId, diff --git a/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go b/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go index fb477c9920..3de3bacf0b 100644 --- a/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go +++ b/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go @@ -226,8 +226,8 @@ func TestMsgServer_AddToOutboundTracker(t *testing.T) { observerMock.On("IsNonTombstonedObserver", mock.Anything, mock.Anything).Return(false) keepertest.MockCctxByNonce(t, ctx, *k, observerMock, types.CctxStatus_PendingOutbound, false) - hashes := make([]*types.TxHash, keeper.MaxOutboundTrackerHashes) - for i := 0; i < keeper.MaxOutboundTrackerHashes; i++ { + hashes := make([]*types.TxHash, types.MaxOutboundTrackerHashes) + for i := 0; i < types.MaxOutboundTrackerHashes; i++ { hashes[i] = &types.TxHash{ TxHash: sample.Hash().Hex(), } diff --git a/x/crosschain/keeper/msg_server_refund_aborted_tx.go b/x/crosschain/keeper/msg_server_refund_aborted_tx.go index 22e2b5da81..a4f899b9e2 100644 --- a/x/crosschain/keeper/msg_server_refund_aborted_tx.go +++ b/x/crosschain/keeper/msg_server_refund_aborted_tx.go @@ -75,7 +75,6 @@ func (k msgServer) RefundAbortedCCTX( cctx.CctxStatus.AbortRefunded() k.SetCrossChainTx(ctx, cctx) - return &types.MsgRefundAbortedCCTXResponse{}, nil } diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx.go b/x/crosschain/keeper/msg_server_vote_inbound_tx.go index 263b7b23bc..0c1b875479 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx.go @@ -64,6 +64,7 @@ func (k msgServer) VoteInbound( // vote on inbound ballot // use a temporary context to not commit any ballot state change in case of error tmpCtx, commit := ctx.CacheContext() + finalized, isNew, err := k.zetaObserverKeeper.VoteOnInboundBallot( tmpCtx, msg.SenderChainId, diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go index 7fc5c9be99..57e73a72ae 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go @@ -52,7 +52,7 @@ func TestKeeper_VoteInbound(t *testing.T) { msgServer := keeper.NewMsgServerImpl(*k) validatorList := setObservers(t, k, ctx, zk) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { @@ -185,7 +185,7 @@ func TestKeeper_VoteInbound(t *testing.T) { observerMock.On("VoteOnInboundBallot", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(true, false, errors.New("err")) msgServer := keeper.NewMsgServerImpl(*k) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId msg := sample.InboundVote(0, from, to) res, err := msgServer.VoteInbound( @@ -208,7 +208,7 @@ func TestKeeper_VoteInbound(t *testing.T) { zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ ObserverList: observerSet, }) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { @@ -255,7 +255,7 @@ func TestKeeper_VoteInbound(t *testing.T) { Return(true, false, nil) observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, false) msgServer := keeper.NewMsgServerImpl(*k) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId msg := sample.InboundVote(0, from, to) res, err := msgServer.VoteInbound( diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index fff9feb92a..155bf06770 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -107,6 +107,7 @@ func (k msgServer) WhitelistERC20( msg.ChainId, ) } + if zrc20Addr == (ethcommon.Address{}) { return nil, errorsmod.Wrapf( types.ErrDeployContract, diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go index c82261bd05..1b2c03710c 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go @@ -18,6 +18,11 @@ import ( ) func TestKeeper_WhitelistERC20(t *testing.T) { + r := sample.Rand() + firstTokenAddress, err := sample.SolanaAddressFromRand(r) + require.NoError(t, err) + secondTokenAddress, err := sample.SolanaAddressFromRand(r) + require.NoError(t, err) tests := []struct { name string tokenAddress string @@ -36,6 +41,12 @@ func TestKeeper_WhitelistERC20(t *testing.T) { secondTokenAddress: sample.SolanaAddress(t), chainID: getValidSolanaChainID(), }, + { + name: "can deploy and whitelist a spl", + tokenAddress: firstTokenAddress, + secondTokenAddress: secondTokenAddress, + chainID: getValidSolanaChainID(), + }, } for _, tt := range tests { diff --git a/x/crosschain/keeper/refund.go b/x/crosschain/keeper/refund.go index 6c9deef32f..8d1e2da230 100644 --- a/x/crosschain/keeper/refund.go +++ b/x/crosschain/keeper/refund.go @@ -27,7 +27,7 @@ func (k Keeper) RefundAbortedAmountOnZetaChain( case coin.CoinType_ERC20: return k.RefundAmountOnZetaChainERC20(ctx, cctx, refundAddress) default: - return errors.New("unsupported coin type for refund on ZetaChain") + return fmt.Errorf("unsupported coin type for refund on ZetaChain : %s", coinType) } } diff --git a/x/crosschain/simulation/decoders.go b/x/crosschain/simulation/decoders.go index dd2c6c4e07..9c7f743960 100644 --- a/x/crosschain/simulation/decoders.go +++ b/x/crosschain/simulation/decoders.go @@ -19,44 +19,80 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { var cctxA, cctxB types.CrossChainTx cdc.MustUnmarshal(kvA.Value, &cctxA) cdc.MustUnmarshal(kvB.Value, &cctxB) - return fmt.Sprintf("%v\n%v", cctxA, cctxB) + return fmt.Sprintf("key %s value A %v value B %v", types.CCTXKey, cctxA, cctxB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockHeightKey)): var lastBlockHeightA, lastBlockHeightB types.LastBlockHeight cdc.MustUnmarshal(kvA.Value, &lastBlockHeightA) cdc.MustUnmarshal(kvB.Value, &lastBlockHeightB) - return fmt.Sprintf("%v\n%v", lastBlockHeightA, lastBlockHeightB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockHeightKey, + lastBlockHeightA, + lastBlockHeightB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.FinalizedInboundsKey)): var finalizedInboundsA, finalizedInboundsB []byte finalizedInboundsA = kvA.Value finalizedInboundsB = kvB.Value - return fmt.Sprintf("%v\n%v", finalizedInboundsA, finalizedInboundsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.FinalizedInboundsKey, + finalizedInboundsA, + finalizedInboundsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.GasPriceKey)): var gasPriceA, gasPriceB types.GasPrice cdc.MustUnmarshal(kvA.Value, &gasPriceA) cdc.MustUnmarshal(kvB.Value, &gasPriceB) - return fmt.Sprintf("%v\n%v", gasPriceA, gasPriceB) + return fmt.Sprintf("key %s value A %v value B %v", types.GasPriceKey, gasPriceA, gasPriceB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.OutboundTrackerKeyPrefix)): var outboundTrackerA, outboundTrackerB types.OutboundTracker cdc.MustUnmarshal(kvA.Value, &outboundTrackerA) cdc.MustUnmarshal(kvB.Value, &outboundTrackerB) - return fmt.Sprintf("%v\n%v", outboundTrackerA, outboundTrackerB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.OutboundTrackerKeyPrefix, + outboundTrackerA, + outboundTrackerB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.InboundTrackerKeyPrefix)): var inboundTrackerA, inboundTrackerB types.InboundTracker cdc.MustUnmarshal(kvA.Value, &inboundTrackerA) cdc.MustUnmarshal(kvB.Value, &inboundTrackerB) - return fmt.Sprintf("%v\n%v", inboundTrackerA, inboundTrackerB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.InboundTrackerKeyPrefix, + inboundTrackerA, + inboundTrackerB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ZetaAccountingKey)): var zetaAccountingA, zetaAccountingB types.ZetaAccounting cdc.MustUnmarshal(kvA.Value, &zetaAccountingA) cdc.MustUnmarshal(kvB.Value, &zetaAccountingB) - return fmt.Sprintf("%v\n%v", zetaAccountingA, zetaAccountingB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.ZetaAccountingKey, + zetaAccountingA, + zetaAccountingB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.RateLimiterFlagsKey)): var rateLimiterFlagsA, rateLimiterFlagsB types.RateLimiterFlags cdc.MustUnmarshal(kvA.Value, &rateLimiterFlagsA) cdc.MustUnmarshal(kvB.Value, &rateLimiterFlagsB) - return fmt.Sprintf("%v\n%v", rateLimiterFlagsA, rateLimiterFlagsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.RateLimiterFlagsKey, + rateLimiterFlagsA, + rateLimiterFlagsB, + ) default: - panic(fmt.Sprintf("invalid crosschain key prefix %X", kvA.Key[:1])) + panic( + fmt.Sprintf( + "invalid crosschain key prefix %X (first 8 bytes: %X)", + kvA.Key[:1], + kvA.Key[:min(8, len(kvA.Key))], + ), + ) } } } diff --git a/x/crosschain/simulation/decoders_test.go b/x/crosschain/simulation/decoders_test.go index 9765aca43d..8c22cf9707 100644 --- a/x/crosschain/simulation/decoders_test.go +++ b/x/crosschain/simulation/decoders_test.go @@ -41,14 +41,37 @@ func TestDecodeStore(t *testing.T) { name string expectedLog string }{ - {"CrossChainTx", fmt.Sprintf("%v\n%v", *cctx, *cctx)}, - {"LastBlockHeight", fmt.Sprintf("%v\n%v", *lastBlockHeight, *lastBlockHeight)}, - {"GasPrice", fmt.Sprintf("%v\n%v", *gasPrice, *gasPrice)}, - {"OutboundTracker", fmt.Sprintf("%v\n%v", outboundTracker, outboundTracker)}, - {"InboundTracker", fmt.Sprintf("%v\n%v", inboundTracker, inboundTracker)}, - {"ZetaAccounting", fmt.Sprintf("%v\n%v", zetaAccounting, zetaAccounting)}, - {"RateLimiterFlags", fmt.Sprintf("%v\n%v", rateLimiterFlags, rateLimiterFlags)}, - {"FinalizedInbounds", fmt.Sprintf("%v\n%v", []byte{1}, []byte{1})}, + {"CrossChainTx", fmt.Sprintf("key %s value A %v value B %v", types.CCTXKey, *cctx, *cctx)}, + { + "LastBlockHeight", + fmt.Sprintf("key %s value A %v value B %v", types.LastBlockHeightKey, *lastBlockHeight, *lastBlockHeight), + }, + {"GasPrice", fmt.Sprintf("key %s value A %v value B %v", types.GasPriceKey, *gasPrice, *gasPrice)}, + { + "OutboundTracker", + fmt.Sprintf( + "key %s value A %v value B %v", + types.OutboundTrackerKeyPrefix, + outboundTracker, + outboundTracker, + ), + }, + { + "InboundTracker", + fmt.Sprintf("key %s value A %v value B %v", types.InboundTrackerKeyPrefix, inboundTracker, inboundTracker), + }, + { + "ZetaAccounting", + fmt.Sprintf("key %s value A %v value B %v", types.ZetaAccountingKey, zetaAccounting, zetaAccounting), + }, + { + "RateLimiterFlags", + fmt.Sprintf("key %s value A %v value B %v", types.RateLimiterFlagsKey, rateLimiterFlags, rateLimiterFlags), + }, + { + "FinalizedInbounds", + fmt.Sprintf("key %s value A %v value B %v", types.FinalizedInboundsKey, []byte{1}, []byte{1}), + }, } for i, tt := range tests { diff --git a/x/crosschain/simulation/operation_abort_stuck_cctx.go b/x/crosschain/simulation/operation_abort_stuck_cctx.go new file mode 100644 index 0000000000..55d0a1cd60 --- /dev/null +++ b/x/crosschain/simulation/operation_abort_stuck_cctx.go @@ -0,0 +1,128 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAbortStuckCCTX generates a MsgAbortStuckCCTX with random values +func SimulateMsgAbortStuckCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Pick a ethereum chain to abort a stuck cctx + chainID := chains.GoerliLocalnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no supported chains found", + ), nil, nil + } + + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + chainID = chain.ChainId + } + } + + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAbortStuckCCTX, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no TSS found", + ), nil, nil + } + + pendingNonces, found := k.GetObserverKeeper().GetPendingNonces(ctx, tss.TssPubkey, chainID) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no pending nonces found", + ), nil, nil + } + + // If nonce low is the same as nonce high, it means that there are no pending nonces to add trackers for + if pendingNonces.NonceLow == pendingNonces.NonceHigh { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no pending nonces found", + ), nil, nil + } + // Pick a random pending nonce + nonce := 0 + switch { + case pendingNonces.NonceHigh <= 1: + nonce = int(pendingNonces.NonceLow) + case pendingNonces.NonceLow == 0: + nonce = r.Intn(int(pendingNonces.NonceHigh)) + default: + nonce = r.Intn(int(pendingNonces.NonceHigh)-int(pendingNonces.NonceLow)) + int(pendingNonces.NonceLow) + } + + nonceToCctx, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tss.TssPubkey, chainID, int64(nonce)) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no cctx found", + ), nil, nil + } + + cctx, found := k.GetCrossChainTx(ctx, nonceToCctx.CctxIndex) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no cctx found", + ), nil, nil + } + + msg := types.MsgAbortStuckCCTX{ + Creator: policyAccount.Address.String(), + CctxIndex: cctx.Index, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate MsgAbortStuckCCTX msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_add_inbound_tracker.go b/x/crosschain/simulation/operation_add_inbound_tracker.go new file mode 100644 index 0000000000..909c5cd9dc --- /dev/null +++ b/x/crosschain/simulation/operation_add_inbound_tracker.go @@ -0,0 +1,72 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAddInboundTracker generates a MsgAddInboundTracker with random values +func SimulateMsgAddInboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddInboundTracker, + "no supported chains found", + ), nil, nil + } + randomChainID := GetRandomChainID(r, supportedChains) + txHash := sample.HashFromRand(r) + coinType := sample.CoinTypeFromRand(r) + + // Add a new inbound Tracker + msg := types.MsgAddInboundTracker{ + Creator: randomObserver, + ChainId: randomChainID, + TxHash: txHash.String(), + CoinType: coinType, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate MsgAddInboundTracker"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_add_outbound_tracker.go b/x/crosschain/simulation/operation_add_outbound_tracker.go new file mode 100644 index 0000000000..1640bfcf07 --- /dev/null +++ b/x/crosschain/simulation/operation_add_outbound_tracker.go @@ -0,0 +1,152 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAddOutboundTracker generates a MsgAddOutboundTracker with random values +func SimulateMsgAddOutboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + chainID := chains.GoerliLocalnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no supported chains found", + ), nil, nil + } + + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + chainID = chain.ChainId + } + } + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + txHash := sample.HashFromRand(r) + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no TSS found", + ), nil, nil + } + + pendingNonces, found := k.GetObserverKeeper().GetPendingNonces(ctx, tss.TssPubkey, chainID) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "pending nonces object not found", + ), nil, nil + } + + // pick a random nonce from the pending nonces between 0 and nonceLow + // If nonce low is the same as nonce high, it means that there are no pending nonces to add trackers for + if pendingNonces.NonceLow == pendingNonces.NonceHigh { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no pending nonces found", + ), nil, nil + } + // Pick a random pending nonce + nonce := 0 + switch { + case pendingNonces.NonceHigh <= 1: + nonce = int(pendingNonces.NonceLow) + case pendingNonces.NonceLow == 0: + nonce = r.Intn(int(pendingNonces.NonceHigh)) + default: + nonce = r.Intn(int(pendingNonces.NonceHigh)-int(pendingNonces.NonceLow)) + int(pendingNonces.NonceLow) + } + + // Verify if the tracker is maxed + tracker, found := k.GetOutboundTracker( + ctx, + chainID, + uint64(nonce), + ) // #nosec G115 - overflow is not an issue here + if found && tracker.MaxReached() { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "tracker is maxed", + ), nil, nil + } + + // Verify the nonceToCCTX exists + nonceToCCTX, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tss.TssPubkey, chainID, int64(nonce)) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no nonce to cctx found", + ), nil, nil + } + + // Verify the cctx exists + _, found = k.GetCrossChainTx(ctx, nonceToCCTX.CctxIndex) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no cctx found for nonce", + ), nil, nil + } + // Add a new inbound Tracker + msg := types.MsgAddOutboundTracker{ + Creator: randomObserver, + ChainId: chainID, + Nonce: uint64(nonce), // #nosec G115 - overflow is not an issue here + TxHash: txHash.String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgAddOutboundTracker msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_gas_price_voter.go b/x/crosschain/simulation/operation_gas_price_voter.go new file mode 100644 index 0000000000..65970dcc91 --- /dev/null +++ b/x/crosschain/simulation/operation_gas_price_voter.go @@ -0,0 +1,78 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgVoteGasPrice generates a MsgVoteGasPrice and delivers it +func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + authz.GasPriceVoter.String(), + "no supported chains found", + ), nil, nil + } + randomChainID := GetRandomChainID(r, supportedChains) + // Vote for random gas price. Gas prices do not use a ballot system, so we can vote directly without having to schedule future operations. + gasPrice := sample.GasPriceFromRand(r, randomChainID) + msg := types.MsgVoteGasPrice{ + Creator: randomObserver, + ChainId: randomChainID, + Price: gasPrice.Prices[0], + PriorityFee: gasPrice.PriorityFees[0], + BlockNumber: uint64(ctx.BlockHeight()) + r.Uint64()%1000, // #nosec G115 - overflow is not a issue here + Supply: sdk.NewInt(r.Int63n(1e18)).String(), + } + + // System contracts are deployed on the first block, so we cannot vote on gas prices before that + if ctx.BlockHeight() <= 1 { + return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_refund_aborted_cctx.go b/x/crosschain/simulation/operation_refund_aborted_cctx.go new file mode 100644 index 0000000000..9f46da5933 --- /dev/null +++ b/x/crosschain/simulation/operation_refund_aborted_cctx.go @@ -0,0 +1,84 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgRefundAbortedCCTX generates a MsgRefundAbortedCCTX with random values +func SimulateMsgRefundAbortedCCTX(k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins} + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.RefundAborted, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + cctxList := k.GetAllCrossChainTx(ctx) + abortedCctx := types.CrossChainTx{} + abortedCctxFound := false + + for _, cctx := range cctxList { + if cctx.CctxStatus.Status == types.CctxStatus_Aborted { + if !cctx.InboundParams.CoinType.SupportsRefund() { + continue + } + if cctx.CctxStatus.IsAbortRefunded { + continue + } + abortedCctx = cctx + abortedCctxFound = true + break + } + } + if !abortedCctxFound { + return simtypes.NoOpMsg(types.ModuleName, types.RefundAborted, "no aborted cctx found"), nil, nil + } + + msg := types.MsgRefundAbortedCCTX{ + Creator: policyAccount.Address.String(), + CctxIndex: abortedCctx.Index, + RefundAddress: sample.EthAddressFromRand(r).String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgRefundAbortedCCTX msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_remove_outbound_tracker.go b/x/crosschain/simulation/operation_remove_outbound_tracker.go new file mode 100644 index 0000000000..6eaa52be81 --- /dev/null +++ b/x/crosschain/simulation/operation_remove_outbound_tracker.go @@ -0,0 +1,72 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgRemoveOutboundTracker generates a MsgRemoveOutboundTracker with random values +func SimulateMsgRemoveOutboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgRemoveOutboundTracker, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + trackers := k.GetAllOutboundTracker(ctx) + + if len(trackers) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveOutboundTracker, + "no outbound trackers found", + ), nil, nil + } + + randomTracker := trackers[r.Intn(len(trackers))] + + msg := types.MsgRemoveOutboundTracker{ + ChainId: randomTracker.ChainId, + Nonce: randomTracker.Nonce, + Creator: policyAccount.Address.String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgRemoveOutboundTracker", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_erc20_pause_status.go b/x/crosschain/simulation/operation_update_erc20_pause_status.go new file mode 100644 index 0000000000..a4fa5b30c0 --- /dev/null +++ b/x/crosschain/simulation/operation_update_erc20_pause_status.go @@ -0,0 +1,120 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateUpdateERC20CustodyPauseStatus generates a MsgUpdateERC20CustodyPauseStatus with random values and delivers it +func SimulateUpdateERC20CustodyPauseStatus(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeUpdateERC20CustodyPauseStatus, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no supported chains found", + ), nil, nil + } + + filteredChains := chains.FilterChains(supportedChains, chains.FilterExternalChains) + + //pick a random chain + randomChain := filteredChains[r.Intn(len(filteredChains))] + + _, found := k.GetObserverKeeper().GetChainNonces(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no chain nonces found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no TSS found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetChainParamsByChainID(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no chain params found", + ), nil, nil + } + medianGasPrice, priorityFee, found := k.GetMedianGasValues(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no median gas values found", + ), nil, nil + } + medianGasPrice = medianGasPrice.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + priorityFee = priorityFee.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + + if priorityFee.GT(medianGasPrice) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "priorityFee is greater than median gasPrice", + ), nil, nil + } + + msg := types.MsgUpdateERC20CustodyPauseStatus{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + Pause: r.Intn(2) == 0, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateERC20CustodyPauseStatus msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_rate_limiter_flags.go b/x/crosschain/simulation/operation_update_rate_limiter_flags.go new file mode 100644 index 0000000000..f835e57e24 --- /dev/null +++ b/x/crosschain/simulation/operation_update_rate_limiter_flags.go @@ -0,0 +1,60 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgUpdateRateLimiterFlags generates a MsgUpdateRateLimiterFlags with random values +func SimulateMsgUpdateRateLimiterFlags(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins} + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateRateLimiterFlags, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgUpdateRateLimiterFlags{ + Creator: policyAccount.Address.String(), + RateLimiterFlags: sample.RateLimiterFlagsFromRand(r), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateRateLimiterFlags msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_tss_address.go b/x/crosschain/simulation/operation_update_tss_address.go new file mode 100644 index 0000000000..aac70b512b --- /dev/null +++ b/x/crosschain/simulation/operation_update_tss_address.go @@ -0,0 +1,129 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" + observertypes "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateMsgUpdateTssAddress generates a MsgUpdateTssAddress with random values and delivers it +func SimulateMsgUpdateTssAddress(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateTssAddress, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetChainsSupportingTSSMigration(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no chains found which support tss migration", + ), nil, nil + } + + cctxList := k.GetAllCrossChainTx(ctx) + if len(cctxList) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no cross chain txs found", + ), nil, nil + } + + // Pick any cctx with status OutboundMined, and use its index for the migration + // We set the fund migrator directly as we are not simulating MsgMigrateTssFunds + minedCCTX := types.CrossChainTx{} + foundMined := false + for _, cctx := range cctxList { + if cctx.CctxStatus.Status == types.CctxStatus_OutboundMined { + minedCCTX = cctx + foundMined = true + break + } + } + if !foundMined { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no mined cross chain txs found in mined state", + ), nil, nil + } + + // Thee tss migrator is set for all chains supporting tss migration + for _, chain := range supportedChains { + tssMigrator := observertypes.TssFundMigratorInfo{ + ChainId: chain.ChainId, + MigrationCctxIndex: minedCCTX.Index, + } + k.GetObserverKeeper().SetFundMigrator(ctx, tssMigrator) + } + + oldTss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no TSS found", + ), nil, nil + } + + // Set the new TSS to state + newTss, err := sample.TSSFromRand(r) + newTss.FinalizedZetaHeight = oldTss.FinalizedZetaHeight + 10 + newTss.KeyGenZetaHeight = oldTss.KeyGenZetaHeight + 10 + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + err.Error()), + nil, nil + } + k.GetObserverKeeper().SetTSSHistory(ctx, newTss) + + msg := types.MsgUpdateTssAddress{ + Creator: policyAccount.Address.String(), + TssPubkey: newTss.TssPubkey, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateTssAddress msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_vote_inbound.go b/x/crosschain/simulation/operation_vote_inbound.go new file mode 100644 index 0000000000..714d649aaa --- /dev/null +++ b/x/crosschain/simulation/operation_vote_inbound.go @@ -0,0 +1,190 @@ +package simulation + +import ( + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// operationSimulateVoteInbound generates a MsgVoteInbound with a random vote and delivers it. +func operationSimulateVoteInbound( + k keeper.Keeper, + msg types.MsgVoteInbound, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateVoteInbound generates a MsgVoteInbound with random values and delivers it. It also schedules future operations for subsequent votes. +func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { + observerVotesTransitionMatrix, statePercentageArray, curNumVotesState := ObserverVotesSimulationMatrix() + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + from = chain.ChainId + } + if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { + to = chain.ChainId + } + } + + asset, err := GetAsset(ctx, k.GetFungibleKeeper(), from) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "unable to get asset"), nil, err + } + + // Generate a random inbound vote , coin type is randomly selected + msg := sample.InboundVoteFromRand(from, to, r, asset) + + cf, found := k.GetObserverKeeper().GetCrosschainFlags(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "crosschain flags not found"), nil, nil + } + + // Return early if inbound is not enabled. + if !cf.IsInboundEnabled { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "inbound is not enabled"), nil, nil + } + + // Return early if the inbound has already been finalized. + if k.IsFinalizedInbound(ctx, msg.InboundHash, msg.SenderChainId, msg.EventIndex) { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "inbound already finalized"), nil, nil + } + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetObserverKeeper().GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first inbound vote"), nil, err + } + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + // Add subsequent votes + observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "observer set not found"), nil, nil + } + + // 1) Schedule operations for votes + // 1.1) first pick a number of people to vote. + curNumVotesState = observerVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) + + // 1.2) select who votes + whoVotes := r.Perm(len(observerSet.ObserverList)) + whoVotes = whoVotes[:numVotes] + + var fops []simtypes.FutureOperation + + for _, observerIdx := range whoVotes { + observerAddress := observerSet.ObserverList[observerIdx] + // firstVoter has already voted. + if observerAddress == firstVoter { + continue + } + observerAccount, err := GetObserverAccount(observerAddress, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = observerAddress + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteInbound(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/crosschain/simulation/operation_vote_outbound.go b/x/crosschain/simulation/operation_vote_outbound.go new file mode 100644 index 0000000000..bf4dbaf5dd --- /dev/null +++ b/x/crosschain/simulation/operation_vote_outbound.go @@ -0,0 +1,226 @@ +package simulation + +import ( + "fmt" + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + ethcommon "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +func operationSimulateVoteOutbound( + k keeper.Keeper, + msg types.MsgVoteOutbound, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateVoteOutbound generates a MsgVoteOutbound with random values and delivers it. +//It also schedules future operations for subsequent votes. + +func SimulateVoteOutbound(k keeper.Keeper) simtypes.Operation { + defaultVote := chains.ReceiveStatus_success + alternativeVote := chains.ReceiveStatus_failed + observerVotesTransitionMatrix, statePercentageArray, curNumVotesState := ObserverVotesSimulationMatrix() + ballotVotesTransitionMatrix, yesVotePercentageArray, ballotVotesState := BallotVoteSimulationMatrix() + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + to = chain.ChainId + } + if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { + from = chain.ChainId + } + } + + _, creator, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + index := ethcrypto.Keccak256Hash([]byte(fmt.Sprintf("%d", r.Int63()))).Hex() + + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "tss not found"), nil, nil + } + + asset, err := GetAsset(ctx, k.GetFungibleKeeper(), from) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "unable to get asset"), nil, err + } + + // Generate a new cctx and save it , which can be used to finalize the outbound + cctx := sample.CCTXfromRand(r, creator, index, to, from, tss.TssPubkey, asset) + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: defaultVote, + Creator: cctx.Creator, + ObservedOutboundHash: ethcommon.BytesToHash(sample.EthAddressFromRand(r).Bytes()).String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: cctx.GetCurrentOutboundParam().ObservedExternalHeight, + ObservedOutboundEffectiveGasPrice: cctx.GetCurrentOutboundParam().EffectiveGasPrice, + ObservedOutboundGasUsed: cctx.GetCurrentOutboundParam().GasUsed, + CoinType: cctx.InboundParams.CoinType, + } + + err = k.SetObserverOutboundInfo(ctx, to, &cctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to set observer outbound info"), nil, err + } + + msg.OutboundTssNonce = cctx.GetCurrentOutboundParam().TssNonce + k.SaveCCTXUpdate(ctx, cctx, tss.TssPubkey) + + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetObserverKeeper().GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first outbound vote"), nil, err + } + + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + // Add subsequent votes + observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "observer set not found"), nil, nil + } + + // 1) Schedule operations for votes + // 1.1) first pick a number of people to vote. + curNumVotesState = observerVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) + + // 1.2) select who votes + whoVotes := r.Perm(len(observerSet.ObserverList)) + whoVotes = whoVotes[:numVotes] + + var fops []simtypes.FutureOperation + + ballotVotesState = ballotVotesTransitionMatrix.NextState(r, ballotVotesState) + yesVotePercentage := yesVotePercentageArray[ballotVotesState] + numberOfYesVotes := int(math.Ceil(float64(numVotes) * yesVotePercentage)) + vote := defaultVote + + for voteCount, observerIdx := range whoVotes { + if voteCount == numberOfYesVotes { + vote = alternativeVote + } + observerAddress := observerSet.ObserverList[observerIdx] + // firstVoter has already voted. + if observerAddress == firstVoter { + continue + } + observerAccount, err := GetObserverAccount(observerAddress, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = observerAddress + votingMsg.Status = vote + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteOutbound(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/crosschain/simulation/operation_whitelist_erc20.go b/x/crosschain/simulation/operation_whitelist_erc20.go new file mode 100644 index 0000000000..b67cad6a44 --- /dev/null +++ b/x/crosschain/simulation/operation_whitelist_erc20.go @@ -0,0 +1,147 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgWhitelistERC20 generates a MsgWhitelistERC20 with random values and delivers it +func SimulateMsgWhitelistERC20(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgWhitelistERC20, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no supported chains found", + ), nil, nil + } + + filteredChains := chains.FilterChains(supportedChains, chains.FilterByVM(chains.Vm_evm)) + if len(filteredChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no EVM-compatible chains found", + ), nil, nil + } + + //pick a random chain + // Keep the switch case to add solana support in future + // TODO : https://github.com/zeta-chain/node/issues/3287 + randomChain := filteredChains[r.Intn(len(filteredChains))] + var tokenAddress string + switch { + case randomChain.IsEVMChain(): + tokenAddress = sample.EthAddressFromRand(r).String() + default: + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgWhitelistERC20, "unsupported chain"), nil, nil + } + + _, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no TSS found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetChainParamsByChainID(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no chain params found", + ), nil, nil + } + + medianGasPrice, priorityFee, isFound := k.GetMedianGasValues(ctx, randomChain.ChainId) + if !isFound { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "median gas price not found", + ), nil, nil + } + + medianGasPrice = medianGasPrice.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + priorityFee = priorityFee.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + + if priorityFee.GT(medianGasPrice) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "priorityFee is greater than median gasPrice", + ), nil, nil + } + + foreignCoins := k.GetFungibleKeeper().GetAllForeignCoins(ctx) + for _, fCoin := range foreignCoins { + if fCoin.Asset == tokenAddress && fCoin.ForeignChainId == randomChain.ChainId { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "ERC20 already whitelisted", + ), nil, nil + } + } + + gasLimit := r.Int63n(1000000000) + 1 + nameLength := r.Intn(97) + 3 + msg := types.MsgWhitelistERC20{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + Erc20Address: tokenAddress, + GasLimit: gasLimit, + Decimals: 18, + Name: sample.StringRandom(r, nameLength), + Symbol: sample.StringRandom(r, 3), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgWhitelistERC20", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operations.go b/x/crosschain/simulation/operations.go index 96b04970b9..5d2403a64f 100644 --- a/x/crosschain/simulation/operations.go +++ b/x/crosschain/simulation/operations.go @@ -2,26 +2,24 @@ package simulation import ( "fmt" - "math" "math/rand" - "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "github.com/zeta-chain/node/pkg/authz" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/x/crosschain/keeper" "github.com/zeta-chain/node/x/crosschain/types" observerTypes "github.com/zeta-chain/node/x/observer/types" ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -30,55 +28,57 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( - DefaultWeightMsgAddOutboundTracker = 50 - DefaultWeightAddInboundTracker = 50 - DefaultWeightRemoveOutboundTracker = 5 - DefaultWeightVoteGasPrice = 100 - DefaultWeightVoteOutbound = 50 - DefaultWeightVoteInbound = 100 - DefaultWeightWhitelistERC20 = 1 - DefaultWeightMigrateTssFunds = 1 - DefaultWeightUpdateTssAddress = 1 - DefaultWeightAbortStuckCCTX = 10 - DefaultWeightUpdateRateLimiterFlags = 1 - - OpWeightMsgAddOutboundTracker = "op_weight_msg_add_outbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightAddInboundTracker = "op_weight_msg_add_inbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightRemoveOutboundTracker = "op_weight_msg_remove_outbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightVoteGasPrice = "op_weight_msg_vote_gas_price" // #nosec G101 not a hardcoded credential - OpWeightVoteOutbound = "op_weight_msg_vote_outbound" // #nosec G101 not a hardcoded credential - OpWeightVoteInbound = "op_weight_msg_vote_inbound" // #nosec G101 not a hardcoded credential - OpWeightWhitelistERC20 = "op_weight_msg_whitelist_erc20" // #nosec G101 not a hardcoded credential - OpWeightMigrateTssFunds = "op_weight_msg_migrate_tss_funds" // #nosec G101 not a hardcoded credential - OpWeightUpdateTssAddress = "op_weight_msg_update_tss_address" // #nosec G101 not a hardcoded credential - OpWeightAbortStuckCCTX = "op_weight_msg_abort_stuck_cctx" // #nosec G101 not a hardcoded credential - OpWeightUpdateRateLimiterFlags = "op_weight_msg_update_rate_limiter_flags" // #nosec G101 not a hardcoded credential - + DefaultWeightAddOutboundTracker = 10 + DefaultWeightAddInboundTracker = 10 + DefaultWeightRemoveOutboundTracker = 10 + DefaultWeightVoteGasPrice = 50 + DefaultWeightVoteOutbound = 10 + DefaultWeightVoteInbound = 10 + DefaultWeightWhitelistERC20 = 10 + DefaultWeightMigrateTssFunds = 1 + DefaultWeightUpdateTssAddress = 10 + DefaultWeightAbortStuckCCTX = 5 + DefaultWeightUpdateRateLimiterFlags = 10 + DefaultWeightRefundAbortedCCTX = 10 + DefaultWeightUpdateERC20CustodyPauseStatus = 10 + + OpWeightMsgAddOutboundTracker = "op_weight_msg_add_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightAddInboundTracker = "op_weight_msg_add_inbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightRemoveOutboundTracker = "op_weight_msg_remove_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightVoteGasPrice = "op_weight_msg_vote_gas_price" // #nosec G101 not a hardcoded credential + OpWeightVoteOutbound = "op_weight_msg_vote_outbound" // #nosec G101 not a hardcoded credential + OpWeightVoteInbound = "op_weight_msg_vote_inbound" // #nosec G101 not a hardcoded credential + OpWeightWhitelistERC20 = "op_weight_msg_whitelist_erc20" // #nosec G101 not a hardcoded credential + OpWeightMigrateTssFunds = "op_weight_msg_migrate_tss_funds" // #nosec G101 not a hardcoded credential + OpWeightUpdateTssAddress = "op_weight_msg_update_tss_address" // #nosec G101 not a hardcoded credential + OpWeightAbortStuckCCTX = "op_weight_msg_abort_stuck_cctx" // #nosec G101 not a hardcoded credential + OpWeightUpdateRateLimiterFlags = "op_weight_msg_update_rate_limiter_flags" // #nosec G101 not a hardcoded credential + OpWeightRefundAbortedCCTX = "op_weight_msg_refund_aborted_cctx" // #nosec G101 not a hardcoded credential + OpWeightUpdateERC20CustodyPauseStatus = "op_weight_msg_update_erc20_custody_pause_status" // #nosec G101 not a hardcoded credential ) func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper) simulation.WeightedOperations { var ( - weightMsgAddOutboundTracker int - weightAddInboundTracker int - weightRemoveOutboundTracker int - weightVoteGasPrice int - weightVoteOutbound int - weightVoteInbound int - weightWhitelistERC20 int - weightMigrateTssFunds int - weightUpdateTssAddress int - weightAbortStuckCCTX int - weightUpdateRateLimiterFlags int + weightAddOutboundTracker int + weightAddInboundTracker int + weightRemoveOutboundTracker int + weightVoteGasPrice int + weightVoteOutbound int + weightVoteInbound int + weightWhitelistERC20 int + weightMigrateTssFunds int + weightUpdateTssAddress int + weightAbortStuckCCTX int + weightUpdateRateLimiterFlags int + weightRefundAbortedCCTX int + weightUpdateERC20CustodyPauseStatus int ) - appParams.GetOrGenerate(cdc, OpWeightMsgAddOutboundTracker, &weightMsgAddOutboundTracker, nil, + appParams.GetOrGenerate(cdc, OpWeightMsgAddOutboundTracker, &weightAddOutboundTracker, nil, func(_ *rand.Rand) { - weightMsgAddOutboundTracker = DefaultWeightMsgAddOutboundTracker + weightAddOutboundTracker = DefaultWeightAddOutboundTracker }, ) @@ -142,6 +142,18 @@ func WeightedOperations( }, ) + appParams.GetOrGenerate(cdc, OpWeightRefundAbortedCCTX, &weightRefundAbortedCCTX, nil, + func(_ *rand.Rand) { + weightRefundAbortedCCTX = DefaultWeightRefundAbortedCCTX + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightUpdateERC20CustodyPauseStatus, &weightUpdateERC20CustodyPauseStatus, nil, + func(_ *rand.Rand) { + weightUpdateERC20CustodyPauseStatus = DefaultWeightUpdateERC20CustodyPauseStatus + }, + ) + return simulation.WeightedOperations{ simulation.NewWeightedOperation( weightVoteGasPrice, @@ -151,239 +163,46 @@ func WeightedOperations( weightVoteInbound, SimulateVoteInbound(k), ), - } -} - -// operationSimulateVoteInbound generates a MsgVoteInbound with a random vote and delivers it. -func operationSimulateVoteInbound( - k keeper.Keeper, - msg types.MsgVoteInbound, - simAccount simtypes.Account, -) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - // Fetch the account from the auth keeper which can then be used to fetch spendable coins - authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) - - // Generate a transaction with a random fee and deliver it - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, - CoinsSpentInMsg: spendable, - } - - // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk - // The main difference between the two functions is that the one defined by us does not error out if the vote fails. - // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. - // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. - return GenAndDeliverTxWithRandFees(txCtx) - } -} - -func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { - // The states are: - // column 1: All observers vote - // column 2: 90% vote - // column 3: 75% vote - // column 4: 40% vote - // column 5: 15% vote - // column 6: noone votes - // All columns sum to 100 for simplicity, but this is arbitrary and can be changed - numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ - {20, 10, 0, 0, 0, 0}, - {55, 50, 20, 10, 0, 0}, - {25, 25, 30, 25, 30, 15}, - {0, 15, 30, 25, 30, 30}, - {0, 0, 20, 30, 30, 30}, - {0, 0, 0, 10, 10, 25}, - }) - - statePercentageArray := []float64{1, .9, .75, .4, .15, 0} - curNumVotesState := 1 - - return func( - r *rand.Rand, - app *baseapp.BaseApp, - ctx sdk.Context, - accs []simtypes.Account, - chainID string, - ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO : randomize these values - // Right now we use a constant value for cctx creation , this is the same as the one used in unit tests for the successful condition. - // TestKeeper_VoteInbound/successfully vote on evm deposit - // But this can improved by adding more randomization - - to, from := int64(1337), int64(101) - supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) - for _, chain := range supportedChains { - if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { - from = chain.ChainId - } - if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { - to = chain.ChainId - } - } - - msg := sample.InboundVoteFromRand(0, from, to, r) - - // Pick a random observer to create the ballot - // If this returns an error, it is likely that the entire observer set has been removed - simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) - if err != nil { - return simtypes.OperationMsg{}, nil, nil - } - - txGen := moduletestutil.MakeTestEncodingConfig().TxConfig - account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - firstMsg := msg - firstMsg.Creator = firstVoter - - err = firstMsg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first inbound vote"), nil, err - } - - tx, err := simtestutil.GenSignedMockTx( - r, - txGen, - []sdk.Msg{&firstMsg}, - sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, - simtestutil.DefaultGenTxGas, - chainID, - []uint64{account.GetAccountNumber()}, - []uint64{account.GetSequence()}, - simAccount.PrivKey, - ) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err - } - - // We can return error here as we can guarantee that the first vote will be successful. - // Since we query the observer set before adding votes - _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err - } - - opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) - - // Add subsequent votes - observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) - if !found { - return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "observer set not found"), nil, nil - } - - // 1) Schedule operations for votes - // 1.1) first pick a number of people to vote. - curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState) - numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) - - // 1.2) select who votes - whoVotes := r.Perm(len(observerSet.ObserverList)) - whoVotes = whoVotes[:numVotes] - - var fops []simtypes.FutureOperation - - for _, observerIdx := range whoVotes { - observerAddress := observerSet.ObserverList[observerIdx] - // firstVoter has already voted. - if observerAddress == firstVoter { - continue - } - observerAccount, err := GetObserverAccount(observerAddress, accs) - if err != nil { - continue - } - // 1.3) schedule the vote - votingMsg := msg - votingMsg.Creator = observerAddress - - e := votingMsg.ValidateBasic() - if e != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e - } - - fops = append(fops, simtypes.FutureOperation{ - // Submit all subsequent votes in the next block. - // We can consider adding a random block height between 1 and ballot maturity blocks in the future. - BlockHeight: int(ctx.BlockHeight() + 1), - Op: operationSimulateVoteInbound(k, votingMsg, observerAccount), - }) - } - return opMsg, fops, nil - } -} - -// SimulateMsgVoteGasPrice generates a MsgVoteGasPrice and delivers it -func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - // Get a random account and observer - // If this returns an error, it is likely that the entire observer set has been removed - simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) - if err != nil { - return simtypes.OperationMsg{}, nil, nil - } - authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) - - supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) - if len(supportedChains) == 0 { - return simtypes.NoOpMsg( - types.ModuleName, - authz.GasPriceVoter.String(), - "no supported chains found", - ), nil, nil - } - randomChainID := GetRandomChainID(r, supportedChains) - - // Vote for random gas price. Gas prices do not use a ballot system, so we can vote directly without having to schedule future operations. - // The random nature of the price might create weird gas prices for the chain, but it is fine for now. We can remove the randomness if needed - msg := types.MsgVoteGasPrice{ - Creator: randomObserver, - ChainId: randomChainID, - Price: r.Uint64(), - PriorityFee: r.Uint64(), - BlockNumber: r.Uint64(), - Supply: fmt.Sprintf("%d", r.Int63()), - } - - // System contracts are deployed on the first block, so we cannot vote on gas prices before that - if ctx.BlockHeight() <= 1 { - return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil - } - - err = msg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err - } - - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, - CoinsSpentInMsg: spendable, - } - - return simulation.GenAndDeliverTxWithRandFees(txCtx) + simulation.NewWeightedOperation( + weightVoteOutbound, + SimulateVoteOutbound(k), + ), + simulation.NewWeightedOperation( + weightAddInboundTracker, + SimulateMsgAddInboundTracker(k), + ), + simulation.NewWeightedOperation( + weightAddOutboundTracker, + SimulateMsgAddOutboundTracker(k), + ), + simulation.NewWeightedOperation( + weightRemoveOutboundTracker, + SimulateMsgRemoveOutboundTracker(k), + ), + simulation.NewWeightedOperation( + weightWhitelistERC20, + SimulateMsgWhitelistERC20(k), + ), + simulation.NewWeightedOperation( + weightAbortStuckCCTX, + SimulateMsgAbortStuckCCTX(k), + ), + simulation.NewWeightedOperation( + weightRefundAbortedCCTX, + SimulateMsgRefundAbortedCCTX(k), + ), + simulation.NewWeightedOperation( + weightUpdateRateLimiterFlags, + SimulateMsgUpdateRateLimiterFlags(k), + ), + simulation.NewWeightedOperation( + weightUpdateERC20CustodyPauseStatus, + SimulateUpdateERC20CustodyPauseStatus(k), + ), + simulation.NewWeightedOperation( + weightUpdateTssAddress, + SimulateMsgUpdateTssAddress(k), + ), } } @@ -413,7 +232,21 @@ func GetRandomAccountAndObserver( return simtypes.Account{}, "", fmt.Errorf("no observers present in observer set found") } - randomObserver := GetRandomObserver(r, observers.ObserverList) + randomObserver := "" + foundObserver := false + for i := 0; i < 10; i++ { + randomObserver = GetRandomObserver(r, observers.ObserverList) + ok := k.GetObserverKeeper().IsNonTombstonedObserver(ctx, randomObserver) + if ok { + foundObserver = true + break + } + } + + if !foundObserver { + return simtypes.Account{}, "", fmt.Errorf("no observer found") + } + simAccount, err := GetObserverAccount(randomObserver, accounts) if err != nil { return simtypes.Account{}, "", err @@ -490,3 +323,76 @@ func GenAndDeliverTx( return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil } + +func ObserverVotesSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + observerVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + // The states are: + // column 1: All observers vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, but this is arbitrary and can be changed + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + return observerVotesTransitionMatrix, statePercentageArray, curNumVotesState +} + +func BallotVoteSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + ballotTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {70, 10, 20}, + {20, 30, 30}, + {10, 60, 50}, + }) + // The states are: + // column 1: 100% vote yes + // column 2: 50% vote yes + // column 3: 0% vote yes + // For all conditions we assume if the the vote is not a yes. + // then it is a no .Not voting condtion is handled by the ObserverVotesSimulationMatrix matrix + yesVoteArray := []float64{1, .5, 0} + ballotVotesState := 1 + return ballotTransitionMatrix, yesVoteArray, ballotVotesState +} + +func GetPolicyAccount(ctx sdk.Context, k types.AuthorityKeeper, accounts []simtypes.Account) (simtypes.Account, error) { + policies, found := k.GetPolicies(ctx) + if !found { + return simtypes.Account{}, fmt.Errorf("policies object not found") + } + if len(policies.Items) == 0 { + return simtypes.Account{}, fmt.Errorf("no policies found") + } + + admin := policies.Items[0].Address + address, err := observerTypes.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.Account{}, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.Account{}, fmt.Errorf("admin account not found in list of simulation accounts") + } + return simAccount, nil +} + +func GetAsset(ctx sdk.Context, k types.FungibleKeeper, chainID int64) (string, error) { + foreignCoins := k.GetAllForeignCoins(ctx) + asset := "" + + for _, coin := range foreignCoins { + if coin.ForeignChainId == chainID { + return coin.Asset, nil + } + } + + return asset, fmt.Errorf("asset not found for chain %d", chainID) +} diff --git a/x/crosschain/types/expected_keepers.go b/x/crosschain/types/expected_keepers.go index 2fee9cb31a..a738242a34 100644 --- a/x/crosschain/types/expected_keepers.go +++ b/x/crosschain/types/expected_keepers.go @@ -13,6 +13,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/proofs" + authoritytypes "github.com/zeta-chain/node/x/authority/types" fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -224,6 +225,7 @@ type FungibleKeeper interface { type AuthorityKeeper interface { CheckAuthorization(ctx sdk.Context, msg sdk.Msg) error GetAdditionalChainList(ctx sdk.Context) (list []chains.Chain) + GetPolicies(ctx sdk.Context) (val authoritytypes.Policies, found bool) } type LightclientKeeper interface { diff --git a/x/crosschain/types/keys.go b/x/crosschain/types/keys.go index 76d343dd3e..26422c6101 100644 --- a/x/crosschain/types/keys.go +++ b/x/crosschain/types/keys.go @@ -26,7 +26,8 @@ const ( ProtocolFee = 2000000000000000000 // CCTXIndexLength is the length of a crosschain transaction index - CCTXIndexLength = 66 + CCTXIndexLength = 66 + MaxOutboundTrackerHashes = 5 ) func GetProtocolFee() math.Uint { diff --git a/x/crosschain/types/outbound_tracker.go b/x/crosschain/types/outbound_tracker.go new file mode 100644 index 0000000000..af986b388c --- /dev/null +++ b/x/crosschain/types/outbound_tracker.go @@ -0,0 +1,6 @@ +package types + +// MaxReached returns true if the OutboundTracker has reached the maximum number of hashes it can store. +func (o *OutboundTracker) MaxReached() bool { + return len(o.HashList) >= MaxOutboundTrackerHashes +} diff --git a/x/crosschain/types/outbound_tracker_test.go b/x/crosschain/types/outbound_tracker_test.go new file mode 100644 index 0000000000..1357a76ee2 --- /dev/null +++ b/x/crosschain/types/outbound_tracker_test.go @@ -0,0 +1,48 @@ +package types_test + +import ( + "testing" + + "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestOutboundTracker_IsMaxed(t *testing.T) { + tests := []struct { + name string + tracker types.OutboundTracker + want bool + }{ + {"Not maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + }}, + false}, + + {"Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + }}, + true}, + {"More than Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + {TxHash: "hash6", TxSigner: "signer6"}, + {TxHash: "hash7", TxSigner: "signer7"}, + }}, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.tracker.MaxReached(); got != tt.want { + t.Errorf("OutboundTracker.MaxReached() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/x/fungible/keeper/deposits.go b/x/fungible/keeper/deposits.go index b6a0c72c4e..3a5c24d1bb 100644 --- a/x/fungible/keeper/deposits.go +++ b/x/fungible/keeper/deposits.go @@ -99,6 +99,7 @@ func (k Keeper) getAndCheckZRC20( // this simplify the current workflow and allow to pause calls by pausing the gas token // TODO: refactor this logic and create specific workflow for no asset call // https://github.com/zeta-chain/node/issues/2627 + if coinType == coin.CoinType_Gas || coinType == coin.CoinType_NoAssetCall { foreignCoin, found = k.GetGasCoinForForeignCoin(ctx, chainID) if !found { diff --git a/x/fungible/simulation/operations.go b/x/fungible/simulation/operations.go index 2a14713979..c66d1bb3c9 100644 --- a/x/fungible/simulation/operations.go +++ b/x/fungible/simulation/operations.go @@ -16,6 +16,9 @@ import ( ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -24,9 +27,6 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( // #nosec G101 not a hardcoded credential OpWeightMsgDeploySystemContracts = "op_weight_msg_deploy_system_contracts" diff --git a/x/observer/genesis.go b/x/observer/genesis.go index e852e407bf..1910e8074d 100644 --- a/x/observer/genesis.go +++ b/x/observer/genesis.go @@ -11,13 +11,19 @@ import ( // InitGenesis initializes the observer module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - observerCount := uint64(0) if genState.Observers.Len() > 0 { k.SetObserverSet(ctx, genState.Observers) - observerCount = uint64(len(genState.Observers.ObserverList)) + } else { + k.SetObserverSet(ctx, types.ObserverSet{}) + } + + if genState.LastObserverCount != nil { + k.SetLastObserverCount(ctx, genState.LastObserverCount) + } else { + k.SetLastObserverCount(ctx, &types.LastObserverCount{LastChangeHeight: 0, Count: genState.Observers.LenUint()}) } - // if chain params are defined set them + // if chain params are defined, set them if len(genState.ChainParamsList.ChainParams) > 0 { k.SetChainParamsList(ctx, genState.ChainParamsList) } else { @@ -82,12 +88,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) }) } - if genState.LastObserverCount != nil { - k.SetLastObserverCount(ctx, genState.LastObserverCount) - } else { - k.SetLastObserverCount(ctx, &types.LastObserverCount{LastChangeHeight: 0, Count: observerCount}) - } - tss := types.TSS{} if genState.Tss != nil { tss = *genState.Tss diff --git a/x/observer/keeper/msg_server_update_observer.go b/x/observer/keeper/msg_server_update_observer.go index 2e60b291fb..7bf02e1b1c 100644 --- a/x/observer/keeper/msg_server_update_observer.go +++ b/x/observer/keeper/msg_server_update_observer.go @@ -36,6 +36,7 @@ func (k msgServer) UpdateObserver( "Observer address is not authorized : %s", msg.OldObserverAddress) } + // The New address should be a validator, not jailed and bonded err = k.IsValidator(ctx, msg.NewObserverAddress) if err != nil { return nil, errorsmod.Wrap(types.ErrUpdateObserver, err.Error()) @@ -72,7 +73,12 @@ func (k msgServer) UpdateObserver( return nil, errorsmod.Wrap(types.ErrLastObserverCountNotFound, "Observer count not found") } if lastBlockCount.Count != totalObserverCountCurrentBlock { - return nil, errorsmod.Wrap(types.ErrUpdateObserver, "Observer count mismatch") + return nil, errorsmod.Wrapf( + types.ErrUpdateObserver, + "Observer count mismatch current block: %d , last block: %d", + totalObserverCountCurrentBlock, + lastBlockCount.Count, + ) } return &types.MsgUpdateObserverResponse{}, nil } diff --git a/x/observer/keeper/msg_server_vote_tss.go b/x/observer/keeper/msg_server_vote_tss.go index 9b39174fd4..fc0156ffdd 100644 --- a/x/observer/keeper/msg_server_vote_tss.go +++ b/x/observer/keeper/msg_server_vote_tss.go @@ -27,8 +27,7 @@ const voteTSSid = "Vote TSS" func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types.MsgVoteTSSResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // Checks whether a signer is authorized to sign, by checking their address against the observer mapper - // which contains the observer list for the chain and type. + // Checks whether a signer is authorized to sign, by checking if the signer has a node account. _, found := k.GetNodeAccount(ctx, msg.Creator) if !found { return nil, errorsmod.Wrapf( @@ -104,7 +103,7 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types }, nil } - // For cases when an observer tries to vote for an older pending ballot, associated with a keygen that was discarded , we would return at this check while still adding the vote to the ballot + // For cases when an observer tries to vote for an older pending ballot, associated with a keygen that was discarded, we would return at this check while still adding the vote to the ballot if msg.KeygenZetaHeight != keygen.BlockNumber { return &types.MsgVoteTSSResponse{ VoteFinalized: isFinalized, diff --git a/x/observer/simulation/decoders.go b/x/observer/simulation/decoders.go index fd8eae7535..8d5243745b 100644 --- a/x/observer/simulation/decoders.go +++ b/x/observer/simulation/decoders.go @@ -19,79 +19,110 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { var crosschainFlagsA, crosschainFlagsB types.CrosschainFlags cdc.MustUnmarshal(kvA.Value, &crosschainFlagsA) cdc.MustUnmarshal(kvB.Value, &crosschainFlagsB) - return fmt.Sprintf("%v\n%v", crosschainFlagsA, crosschainFlagsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.CrosschainFlagsKey, + crosschainFlagsA, + crosschainFlagsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockObserverCountKey)): var lastBlockObserverCountA, lastBlockObserverCountB types.LastObserverCount cdc.MustUnmarshal(kvA.Value, &lastBlockObserverCountA) cdc.MustUnmarshal(kvB.Value, &lastBlockObserverCountB) - return fmt.Sprintf("%v\n%v", lastBlockObserverCountA, lastBlockObserverCountB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockObserverCountKey, + lastBlockObserverCountA, + lastBlockObserverCountB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.NodeAccountKey)): var nodeAccountA, nodeAccountB types.NodeAccount cdc.MustUnmarshal(kvA.Value, &nodeAccountA) cdc.MustUnmarshal(kvB.Value, &nodeAccountB) - return fmt.Sprintf("%v\n%v", nodeAccountA, nodeAccountB) + return fmt.Sprintf("key %s value A %v value B %v", types.NodeAccountKey, nodeAccountA, nodeAccountB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.KeygenKey)): var keygenA, keygenB types.Keygen cdc.MustUnmarshal(kvA.Value, &keygenA) cdc.MustUnmarshal(kvB.Value, &keygenB) - return fmt.Sprintf("%v\n%v", keygenA, keygenB) + return fmt.Sprintf("key %s value A %v value B %v", types.KeygenKey, keygenA, keygenB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.BallotListKey)): var ballotListA, ballotListB types.BallotListForHeight cdc.MustUnmarshal(kvA.Value, &ballotListA) cdc.MustUnmarshal(kvB.Value, &ballotListB) - return fmt.Sprintf("%v\n%v", ballotListA, ballotListB) + return fmt.Sprintf("key %s value A %v value B %v", types.BallotListKey, ballotListA, ballotListB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.VoterKey)): var voterA, voterB types.Ballot cdc.MustUnmarshal(kvA.Value, &voterA) cdc.MustUnmarshal(kvB.Value, &voterB) - return fmt.Sprintf("%v\n%v", voterA, voterB) + return fmt.Sprintf("key %s value A %v value B %v", types.VoterKey, voterA, voterB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSKey)): var tssA, tssB types.TSS cdc.MustUnmarshal(kvA.Value, &tssA) cdc.MustUnmarshal(kvB.Value, &tssB) - return fmt.Sprintf("%v\n%v", tssA, tssB) + return fmt.Sprintf("key %s value A %v value B %v", types.TSSKey, tssA, tssB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ObserverSetKey)): var observerSetA, observerSetB types.ObserverSet cdc.MustUnmarshal(kvA.Value, &observerSetA) cdc.MustUnmarshal(kvB.Value, &observerSetB) - return fmt.Sprintf("%v\n%v", observerSetA, observerSetB) + return fmt.Sprintf("key %s value A %v value B %v", types.ObserverSetKey, observerSetA, observerSetB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.AllChainParamsKey)): var allChainParamsA, allChainParamsB types.ChainParamsList cdc.MustUnmarshal(kvA.Value, &allChainParamsA) cdc.MustUnmarshal(kvB.Value, &allChainParamsB) - return fmt.Sprintf("%v\n%v", allChainParamsA, allChainParamsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.AllChainParamsKey, + allChainParamsA, + allChainParamsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSHistoryKey)): var tssHistoryA, tssHistoryB types.TSS cdc.MustUnmarshal(kvA.Value, &tssHistoryA) cdc.MustUnmarshal(kvB.Value, &tssHistoryB) - return fmt.Sprintf("%v\n%v", tssHistoryA, tssHistoryB) + return fmt.Sprintf("key %s value A %v value B %v", types.TSSHistoryKey, tssHistoryA, tssHistoryB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TssFundMigratorKey)): var tssFundMigratorA, tssFundMigratorB types.TssFundMigratorInfo cdc.MustUnmarshal(kvA.Value, &tssFundMigratorA) cdc.MustUnmarshal(kvB.Value, &tssFundMigratorB) - return fmt.Sprintf("%v\n%v", tssFundMigratorA, tssFundMigratorB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.TssFundMigratorKey, + tssFundMigratorA, + tssFundMigratorB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.PendingNoncesKeyPrefix)): var pendingNoncesA, pendingNoncesB types.PendingNonces cdc.MustUnmarshal(kvA.Value, &pendingNoncesA) cdc.MustUnmarshal(kvB.Value, &pendingNoncesB) - return fmt.Sprintf("%v\n%v", pendingNoncesA, pendingNoncesB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.PendingNoncesKeyPrefix, + pendingNoncesA, + pendingNoncesB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ChainNoncesKey)): var chainNoncesA, chainNoncesB types.ChainNonces cdc.MustUnmarshal(kvA.Value, &chainNoncesA) cdc.MustUnmarshal(kvB.Value, &chainNoncesB) - return fmt.Sprintf("%v\n%v", chainNoncesA, chainNoncesB) + return fmt.Sprintf("key %s value A %v value B %v", types.ChainNoncesKey, chainNoncesA, chainNoncesB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.NonceToCctxKeyPrefix)): var nonceToCctxA, nonceToCctxB types.NonceToCctx cdc.MustUnmarshal(kvA.Value, &nonceToCctxA) cdc.MustUnmarshal(kvB.Value, &nonceToCctxB) - return fmt.Sprintf("%v\n%v", nonceToCctxA, nonceToCctxB) + return fmt.Sprintf("key %s value A %v value B %v", types.NonceToCctxKeyPrefix, nonceToCctxA, nonceToCctxB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ParamsKey)): var paramsA, paramsB types.Params cdc.MustUnmarshal(kvA.Value, ¶msA) cdc.MustUnmarshal(kvB.Value, ¶msB) - return fmt.Sprintf("%v\n%v", paramsA, paramsB) + return fmt.Sprintf("key %s value A %v value B %v", types.ParamsKey, paramsA, paramsB) default: - panic(fmt.Sprintf("invalid observer key prefix %X", kvA.Key)) + panic( + fmt.Sprintf( + "invalid observer key prefix %X (first 8 bytes: %X)", + kvA.Key[:1], + kvA.Key[:min(8, len(kvA.Key))], + ), + ) } } } diff --git a/x/observer/simulation/decoders_test.go b/x/observer/simulation/decoders_test.go index 616994a664..d5a397d3ad 100644 --- a/x/observer/simulation/decoders_test.go +++ b/x/observer/simulation/decoders_test.go @@ -62,21 +62,44 @@ func TestNewDecodeStore(t *testing.T) { name string expectedLog string }{ - {"CrosschainFlags", fmt.Sprintf("%v\n%v", *crosschainFlags, *crosschainFlags)}, - {"LastBlockObserverCount", fmt.Sprintf("%v\n%v", *lastBlockObserverCount, *lastBlockObserverCount)}, - {"NodeAccount", fmt.Sprintf("%v\n%v", *nodeAccount, *nodeAccount)}, - {"Keygen", fmt.Sprintf("%v\n%v", *keygen, *keygen)}, - {"BallotList", fmt.Sprintf("%v\n%v", ballotList, ballotList)}, - {"Ballot", fmt.Sprintf("%v\n%v", *ballot, *ballot)}, - {"TSS", fmt.Sprintf("%v\n%v", tss, tss)}, - {"TSSHistory", fmt.Sprintf("%v\n%v", tss, tss)}, - {"ObserverSet", fmt.Sprintf("%v\n%v", observerSet, observerSet)}, - {"ChainParamsList", fmt.Sprintf("%v\n%v", chainParamsList, chainParamsList)}, - {"TssFundMigrator", fmt.Sprintf("%v\n%v", tssFundMigrator, tssFundMigrator)}, - {"PendingNonces", fmt.Sprintf("%v\n%v", pendingNonce, pendingNonce)}, - {"ChainNonces", fmt.Sprintf("%v\n%v", chainNonces, chainNonces)}, - {"NonceToCctx", fmt.Sprintf("%v\n%v", nonceToCctx, nonceToCctx)}, - {"Params", fmt.Sprintf("%v\n%v", params, params)}, + { + "CrosschainFlags", + fmt.Sprintf("key %s value A %v value B %v", types.CrosschainFlagsKey, *crosschainFlags, *crosschainFlags), + }, + { + "LastBlockObserverCount", + fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockObserverCountKey, + *lastBlockObserverCount, + *lastBlockObserverCount, + ), + }, + {"NodeAccount", fmt.Sprintf("key %s value A %v value B %v", types.NodeAccountKey, *nodeAccount, *nodeAccount)}, + {"Keygen", fmt.Sprintf("key %s value A %v value B %v", types.KeygenKey, *keygen, *keygen)}, + {"BallotList", fmt.Sprintf("key %s value A %v value B %v", types.BallotListKey, ballotList, ballotList)}, + {"Ballot", fmt.Sprintf("key %s value A %v value B %v", types.VoterKey, *ballot, *ballot)}, + {"TSS", fmt.Sprintf("key %s value A %v value B %v", types.TSSKey, tss, tss)}, + {"TSSHistory", fmt.Sprintf("key %s value A %v value B %v", types.TSSHistoryKey, tss, tss)}, + {"ObserverSet", fmt.Sprintf("key %s value A %v value B %v", types.ObserverSetKey, observerSet, observerSet)}, + { + "ChainParamsList", + fmt.Sprintf("key %s value A %v value B %v", types.AllChainParamsKey, chainParamsList, chainParamsList), + }, + { + "TssFundMigrator", + fmt.Sprintf("key %s value A %v value B %v", types.TssFundMigratorKey, tssFundMigrator, tssFundMigrator), + }, + { + "PendingNonces", + fmt.Sprintf("key %s value A %v value B %v", types.PendingNoncesKeyPrefix, pendingNonce, pendingNonce), + }, + {"ChainNonces", fmt.Sprintf("key %s value A %v value B %v", types.ChainNoncesKey, chainNonces, chainNonces)}, + { + "NonceToCctx", + fmt.Sprintf("key %s value A %v value B %v", types.NonceToCctxKeyPrefix, nonceToCctx, nonceToCctx), + }, + {"Params", fmt.Sprintf("key %s value A %v value B %v", types.ParamsKey, params, params)}, } for i, tt := range tests { diff --git a/x/observer/simulation/operation_add_observer.go b/x/observer/simulation/operation_add_observer.go new file mode 100644 index 0000000000..95eff5ddbb --- /dev/null +++ b/x/observer/simulation/operation_add_observer.go @@ -0,0 +1,95 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateAddObserver generates a TypeMsgAddObserver and delivers it. This message sets AddNodeAccountOnly to false; +// Therefore, it adds the observer to the observer set +func SimulateAddObserver(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + observerSet, found := k.GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no observer set found", + ), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerSet.ObserverList { + observerMap[observer] = true + } + + nodeAccounts := k.GetAllNodeAccount(ctx) + + // Pick a random observer which part of the node account but not in the observer set + // New accounts are added to the node account list via SimulateAddObserverNodeAccount + var newObserver string + foundNA := RepeatCheck(func() bool { + newObserver = nodeAccounts[r.Intn(len(nodeAccounts))].Operator + if _, found := observerMap[newObserver]; !found { + return true + } + return false + }) + if !foundNA { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no node accounts available which can be added as observer", + ), nil, nil + } + pubkey, err := sample.PubkeyStringFromRand(r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + msg := types.MsgAddObserver{ + Creator: policyAccount.Address.String(), + ObserverAddress: newObserver, + ZetaclientGranteePubkey: pubkey, + AddNodeAccountOnly: false, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_add_observer_node_account.go b/x/observer/simulation/operation_add_observer_node_account.go new file mode 100644 index 0000000000..e182123fe1 --- /dev/null +++ b/x/observer/simulation/operation_add_observer_node_account.go @@ -0,0 +1,110 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateAddObserverNodeAccount generates a TypeMsgAddObserver and delivers it. +// This message sets AddNodeAccountOnly to true to it does not add the observer to the observer set +func SimulateAddObserverNodeAccount(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + observerSet, found := k.GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no observer set found", + ), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerSet.ObserverList { + observerMap[observer] = true + } + + validators := k.GetStakingKeeper().GetAllValidators(ctx) + if len(validators) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no validators found", + ), nil, nil + } + newObserver := "" + foundNewObserver := RepeatCheck(func() bool { + randomValidator := validators[r.Intn(len(validators))] + randomValidatorAddress, err := types.GetAccAddressFromOperatorAddress(randomValidator.OperatorAddress) + if err != nil { + return false + } + newObserver = randomValidatorAddress.String() + err = k.IsValidator(ctx, newObserver) + if err != nil { + return false + } + if _, ok := observerMap[newObserver]; !ok { + return true + } + return false + }) + + if !foundNewObserver { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no new observer found", + ), nil, nil + } + + pubkey, err := sample.PubkeyStringFromRand(r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + msg := types.MsgAddObserver{ + Creator: policyAccount.Address.String(), + ObserverAddress: newObserver, + ZetaclientGranteePubkey: pubkey, + AddNodeAccountOnly: true, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_disable_cctx.go b/x/observer/simulation/operation_disable_cctx.go new file mode 100644 index 0000000000..9364aa2d87 --- /dev/null +++ b/x/observer/simulation/operation_disable_cctx.go @@ -0,0 +1,55 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateDisableCCTX generates a MsgDisableCCTX and delivers it. +func SimulateDisableCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDisableCCTX, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgDisableCCTX{ + Creator: policyAccount.Address.String(), + DisableInbound: false, + DisableOutbound: true, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_enable_cctx.go b/x/observer/simulation/operation_enable_cctx.go new file mode 100644 index 0000000000..7ee1d06776 --- /dev/null +++ b/x/observer/simulation/operation_enable_cctx.go @@ -0,0 +1,55 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateEnableCCTX generates a MsgEnableCCTX and delivers it. +func SimulateEnableCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgEnableCCTX{ + Creator: policyAccount.Address.String(), + EnableInbound: true, + EnableOutbound: true, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_remove_chain_params.go b/x/observer/simulation/operation_remove_chain_params.go new file mode 100644 index 0000000000..a0e9f71681 --- /dev/null +++ b/x/observer/simulation/operation_remove_chain_params.go @@ -0,0 +1,83 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateMsgRemoveChainParams generates a MsgRemoveChainParams and delivers it. This message removes a chain from the list +// This is not being run right now as the removal causes a lot of errors for the other operations. +func SimulateMsgRemoveChainParams(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgRemoveChainParams, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveChainParams, + "no supported chains found", + ), nil, nil + } + + randomExternalChain := int64(0) + foundExternalChain := RepeatCheck(func() bool { + c := supportedChains[r.Intn(len(supportedChains))] + if !c.IsZetaChain() { + randomExternalChain = c.ChainId + return true + } + return false + }) + + if !foundExternalChain { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveChainParams, + "no external chain found", + ), nil, nil + } + + msg := types.MsgRemoveChainParams{ + Creator: policyAccount.Address.String(), + ChainId: randomExternalChain, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_reset_chain_nonces.go b/x/observer/simulation/operation_reset_chain_nonces.go new file mode 100644 index 0000000000..38ccacac37 --- /dev/null +++ b/x/observer/simulation/operation_reset_chain_nonces.go @@ -0,0 +1,87 @@ +package simulation + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateResetChainNonces generates a MsgResetChainNonces and delivers it. +func SimulateResetChainNonces(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgResetChainNonces, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + randomChain, err := GetExternalChain(ctx, k, r) + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgResetChainNonces, + err.Error(), + ), nil, fmt.Errorf( + "error getting external chain", + ) + } + + tss, found := k.GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgResetChainNonces, + "TSS not found", + ), nil, fmt.Errorf( + "TSS not found", + ) + } + pendingNonces, found := k.GetPendingNonces(ctx, tss.TssPubkey, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgResetChainNonces, "Pending nonces not found"), nil, + fmt.Errorf("pending nonces not found for chain %d %s", randomChain.ChainId, randomChain.ChainName) + } + + nonceIncrement := int64(r.Intn(99)) + 1 + + msg := types.MsgResetChainNonces{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + ChainNonceHigh: pendingNonces.NonceHigh + nonceIncrement, + ChainNonceLow: pendingNonces.NonceLow + nonceIncrement, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_chain_params.go b/x/observer/simulation/operation_update_chain_params.go new file mode 100644 index 0000000000..21f45d9ee3 --- /dev/null +++ b/x/observer/simulation/operation_update_chain_params.go @@ -0,0 +1,67 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateChainParams generates a MsgUpdateChainParams and delivers it. +func SimulateUpdateChainParams(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + randomChain, err := GetExternalChain(ctx, k, r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + cp := sample.ChainParamsFromRand(r, randomChain.ChainId) + err = types.ValidateChainParams(cp) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + msg := types.MsgUpdateChainParams{ + Creator: policyAccount.Address.String(), + ChainParams: cp, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_gas_price_increase_flags.go b/x/observer/simulation/operation_update_gas_price_increase_flags.go new file mode 100644 index 0000000000..fdb59d789d --- /dev/null +++ b/x/observer/simulation/operation_update_gas_price_increase_flags.go @@ -0,0 +1,58 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateGasPriceIncreaseFlags generates a MsgUpdateGasPriceIncreaseFlags with random values +func SimulateUpdateGasPriceIncreaseFlags(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateGasPriceIncreaseFlags, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + gp := sample.GasPriceIncreaseFlagsFromRand(r) + + msg := types.MsgUpdateGasPriceIncreaseFlags{ + Creator: policyAccount.Address.String(), + GasPriceIncreaseFlags: gp, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_keygen.go b/x/observer/simulation/operation_update_keygen.go new file mode 100644 index 0000000000..c6718b0dcc --- /dev/null +++ b/x/observer/simulation/operation_update_keygen.go @@ -0,0 +1,66 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateKeygen generates a MsgUpdateKeygen and delivers it. +func SimulateUpdateKeygen(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateKeygen, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + _, found := k.GetKeygen(ctx) + if !found { + kg := sample.KeygenFromRand(r) + k.SetKeygen(ctx, kg) + } + + blockHeightMin := ctx.BlockHeight() + 11 + blockHeightMax := ctx.BlockHeight() + 1000 + keygenBlockHeight := int64(r.Intn(int(blockHeightMax-blockHeightMin))) + blockHeightMin + + msg := types.MsgUpdateKeygen{ + Creator: policyAccount.Address.String(), + Block: keygenBlockHeight, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_observer.go b/x/observer/simulation/operation_update_observer.go new file mode 100644 index 0000000000..bc2f729942 --- /dev/null +++ b/x/observer/simulation/operation_update_observer.go @@ -0,0 +1,118 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateObserver generates a TypeMsgUpdateObserver and delivers it. +func SimulateUpdateObserver(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + _, randomObserver, observerList, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateObserver, err.Error()), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerList { + observerMap[observer] = true + } + + validators := k.GetStakingKeeper().GetAllValidators(ctx) + if len(validators) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no validators found", + ), nil, nil + } + + newObserver := "" + foundNewObserver := RepeatCheck(func() bool { + randomValidator := validators[r.Intn(len(validators))] + randomValidatorAddress, err := types.GetAccAddressFromOperatorAddress(randomValidator.OperatorAddress) + if err != nil { + return false + } + newObserver = randomValidatorAddress.String() + err = k.IsValidator(ctx, newObserver) + if err != nil { + return false + } + if _, ok := observerMap[newObserver]; !ok { + return true + } + return false + }) + + if !foundNewObserver { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no new observer found", + ), nil, nil + } + + lastBlockCount, found := k.GetLastObserverCount(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no last block count found", + ), nil, nil + } + // #nosec G115 - overflow is not a concern here + if int(lastBlockCount.Count) != len(observerList) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "observer count mismatch", + ), nil, nil + } + + msg := types.MsgUpdateObserver{ + Creator: policyAccount.Address.String(), + OldObserverAddress: randomObserver, + NewObserverAddress: newObserver, + UpdateReason: types.ObserverUpdateReason_AdminUpdate, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_vote_tss.go b/x/observer/simulation/operation_vote_tss.go new file mode 100644 index 0000000000..8dc3c5668d --- /dev/null +++ b/x/observer/simulation/operation_vote_tss.go @@ -0,0 +1,177 @@ +package simulation + +import ( + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// operationSimulateVoteTss generates a MsgVoteTSS with random values +func operationSimulateVoteTss( + k keeper.Keeper, + msg types.MsgVoteTSS, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateMsgVoteTSS generates a MsgVoteTSS with random values and delivers it, it also schedules future votes for the same ballot +func SimulateMsgVoteTSS(k keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + yesVote := chains.ReceiveStatus_success + noVote := chains.ReceiveStatus_failed + ballotVotesTransitionMatrix, yesVotePercentageArray, ballotVotesState := BallotVoteSimulationMatrix() + nodeAccounts := k.GetAllNodeAccount(ctx) + numVotes := len(nodeAccounts) + ballotVotesState = ballotVotesTransitionMatrix.NextState(r, ballotVotesState) + yesVotePercentage := yesVotePercentageArray[ballotVotesState] + numberOfYesVotes := int(math.Ceil(float64(numVotes) * yesVotePercentage)) + + vote := yesVote + if numberOfYesVotes == 0 { + vote = noVote + } + + newTss, err := sample.TSSFromRand(r) + if err != nil { + return simtypes.OperationMsg{}, nil, err + } + + keygen, found := k.GetKeygen(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVoteTSS, "keygen not found"), nil, nil + } + + msg := types.MsgVoteTSS{ + Creator: "", + TssPubkey: newTss.TssPubkey, + KeygenZetaHeight: keygen.BlockNumber, + Status: vote, + } + + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomNodeAccount(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first tss vote"), nil, err + } + + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + var fops []simtypes.FutureOperation + + for voteCount, nodeAccount := range nodeAccounts { + if vote == yesVote && voteCount == numberOfYesVotes { + vote = noVote + } + // firstVoter has already voted. + if nodeAccount.Operator == firstVoter { + continue + } + observerAccount, err := GetSimAccount(nodeAccount.Operator, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = nodeAccount.Operator + votingMsg.Status = vote + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteTss(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/observer/simulation/operations.go b/x/observer/simulation/operations.go index 7c12ced3c0..1132e8a87e 100644 --- a/x/observer/simulation/operations.go +++ b/x/observer/simulation/operations.go @@ -1,20 +1,24 @@ package simulation import ( + "fmt" "math/rand" - "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/x/observer/keeper" "github.com/zeta-chain/node/x/observer/types" ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -23,85 +27,389 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( - // #nosec G101 not a hardcoded credential - OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" - // DefaultWeightMsgTypeMsgEnableCCTX We ues a high weight for this operation - // to ensure that it is present in the block more number of times than any operation that changes the validator set + OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgDisableCCTX = "op_weight_msg_disable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgVoteTSS = "op_weight_msg_vote_tss" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateKeygen = "op_weight_msg_update_keygen" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateObserver = "op_weight_msg_update_observer" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateChainParams = "op_weight_msg_update_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgRemoveChainParams = "op_weight_msg_remove_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgResetChainNonces = "op_weight_msg_reset_chain_nonces" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = "op_weight_msg_update_gas_price_increase_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgAddObserver = "op_weight_msg_add_observer" // #nosec G101 not a hardcoded credential + // DefaultWeightMsgTypeMsgEnableCCTX We use a high weight for this operation + // to ensure that it is present in the block more number of times than any operation that changes the validator set // Arrived at this number based on the weights used in the cosmos sdk staking module and through some trial and error - DefaultWeightMsgTypeMsgEnableCCTX = 3650 + DefaultWeightMsgTypeMsgEnableCCTX = 100 + DefaultWeightMsgTypeMsgDisableCCTX = 10 + DefaultWeightMsgTypeMsgVoteTSS = 10 + DefaultWeightMsgTypeMsgUpdateKeygen = 10 + DefaultWeightMsgTypeMsgUpdateObserver = 10 + DefaultWeightMsgTypeMsgUpdateChainParams = 10 + DefaultWeightMsgTypeMsgRemoveChainParams = 10 + DefaultWeightMsgTypeMsgResetChainNonces = 5 + DefaultWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = 10 + DefaultWeightMsgTypeMsgAddObserver = 5 + + DefaultRetryCount = 10 ) // WeightedOperations for observer module func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper, ) simulation.WeightedOperations { - var weightMsgTypeMsgEnableCCTX int + var ( + weightMsgTypeMsgEnableCCTX int + weightMsgTypeMsgDisableCCTX int + weightMsgTypeMsgVoteTSS int + weightMsgTypeMsgUpdateKeygen int + weightMsgTypeMsgUpdateObserver int + weightMsgTypeMsgUpdateChainParams int + weightMsgTypeMsgRemoveChainParams int + weightMsgTypeMsgResetChainNonces int + weightMsgTypeMsgUpdateGasPriceIncreaseFlags int + weightMsgTypeMsgAddObserver int + ) appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgEnableCCTX, &weightMsgTypeMsgEnableCCTX, nil, func(_ *rand.Rand) { weightMsgTypeMsgEnableCCTX = DefaultWeightMsgTypeMsgEnableCCTX }) + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgDisableCCTX, &weightMsgTypeMsgDisableCCTX, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgDisableCCTX = DefaultWeightMsgTypeMsgDisableCCTX + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgVoteTSS, &weightMsgTypeMsgVoteTSS, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgVoteTSS = DefaultWeightMsgTypeMsgVoteTSS + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateKeygen, &weightMsgTypeMsgUpdateKeygen, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateKeygen = DefaultWeightMsgTypeMsgUpdateKeygen + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateObserver, &weightMsgTypeMsgUpdateObserver, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateObserver = DefaultWeightMsgTypeMsgUpdateObserver + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateChainParams, &weightMsgTypeMsgUpdateChainParams, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateChainParams = DefaultWeightMsgTypeMsgUpdateChainParams + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgRemoveChainParams, &weightMsgTypeMsgRemoveChainParams, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgRemoveChainParams = DefaultWeightMsgTypeMsgRemoveChainParams + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgResetChainNonces, &weightMsgTypeMsgResetChainNonces, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgResetChainNonces = DefaultWeightMsgTypeMsgResetChainNonces + }) + + appParams.GetOrGenerate( + cdc, + OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags, + &weightMsgTypeMsgUpdateGasPriceIncreaseFlags, + nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateGasPriceIncreaseFlags = DefaultWeightMsgTypeMsgUpdateGasPriceIncreaseFlags + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgAddObserver, &weightMsgTypeMsgAddObserver, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgAddObserver = DefaultWeightMsgTypeMsgAddObserver + }) + return simulation.WeightedOperations{ simulation.NewWeightedOperation( weightMsgTypeMsgEnableCCTX, - SimulateMsgTypeMsgEnableCCTX(k), + SimulateEnableCCTX(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgDisableCCTX, + SimulateDisableCCTX(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateKeygen, + SimulateUpdateKeygen(k), + ), + // + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateChainParams, + SimulateUpdateChainParams(k), + ), + // + //simulation.NewWeightedOperation( + // weightMsgTypeMsgRemoveChainParams, + // SimulateMsgRemoveChainParams(k), + //), + + simulation.NewWeightedOperation( + weightMsgTypeMsgResetChainNonces, + SimulateResetChainNonces(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateGasPriceIncreaseFlags, + SimulateUpdateGasPriceIncreaseFlags(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateUpdateObserver(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateAddObserverNodeAccount(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateAddObserver(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgVoteTSS, + SimulateMsgVoteTSS(k), ), } } -// SimulateMsgTypeMsgEnableCCTX generates a MsgEnableCCTX and delivers it. -func SimulateMsgTypeMsgEnableCCTX(k keeper.Keeper) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - policies, found := k.GetAuthorityKeeper().GetPolicies(ctx) - if !found { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "policies object not found"), nil, nil - } - if len(policies.Items) == 0 { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "no policies found"), nil, nil +func GetPolicyAccount(ctx sdk.Context, k types.AuthorityKeeper, accounts []simtypes.Account) (simtypes.Account, error) { + policies, found := k.GetPolicies(ctx) + if !found { + return simtypes.Account{}, fmt.Errorf("policies object not found") + } + if len(policies.Items) == 0 { + return simtypes.Account{}, fmt.Errorf("no policies found") + } + + admin := policies.Items[0].Address + address, err := types.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.Account{}, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.Account{}, fmt.Errorf("admin account not found in list of simulation accounts") + } + return simAccount, nil +} + +func GetExternalChain(ctx sdk.Context, k keeper.Keeper, r *rand.Rand) (chains.Chain, error) { + supportedChains := k.GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return chains.Chain{}, fmt.Errorf("no supported chains found") + } + externalChain := chains.Chain{} + foundExternalChain := RepeatCheck(func() bool { + c := supportedChains[r.Intn(len(supportedChains))] + if !c.IsZetaChain() { + externalChain = c + return true } + return false + }) - admin := policies.Items[0].Address - address, err := types.GetOperatorAddressFromAccAddress(admin) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, err.Error()), nil, err + if !foundExternalChain { + return chains.Chain{}, fmt.Errorf("no external chain found") + } + return externalChain, nil +} + +// GetRandomAccountAndObserver returns a random account and the associated observer address +func GetRandomAccountAndObserver( + r *rand.Rand, + ctx sdk.Context, + k keeper.Keeper, + accounts []simtypes.Account, +) (simtypes.Account, string, []string, error) { + observerList := []string{} + observers, found := k.GetObserverSet(ctx) + if !found { + return simtypes.Account{}, "", observerList, fmt.Errorf("observer set not found") + } + + observerList = observers.ObserverList + + if len(observers.ObserverList) == 0 { + return simtypes.Account{}, "", observerList, fmt.Errorf("no observers present in observer set found") + } + + randomObserver := "" + foundObserver := RepeatCheck(func() bool { + randomObserver = GetRandomObserver(r, observerList) + _, foundNodeAccount := k.GetNodeAccount(ctx, randomObserver) + if !foundNodeAccount { + return false } - simAccount, found := simtypes.FindAccount(accounts, address) - if !found { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "admin account not found"), nil, nil + ok := k.IsNonTombstonedObserver(ctx, randomObserver) + if ok { + return true } + return false + }) - msg := types.MsgEnableCCTX{ - Creator: simAccount.Address.String(), - EnableInbound: true, - EnableOutbound: false, - } + if !foundObserver { + return simtypes.Account{}, "", nil, fmt.Errorf("no observer found") + } - err = msg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err - } + simAccount, err := GetSimAccount(randomObserver, accounts) + if err != nil { + return simtypes.Account{}, "", observerList, err + } + return simAccount, randomObserver, observerList, nil +} + +func GetRandomNodeAccount( + r *rand.Rand, + ctx sdk.Context, + k keeper.Keeper, + accounts []simtypes.Account, +) (simtypes.Account, string, error) { + nodeAccounts := k.GetAllNodeAccount(ctx) + + if len(nodeAccounts) == 0 { + return simtypes.Account{}, "", fmt.Errorf("no node accounts present") + } + + randomNodeAccount := nodeAccounts[r.Intn(len(nodeAccounts))].Operator - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, + simAccount, err := GetSimAccount(randomNodeAccount, accounts) + if err != nil { + return simtypes.Account{}, "", err + } + return simAccount, randomNodeAccount, nil +} + +func GetRandomObserver(r *rand.Rand, observerList []string) string { + idx := r.Intn(len(observerList)) + return observerList[idx] +} + +// GetSimAccount returns the account associated with the observer address from the list of accounts provided +// GetSimAccount can fail if all the observers are removed from the observer set ,this can happen +//if the other modules create transactions which affect the validator +//and triggers any of the staking hooks defined in the observer modules + +func GetSimAccount(observerAddress string, accounts []simtypes.Account) (simtypes.Account, error) { + operatorAddress, err := types.GetOperatorAddressFromAccAddress(observerAddress) + if err != nil { + return simtypes.Account{}, fmt.Errorf("validator not found for observer ") + } + + simAccount, found := simtypes.FindAccount(accounts, operatorAddress) + if !found { + return simtypes.Account{}, fmt.Errorf("operator account not found") + } + return simAccount, nil +} + +func RepeatCheck(fn func() bool) bool { + for i := 0; i < DefaultRetryCount; i++ { + if fn() { + return true } + } + return false +} - return simulation.GenAndDeliverTxWithRandFees(txCtx) +func ObserverVotesSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + observerVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + // The states are: + // column 1: All observers vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, but this is arbitrary and can be changed + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + return observerVotesTransitionMatrix, statePercentageArray, curNumVotesState +} + +func BallotVoteSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + ballotTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {70, 10}, + {30, 10}, + }) + // The states are: + // column 1: 100% vote yes + // column 2: 0% vote yes + // For all conditions we assume if the vote is not a yes + // then it is a no . + yesVoteArray := []float64{1, 0} + ballotVotesState := 1 + return ballotTransitionMatrix, yesVoteArray, ballotVotesState +} + +// GenAndDeliverTxWithRandFees generates a transaction with a random fee and delivers it. +func GenAndDeliverTxWithRandFees( + txCtx simulation.OperationInput, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + spendable := txCtx.Bankkeeper.SpendableCoins(txCtx.Context, account.GetAddress()) + + var fees sdk.Coins + var err error + + coins, hasNeg := spendable.SafeSub(txCtx.CoinsSpentInMsg...) + if hasNeg { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "message doesn't leave room for fees"), nil, err + } + + fees, err = simtypes.RandomFees(txCtx.R, txCtx.Context, coins) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate fees"), nil, err } + return GenAndDeliverTx(txCtx, fees) +} + +// GenAndDeliverTx generates a transactions and delivers it with the provided fees. +// This function does not return an error if the transaction fails to deliver. +func GenAndDeliverTx( + txCtx simulation.OperationInput, + fees sdk.Coins, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + tx, err := simtestutil.GenSignedMockTx( + txCtx.R, + txCtx.TxGen, + []sdk.Msg{txCtx.Msg}, + fees, + simtestutil.DefaultGenTxGas, + txCtx.Context.ChainID(), + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + txCtx.SimAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate mock tx"), nil, err + } + + _, _, err = txCtx.App.SimDeliver(txCtx.TxGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to deliver tx"), nil, nil + } + + return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil } diff --git a/x/observer/types/expected_keepers.go b/x/observer/types/expected_keepers.go index 2788187c94..15eb476af6 100644 --- a/x/observer/types/expected_keepers.go +++ b/x/observer/types/expected_keepers.go @@ -19,6 +19,7 @@ type StakingKeeper interface { valAddr sdk.ValAddress, ) (delegation stakingtypes.Delegation, found bool) SetValidator(ctx sdk.Context, validator stakingtypes.Validator) + GetAllValidators(ctx sdk.Context) (validators []stakingtypes.Validator) } type SlashingKeeper interface { diff --git a/x/observer/types/keys.go b/x/observer/types/keys.go index ed57b13d79..e2bed33dec 100644 --- a/x/observer/types/keys.go +++ b/x/observer/types/keys.go @@ -71,6 +71,10 @@ const ( NodeAccountKey = "NodeAccount-value-" KeygenKey = "Keygen-value-" + // TODO remove unused keys + BlockHeaderKey = "BlockHeader-value-" + BlockHeaderStateKey = "BlockHeaderState-value-" + // TODO rename to BallotListForHeightKey BallotListKey = "BallotList-value-" TSSKey = "TSS-value-" diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 2998f094dc..f8808d495d 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -20,7 +20,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" - crosschainkeeper "github.com/zeta-chain/node/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/evm" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -126,7 +125,7 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { // should not happen. We can't tell which txHash is true. It might happen (e.g. bug, glitchy/hacked endpoint) ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, chainID, nonce) } else { - if len(tracker.HashList) == crosschainkeeper.MaxOutboundTrackerHashes { + if tracker.MaxReached() { ob.Logger().Outbound.Error().Msgf("WatchOutbound: outbound tracker is full of hashes for chain %d nonce %d", chainID, nonce) } } From 8ce300b3623e0f710077f4b204370330e9b82a9a Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:52:45 +0100 Subject: [PATCH 10/10] feat(zetaclient)!: orchestrator V2 (#3332) * Minor fixes * Add orchestrator V2. Move context updater to v2 * Fix orchestrator_v2 test cases * Fix flaky test cases during concurrent runs (spoiler: goroutines) * Add V2 to start.go * chain sync skeleton * Move common btc stuff to common/ to fix import cycle * Implement BTC observerSigner * Drop redundant code * Fix ticker concurrency bug * Add scheduler.Tasks() * Add v2 btc observer-signer 101 test cases. Drop redundant tests * Address PR comments * Add issue * fix inbound debug cmd * Add tss graceful shutdown * Update changelog * fix tss tests * Fix IntervalUpdater * Mitigate errors when BTC node is disabled * Implement pkg/fanout * Apply fanout to block subscriber * Fix typo * Minor btc signer improvements * Make V1.Stop() safe to call multiple times * FIX DATA RACE --- changelog.md | 1 + cmd/zetaclientd/inbound.go | 36 +- cmd/zetaclientd/start.go | 21 ++ .../test_bitcoin_deposit_and_call_revert.go | 2 +- ...tcoin_deposit_and_call_revert_with_dust.go | 2 +- e2e/e2etests/test_bitcoin_deposit_call.go | 6 +- e2e/e2etests/test_bitcoin_donation.go | 2 +- e2e/e2etests/test_bitcoin_std_deposit.go | 4 +- .../test_bitcoin_std_deposit_and_call.go | 2 +- ...oin_std_memo_inscribed_deposit_and_call.go | 4 +- e2e/runner/bitcoin.go | 12 +- go.mod | 6 +- pkg/chains/chain.go | 9 + pkg/fanout/fanout.go | 66 ++++ pkg/fanout/fanout_test.go | 72 ++++ pkg/scheduler/opts.go | 11 +- pkg/scheduler/scheduler.go | 34 ++ pkg/scheduler/scheduler_test.go | 9 +- pkg/scheduler/tickers.go | 2 +- pkg/ticker/ticker.go | 15 +- zetaclient/chains/base/observer.go | 6 + zetaclient/chains/bitcoin/bitcoin.go | 229 +++++++++++++ zetaclient/chains/bitcoin/{ => common}/fee.go | 2 +- .../chains/bitcoin/{ => common}/fee_test.go | 5 +- .../chains/bitcoin/{ => common}/tx_script.go | 2 +- .../bitcoin/{ => common}/tx_script_test.go | 105 +++--- .../chains/bitcoin/{ => common}/utils.go | 16 +- zetaclient/chains/bitcoin/errors.go | 6 - zetaclient/chains/bitcoin/observer/inbound.go | 112 +------ .../chains/bitcoin/observer/inbound_test.go | 32 +- .../chains/bitcoin/observer/observer.go | 142 ++------ .../chains/bitcoin/observer/outbound.go | 131 +++----- .../chains/bitcoin/observer/rpc_status.go | 42 +-- zetaclient/chains/bitcoin/observer/witness.go | 10 +- .../chains/bitcoin/observer/witness_test.go | 4 +- zetaclient/chains/bitcoin/rpc/rpc.go | 17 +- .../chains/bitcoin/rpc/rpc_live_test.go | 14 +- zetaclient/chains/bitcoin/signer/signer.go | 115 ++----- zetaclient/context/chain.go | 6 + zetaclient/context/context.go | 2 +- .../maintenance/shutdown_listener_test.go | 2 +- zetaclient/orchestrator/bootstrap.go | 52 +-- .../{bootstap_test.go => bootstrap_test.go} | 125 ++----- zetaclient/orchestrator/contextupdater.go | 43 --- zetaclient/orchestrator/orchestrator.go | 119 ++----- zetaclient/orchestrator/v2_bootstrap.go | 73 +++++ zetaclient/orchestrator/v2_bootstrap_test.go | 78 +++++ zetaclient/orchestrator/v2_orchestrator.go | 307 ++++++++++++++++++ .../orchestrator/v2_orchestrator_test.go | 267 +++++++++++++++ zetaclient/testutils/mocks/chain_params.go | 2 +- zetaclient/testutils/testlog/log.go | 50 +++ zetaclient/tss/service.go | 7 + zetaclient/tss/service_test.go | 2 + zetaclient/zetacore/client.go | 5 + zetaclient/zetacore/client_subscriptions.go | 92 +++++- 55 files changed, 1672 insertions(+), 866 deletions(-) create mode 100644 pkg/fanout/fanout.go create mode 100644 pkg/fanout/fanout_test.go create mode 100644 zetaclient/chains/bitcoin/bitcoin.go rename zetaclient/chains/bitcoin/{ => common}/fee.go (99%) rename zetaclient/chains/bitcoin/{ => common}/fee_test.go (99%) rename zetaclient/chains/bitcoin/{ => common}/tx_script.go (99%) rename zetaclient/chains/bitcoin/{ => common}/tx_script_test.go (88%) rename zetaclient/chains/bitcoin/{ => common}/utils.go (77%) delete mode 100644 zetaclient/chains/bitcoin/errors.go rename zetaclient/orchestrator/{bootstap_test.go => bootstrap_test.go} (78%) create mode 100644 zetaclient/orchestrator/v2_bootstrap.go create mode 100644 zetaclient/orchestrator/v2_bootstrap_test.go create mode 100644 zetaclient/orchestrator/v2_orchestrator.go create mode 100644 zetaclient/orchestrator/v2_orchestrator_test.go create mode 100644 zetaclient/testutils/testlog/log.go diff --git a/changelog.md b/changelog.md index 44c0a2765c..1128fda716 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ * [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient * [3291](https://github.com/zeta-chain/node/pull/3291) - revamp zetaclient initialization (+ graceful shutdown) * [3319](https://github.com/zeta-chain/node/pull/3319) - implement scheduler for zetaclient +* [3332](https://github.com/zeta-chain/node/pull/3332) - implement orchestrator V2. Move BTC observer-signer to V2 ### Fixes diff --git a/cmd/zetaclientd/inbound.go b/cmd/zetaclientd/inbound.go index ee602357fa..ee1ac98a05 100644 --- a/cmd/zetaclientd/inbound.go +++ b/cmd/zetaclientd/inbound.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -16,6 +16,7 @@ import ( "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/chains/base" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + btcrpc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" evmobserver "github.com/zeta-chain/node/zetaclient/chains/evm/observer" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" @@ -156,17 +157,36 @@ func InboundGetBallot(_ *cobra.Command, args []string) error { } fmt.Println("CoinType : ", coinType) } else if chain.IsBitcoin() { - observer, ok := observers[chainID] - if !ok { - return fmt.Errorf("observer not found for btc chain %d", chainID) + bitcoinConfig, found := appContext.Config().GetBTCConfig(chain.ID()) + if !found { + return fmt.Errorf("unable to find btc config") } - btcObserver, ok := observer.(*btcobserver.Observer) - if !ok { - return fmt.Errorf("observer is not btc observer for chain %d", chainID) + rpcClient, err := btcrpc.NewRPCClient(bitcoinConfig) + if err != nil { + return errors.Wrap(err, "unable to create rpc client") + } + + database, err := db.NewFromSqliteInMemory(true) + if err != nil { + return errors.Wrap(err, "unable to open database") + } + + observer, err := btcobserver.NewObserver( + *chain.RawChain(), + rpcClient, + *chain.Params(), + client, + nil, + database, + baseLogger, + nil, + ) + if err != nil { + return errors.Wrap(err, "unable to create btc observer") } - ballotIdentifier, err = btcObserver.CheckReceiptForBtcTxHash(ctx, inboundHash, false) + ballotIdentifier, err = observer.CheckReceiptForBtcTxHash(ctx, inboundHash, false) if err != nil { return err } diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index ae7da7e6ac..3f2a9f6e4b 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -14,6 +14,7 @@ import ( "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/graceful" zetaos "github.com/zeta-chain/node/pkg/os" + "github.com/zeta-chain/node/pkg/scheduler" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" @@ -115,6 +116,8 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to setup TSS service") } + graceful.AddStopper(tss.Stop) + // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, logger.Std).Listen(ctx, func() { @@ -159,9 +162,27 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to create orchestrator") } + taskScheduler := scheduler.New(logger.Std) + maestroV2Deps := &orchestrator.Dependencies{ + Zetacore: zetacoreClient, + TSS: tss, + DBPath: dbPath, + Telemetry: telemetry, + } + + maestroV2, err := orchestrator.NewV2(taskScheduler, maestroV2Deps, logger) + if err != nil { + return errors.Wrap(err, "unable to create orchestrator V2") + } + // Start orchestrator with all observers and signers graceful.AddService(ctx, maestro) + // Start orchestrator V2 + // V2 will co-exist with V1 until all types of chains will be refactored (BTC, EVM, SOL, TON). + // (currently it's only BTC) + graceful.AddService(ctx, maestroV2) + // Block current routine until a shutdown signal is received graceful.WaitForShutdown() diff --git a/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go b/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go index c469063f00..c94e6cb6b7 100644 --- a/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go +++ b/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go @@ -6,7 +6,7 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/testutil/sample" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinDepositAndCallRevert(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go b/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go index 9e3606759b..c7ae665423 100644 --- a/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go +++ b/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/testutil/sample" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) // TestBitcoinDepositAndCallRevertWithDust sends a Bitcoin deposit that reverts with a dust amount in the revert outbound. diff --git a/e2e/e2etests/test_bitcoin_deposit_call.go b/e2e/e2etests/test_bitcoin_deposit_call.go index 415dc780f0..c73bc3d6b0 100644 --- a/e2e/e2etests/test_bitcoin_deposit_call.go +++ b/e2e/e2etests/test_bitcoin_deposit_call.go @@ -9,7 +9,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { @@ -20,7 +20,7 @@ func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { // Given amount to send require.Len(r, args, 1) amount := utils.ParseFloat(r, args[0]) - amountTotal := amount + zetabitcoin.DefaultDepositorFee + amountTotal := amount + common.DefaultDepositorFee // Given a list of UTXOs utxos, err := r.ListDeployerUTXOs() @@ -45,7 +45,7 @@ func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // check if example contract has been called, 'bar' value should be set to amount - amountSats, err := zetabitcoin.GetSatoshis(amount) + amountSats, err := common.GetSatoshis(amount) require.NoError(r, err) utils.MustHaveCalledExampleContract(r, contract, big.NewInt(amountSats)) } diff --git a/e2e/e2etests/test_bitcoin_donation.go b/e2e/e2etests/test_bitcoin_donation.go index 203914545a..ccddb91c51 100644 --- a/e2e/e2etests/test_bitcoin_donation.go +++ b/e2e/e2etests/test_bitcoin_donation.go @@ -9,7 +9,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/constant" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinDonation(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_bitcoin_std_deposit.go b/e2e/e2etests/test_bitcoin_std_deposit.go index fefd5ae039..ff8e9de4cd 100644 --- a/e2e/e2etests/test_bitcoin_std_deposit.go +++ b/e2e/e2etests/test_bitcoin_std_deposit.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinStdMemoDeposit(r *runner.E2ERunner, args []string) { @@ -54,7 +54,7 @@ func TestBitcoinStdMemoDeposit(r *runner.E2ERunner, args []string) { // the runner balance should be increased by the deposit amount amountIncreased := new(big.Int).Sub(balanceAfter, balanceBefore) - amountSatoshis, err := bitcoin.GetSatoshis(amount) + amountSatoshis, err := common.GetSatoshis(amount) require.NoError(r, err) require.Positive(r, amountSatoshis) // #nosec G115 always positive diff --git a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go index d223fa6afd..e1d897fca5 100644 --- a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go +++ b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/pkg/memo" testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinStdMemoDepositAndCall(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go index 162cf7c123..92f907a20a 100644 --- a/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go +++ b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/pkg/memo" testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinStdMemoInscribedDepositAndCall(r *runner.E2ERunner, args []string) { @@ -53,7 +53,7 @@ func TestBitcoinStdMemoInscribedDepositAndCall(r *runner.E2ERunner, args []strin utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // check if example contract has been called, 'bar' value should be set to correct amount - depositFeeSats, err := zetabitcoin.GetSatoshis(zetabitcoin.DefaultDepositorFee) + depositFeeSats, err := common.GetSatoshis(common.DefaultDepositorFee) require.NoError(r, err) receiveAmount := depositAmount - depositFeeSats utils.MustHaveCalledExampleContract(r, contract, big.NewInt(receiveAmount)) diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index ab322532a8..619844a30c 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -23,7 +23,7 @@ import ( "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabtc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" ) @@ -100,7 +100,7 @@ func (r *E2ERunner) DepositBTCWithAmount(amount float64, memo *memo.InboundMemo) r.Logger.Info("Now sending two txs to TSS address...") // add depositor fee so that receiver gets the exact given 'amount' in ZetaChain - amount += zetabitcoin.DefaultDepositorFee + amount += zetabtc.DefaultDepositorFee // deposit to TSS address var txHash *chainhash.Hash @@ -148,7 +148,7 @@ func (r *E2ERunner) DepositBTC(receiver common.Address) { r.Logger.Info("Now sending two txs to TSS address and tester ZEVM address...") // send initial BTC to the tester ZEVM address - amount := 1.15 + zetabitcoin.DefaultDepositorFee + amount := 1.15 + zetabtc.DefaultDepositorFee txHash, err := r.DepositBTCWithLegacyMemo(amount, utxos[:2], receiver) require.NoError(r, err) @@ -241,7 +241,7 @@ func (r *E2ERunner) sendToAddrFromDeployerWithMemo( // use static fee 0.0005 BTC to calculate change feeSats := btcutil.Amount(0.0005 * btcutil.SatoshiPerBitcoin) - amountInt, err := zetabitcoin.GetSatoshis(amount) + amountInt, err := zetabtc.GetSatoshis(amount) require.NoError(r, err) amountSats := btcutil.Amount(amountInt) change := inputSats - feeSats - amountSats @@ -351,7 +351,7 @@ func (r *E2ERunner) InscribeToTSSFromDeployerWithMemo( // parameters to build the reveal transaction commitOutputIdx := uint32(0) - commitAmount, err := zetabitcoin.GetSatoshis(amount) + commitAmount, err := zetabtc.GetSatoshis(amount) require.NoError(r, err) // build the reveal transaction to spend above funds @@ -412,7 +412,7 @@ func (r *E2ERunner) QueryOutboundReceiverAndAmount(txid string) (string, int64) // parse receiver address from pkScript txOutput := revertTx.MsgTx().TxOut[1] pkScript := txOutput.PkScript - receiver, err := zetabitcoin.DecodeScriptP2WPKH(hex.EncodeToString(pkScript), r.BitcoinParams) + receiver, err := zetabtc.DecodeScriptP2WPKH(hex.EncodeToString(pkScript), r.BitcoinParams) require.NoError(r, err) return receiver, txOutput.Value diff --git a/go.mod b/go.mod index 8c069a886b..a0169489b7 100644 --- a/go.mod +++ b/go.mod @@ -163,7 +163,7 @@ require ( github.com/google/gopacket v1.1.19 // indirect github.com/google/orderedcode v0.0.1 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/uuid v1.6.0 github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect @@ -249,8 +249,8 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/client_model v0.4.0 + github.com/prometheus/common v0.42.0 github.com/prometheus/procfs v0.9.0 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index e0d8478fca..68e7db1194 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -8,6 +8,8 @@ import ( "github.com/btcsuite/btcd/chaincfg" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/tonkeeper/tongo/ton" + + "github.com/zeta-chain/node/zetaclient/logs" ) // Validate checks whether the chain is valid @@ -108,6 +110,13 @@ func (chain Chain) IsTONChain() bool { return chain.Consensus == Consensus_catchain_consensus } +func (chain Chain) LogFields() map[string]any { + return map[string]any{ + logs.FieldChain: chain.ChainId, + logs.FieldChainNetwork: chain.Network.String(), + } +} + // DecodeAddressFromChainID decode the address string to bytes // additionalChains is a list of additional chains to search from // in practice, it is used in the protocol to dynamically support new chains without doing an upgrade diff --git a/pkg/fanout/fanout.go b/pkg/fanout/fanout.go new file mode 100644 index 0000000000..7a5f277842 --- /dev/null +++ b/pkg/fanout/fanout.go @@ -0,0 +1,66 @@ +// Package fanout provides a fan-out pattern implementation. +// It allows one channel to stream data to multiple independent channels. +// Note that context handling is out of the scope of this package. +package fanout + +import "sync" + +const DefaultBuffer = 8 + +// FanOut is a fan-out pattern implementation. +// It is NOT a worker pool, so use it wisely. +type FanOut[T any] struct { + input <-chan T + outputs []chan T + + // outputBuffer chan buffer size for outputs channels. + // This helps with writing to chan in case of slow consumers. + outputBuffer int + + mu sync.RWMutex +} + +// New constructs FanOut +func New[T any](source <-chan T, buf int) *FanOut[T] { + return &FanOut[T]{ + input: source, + outputs: make([]chan T, 0), + outputBuffer: buf, + } +} + +func (f *FanOut[T]) Add() <-chan T { + out := make(chan T, f.outputBuffer) + + f.mu.Lock() + defer f.mu.Unlock() + + f.outputs = append(f.outputs, out) + + return out +} + +// Start starts the fan-out process +func (f *FanOut[T]) Start() { + go func() { + // loop for new data + for data := range f.input { + f.mu.RLock() + for _, output := range f.outputs { + // note that this might spawn lots of goroutines. + // it is a naive approach, but should be more than enough for our use cases. + go func(output chan<- T) { output <- data }(output) + } + f.mu.RUnlock() + } + + // at this point, the input was closed + f.mu.Lock() + defer f.mu.Unlock() + for _, out := range f.outputs { + close(out) + } + + f.outputs = nil + }() +} diff --git a/pkg/fanout/fanout_test.go b/pkg/fanout/fanout_test.go new file mode 100644 index 0000000000..884d122e30 --- /dev/null +++ b/pkg/fanout/fanout_test.go @@ -0,0 +1,72 @@ +package fanout + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestFanOut(t *testing.T) { + // ARRANGE + // Given an input + input := make(chan int) + + // Given a fanout + f := New(input, DefaultBuffer) + + // That has 3 outputs + out1 := f.Add() + out2 := f.Add() + out3 := f.Add() + + // Given a wait group + wg := sync.WaitGroup{} + wg.Add(3) + + // Given a sample number + var total int32 + + // Given a consumer + consumer := func(out <-chan int, name string, lag time.Duration) { + defer wg.Done() + var local int32 + for i := range out { + // simulate some work + time.Sleep(lag) + + local += int32(i) + t.Logf("%s: received %d", name, i) + } + + // add only if input was closed + atomic.AddInt32(&total, local) + } + + // ACT + f.Start() + + // Write to the channel + go func() { + for i := 1; i <= 10; i++ { + input <- i + t.Logf("fan-out: sent %d", i) + time.Sleep(50 * time.Millisecond) + } + + close(input) + }() + + go consumer(out1, "out1: fast consumer", 10*time.Millisecond) + go consumer(out2, "out2: average consumer", 60*time.Millisecond) + go consumer(out3, "out3: slow consumer", 150*time.Millisecond) + + wg.Wait() + + // ASSERT + // Check that total is valid + // total == sum(1...10) * 3 = n(n+1)/2 * 3 = 55 * 3 = 165 + require.Equal(t, int32(165), total) +} diff --git a/pkg/scheduler/opts.go b/pkg/scheduler/opts.go index 8e5d54e370..44ec8493c8 100644 --- a/pkg/scheduler/opts.go +++ b/pkg/scheduler/opts.go @@ -26,17 +26,20 @@ func LogFields(fields map[string]any) Opt { // Interval sets initial task interval. func Interval(interval time.Duration) Opt { - return func(_ *Task, opts *taskOpts) { opts.interval = interval } + return func(_ *Task, opts *taskOpts) { opts.interval = normalizeInterval(interval) } } -// Skipper sets task skipper function +// Skipper sets task skipper function. If it returns true, the task is skipped. func Skipper(skipper func() bool) Opt { return func(t *Task, _ *taskOpts) { t.skipper = skipper } } -// IntervalUpdater sets interval updater function. +// IntervalUpdater sets interval updater function. Overrides Interval. func IntervalUpdater(intervalUpdater func() time.Duration) Opt { - return func(_ *Task, opts *taskOpts) { opts.intervalUpdater = intervalUpdater } + return func(_ *Task, opts *taskOpts) { + opts.interval = normalizeInterval(intervalUpdater()) + opts.intervalUpdater = intervalUpdater + } } // BlockTicker makes Task to listen for new zeta blocks diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 2328cbddd7..c6bbb5c241 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -107,6 +107,18 @@ func (s *Scheduler) Register(ctx context.Context, exec Executable, opts ...Opt) return task } +func (s *Scheduler) Tasks() map[uuid.UUID]*Task { + s.mu.RLock() + defer s.mu.RUnlock() + + copied := make(map[uuid.UUID]*Task, len(s.tasks)) + for k, v := range s.tasks { + copied[k] = v + } + + return copied +} + // Stop stops all tasks. func (s *Scheduler) Stop() { s.StopGroup("") @@ -132,6 +144,11 @@ func (s *Scheduler) StopGroup(group Group) { return } + s.logger.Info(). + Int("tasks", len(selectedTasks)). + Str("group", string(group)). + Msg("Stopping scheduler group") + // Stop all selected tasks concurrently var wg sync.WaitGroup wg.Add(len(selectedTasks)) @@ -161,6 +178,14 @@ func (t *Task) Stop() { t.logger.Info().Int64("time_taken_ms", timeTakenMS).Msg("Stopped scheduler task") } +func (t *Task) Group() Group { + return t.group +} + +func (t *Task) Name() string { + return t.name +} + // execute executes Task with additional logging and metrics. func (t *Task) execute(ctx context.Context) error { startedAt := time.Now().UTC() @@ -209,3 +234,12 @@ func newTickable(task *Task, opts *taskOpts) tickable { task.logger, ) } + +// normalizeInterval ensures that the interval is positive to prevent panics. +func normalizeInterval(dur time.Duration) time.Duration { + if dur > 0 { + return dur + } + + return time.Second +} diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go index a993bc875a..9d82e8f7e3 100644 --- a/pkg/scheduler/scheduler_test.go +++ b/pkg/scheduler/scheduler_test.go @@ -157,12 +157,17 @@ func TestScheduler(t *testing.T) { // Interval updater that increases the interval by 50ms on each counter increment. intervalUpdater := func() time.Duration { - return time.Duration(atomic.LoadInt32(&counter)) * 50 * time.Millisecond + cnt := atomic.LoadInt32(&counter) + if cnt == 0 { + return time.Millisecond + } + + return time.Duration(cnt) * 50 * time.Millisecond } // ACT // Register task and stop it after x1.5 interval. - task := ts.scheduler.Register(ts.ctx, exec, Interval(time.Millisecond), IntervalUpdater(intervalUpdater)) + task := ts.scheduler.Register(ts.ctx, exec, IntervalUpdater(intervalUpdater)) time.Sleep(time.Second) task.Stop() diff --git a/pkg/scheduler/tickers.go b/pkg/scheduler/tickers.go index 613194c44b..228f11f82c 100644 --- a/pkg/scheduler/tickers.go +++ b/pkg/scheduler/tickers.go @@ -31,7 +31,7 @@ func newIntervalTicker( if intervalUpdater != nil { // noop if interval is not changed - t.SetInterval(intervalUpdater()) + t.SetInterval(normalizeInterval(intervalUpdater())) } return nil diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index 9ec0d4cb06..3fb98551d1 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -98,7 +98,7 @@ func Run(ctx context.Context, interval time.Duration, task Task, opts ...Opt) er return New(interval, task, opts...).Start(ctx) } -// Run runs the ticker by blocking current goroutine. It also invokes BEFORE ticker starts. +// Start runs the ticker by blocking current goroutine. It also invokes BEFORE ticker starts. // Stops when (if any): // - context is done (returns ctx.Err()) // - task returns an error or panics @@ -139,7 +139,7 @@ func (t *Ticker) Start(ctx context.Context) (err error) { case <-ctx.Done(): // if task is finished (i.e. last tick completed BEFORE ticker.Stop(), // then we need to return nil) - if t.stopped { + if t.isStopped() { return nil } return ctx.Err() @@ -214,11 +214,20 @@ func (t *Ticker) setStopState() { t.ctxCancel() t.stopped = true - t.ticker.Stop() + if t.ticker != nil { + t.ticker.Stop() + } t.logger.Info().Msgf("Ticker stopped") } +func (t *Ticker) isStopped() bool { + t.stateMu.Lock() + defer t.stateMu.Unlock() + + return t.stopped +} + // DurationFromUint64Seconds converts uint64 of seconds to time.Duration. func DurationFromUint64Seconds(seconds uint64) time.Duration { // #nosec G115 seconds should be in range and is not user controlled diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 5afb1cae11..699629684f 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -166,7 +166,13 @@ func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.mu.Lock() defer ob.mu.Unlock() + if observertypes.ChainParamsEqual(ob.chainParams, params) { + return + } + ob.chainParams = params + + ob.logger.Chain.Info().Any("observer.chain_params", params).Msg("updated chain params") } // ZetacoreClient returns the zetacore client for the observer. diff --git a/zetaclient/chains/bitcoin/bitcoin.go b/zetaclient/chains/bitcoin/bitcoin.go new file mode 100644 index 0000000000..0cfcbe1bad --- /dev/null +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -0,0 +1,229 @@ +package bitcoin + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/scheduler" + "github.com/zeta-chain/node/pkg/ticker" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/outboundprocessor" +) + +type Bitcoin struct { + scheduler *scheduler.Scheduler + observer *observer.Observer + signer *signer.Signer + proc *outboundprocessor.Processor +} + +func New( + scheduler *scheduler.Scheduler, + observer *observer.Observer, + signer *signer.Signer, +) *Bitcoin { + // TODO move this to base signer + // https://github.com/zeta-chain/node/issues/3330 + proc := outboundprocessor.NewProcessor(observer.Logger().Outbound) + + return &Bitcoin{ + scheduler: scheduler, + observer: observer, + signer: signer, + proc: proc, + } +} + +func (b *Bitcoin) Chain() chains.Chain { + return b.observer.Chain() +} + +func (b *Bitcoin) Start(ctx context.Context) error { + if ok := b.observer.Observer.Start(); !ok { + return errors.New("observer is already started") + } + + app, err := zctx.FromContext(ctx) + if err != nil { + return errors.Wrap(err, "unable to get app from context") + } + + newBlockChan, err := b.observer.ZetacoreClient().NewBlockSubscriber(ctx) + if err != nil { + return errors.Wrap(err, "unable to create new block subscriber") + } + + optInboundInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().InboundTicker) + }) + + optGasInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().GasPriceTicker) + }) + + optUTXOInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().WatchUtxoTicker) + }) + + optOutboundInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().OutboundTicker) + }) + + optInboundSkipper := scheduler.Skipper(func() bool { + return !app.IsInboundObservationEnabled() + }) + + optOutboundSkipper := scheduler.Skipper(func() bool { + return !app.IsOutboundObservationEnabled() + }) + + optGenericSkipper := scheduler.Skipper(func() bool { + return !b.observer.ChainParams().IsSupported + }) + + register := func(exec scheduler.Executable, name string, opts ...scheduler.Opt) { + opts = append([]scheduler.Opt{ + scheduler.GroupName(b.group()), + scheduler.Name(name), + }, opts...) + + b.scheduler.Register(ctx, exec, opts...) + } + + // Observers + register(b.observer.ObserveInbound, "observe_inbound", optInboundInterval, optInboundSkipper) + register(b.observer.ObserveInboundTrackers, "observe_inbound_trackers", optInboundInterval, optInboundSkipper) + register(b.observer.FetchUTXOs, "fetch_utxos", optUTXOInterval, optGenericSkipper) + register(b.observer.PostGasPrice, "post_gas_price", optGasInterval, optGenericSkipper) + register(b.observer.CheckRPCStatus, "check_rpc_status") + register(b.observer.ObserveOutbound, "observe_outbound", optOutboundInterval, optOutboundSkipper) + + // CCTX Scheduler + register(b.scheduleCCTX, "schedule_cctx", scheduler.BlockTicker(newBlockChan), optOutboundSkipper) + + return nil +} + +func (b *Bitcoin) Stop() { + b.observer.Logger().Chain.Info().Msg("stopping observer-signer") + b.scheduler.StopGroup(b.group()) +} + +func (b *Bitcoin) group() scheduler.Group { + return scheduler.Group( + fmt.Sprintf("btc:%d", b.observer.Chain().ChainId), + ) +} + +// scheduleCCTX schedules pending cross-chain transactions on NEW zeta blocks +// 1. schedule at most one keysign per ticker +// 2. schedule keysign only when nonce-mark UTXO is available +// 3. stop keysign when lookahead is reached +func (b *Bitcoin) scheduleCCTX(ctx context.Context) error { + var ( + lookahead = b.observer.ChainParams().OutboundScheduleLookahead + chainID = b.observer.Chain().ChainId + ) + + if err := b.updateChainParams(ctx); err != nil { + return errors.Wrap(err, "unable to update chain params") + } + + zetaBlock, ok := scheduler.BlockFromContext(ctx) + if !ok { + return errors.New("unable to get zeta block from context") + } + + // #nosec G115 always in range + zetaHeight := uint64(zetaBlock.Block.Height) + + cctxList, _, err := b.observer.ZetacoreClient().ListPendingCCTX(ctx, chainID) + if err != nil { + return errors.Wrap(err, "unable to list pending cctx") + } + + // schedule at most one keysign per ticker + for idx, cctx := range cctxList { + var ( + params = cctx.GetCurrentOutboundParam() + nonce = params.TssNonce + outboundID = outboundprocessor.ToOutboundID(cctx.Index, params.ReceiverChainId, nonce) + ) + + if params.ReceiverChainId != chainID { + b.outboundLogger(outboundID).Error().Msg("Schedule CCTX: chain id mismatch") + + continue + } + + // try confirming the outbound + continueKeysign, err := b.observer.VoteOutboundIfConfirmed(ctx, cctx) + + switch { + case err != nil: + b.outboundLogger(outboundID).Error().Err(err).Msg("Schedule CCTX: VoteOutboundIfConfirmed failed") + continue + case !continueKeysign: + b.outboundLogger(outboundID).Info().Msg("Schedule CCTX: outbound already processed") + continue + case nonce > b.observer.GetPendingNonce(): + // stop if the nonce being processed is higher than the pending nonce + return nil + case int64(idx) >= lookahead: + // stop if lookahead is reached 2 bitcoin confirmations span is 20 minutes on average. + // We look ahead up to 100 pending cctx to target TPM of 5. + b.outboundLogger(outboundID).Warn(). + Uint64("outbound.earliest_pending_nonce", cctxList[0].GetCurrentOutboundParam().TssNonce). + Msg("Schedule CCTX: lookahead reached") + return nil + case b.proc.IsOutboundActive(outboundID): + // outbound is already being processed + continue + } + + b.proc.StartTryProcess(outboundID) + + go b.signer.TryProcessOutbound( + ctx, + cctx, + b.proc, + outboundID, + b.observer, + b.observer.ZetacoreClient(), + zetaHeight, + ) + } + + return nil +} + +func (b *Bitcoin) updateChainParams(ctx context.Context) error { + // no changes for signer + + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + chain, err := app.GetChain(b.observer.Chain().ChainId) + if err != nil { + return err + } + + b.observer.SetChainParams(*chain.Params()) + + return nil +} + +func (b *Bitcoin) outboundLogger(id string) *zerolog.Logger { + l := b.observer.Logger().Outbound.With().Str("outbound.id", id).Logger() + + return &l +} diff --git a/zetaclient/chains/bitcoin/fee.go b/zetaclient/chains/bitcoin/common/fee.go similarity index 99% rename from zetaclient/chains/bitcoin/fee.go rename to zetaclient/chains/bitcoin/common/fee.go index 7ce483a5bf..84dad1687d 100644 --- a/zetaclient/chains/bitcoin/fee.go +++ b/zetaclient/chains/bitcoin/common/fee.go @@ -1,4 +1,4 @@ -package bitcoin +package common import ( "encoding/hex" diff --git a/zetaclient/chains/bitcoin/fee_test.go b/zetaclient/chains/bitcoin/common/fee_test.go similarity index 99% rename from zetaclient/chains/bitcoin/fee_test.go rename to zetaclient/chains/bitcoin/common/fee_test.go index 82f60ff0ef..8967c86cfc 100644 --- a/zetaclient/chains/bitcoin/fee_test.go +++ b/zetaclient/chains/bitcoin/common/fee_test.go @@ -1,4 +1,4 @@ -package bitcoin +package common import ( "math/rand" @@ -6,14 +6,13 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" + btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/require" - - btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/zeta-chain/node/pkg/chains" ) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/common/tx_script.go similarity index 99% rename from zetaclient/chains/bitcoin/tx_script.go rename to zetaclient/chains/bitcoin/common/tx_script.go index e15268a868..5b3fe9d11b 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/common/tx_script.go @@ -1,4 +1,4 @@ -package bitcoin +package common // #nosec G507 ripemd160 required for bitcoin address encoding import ( diff --git a/zetaclient/chains/bitcoin/tx_script_test.go b/zetaclient/chains/bitcoin/common/tx_script_test.go similarity index 88% rename from zetaclient/chains/bitcoin/tx_script_test.go rename to zetaclient/chains/bitcoin/common/tx_script_test.go index 0d4b96bd63..b47e2249e0 100644 --- a/zetaclient/chains/bitcoin/tx_script_test.go +++ b/zetaclient/chains/bitcoin/common/tx_script_test.go @@ -1,4 +1,4 @@ -package bitcoin_test +package common_test import ( "bytes" @@ -10,15 +10,14 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/testutil" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/testutils" ) // the relative path to the testdata directory -var TestDataDir = "../../" +var TestDataDir = "../../../" func TestDecodeVoutP2TR(t *testing.T) { // load archived tx raw result @@ -31,7 +30,7 @@ func TestDecodeVoutP2TR(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2TR - receiver, err := bitcoin.DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", receiver) } @@ -47,14 +46,14 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) @@ -62,7 +61,7 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_1 '51' to OP_2 '52' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "51", "52", 1) - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) @@ -70,7 +69,7 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '20' to '19' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "5120", "5119", 1) - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) } @@ -86,7 +85,7 @@ func TestDecodeVoutP2WSH(t *testing.T) { require.Len(t, rawResult.Vout, 1) // decode vout 0, P2WSH - receiver, err := bitcoin.DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", receiver) } @@ -102,14 +101,14 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) @@ -117,7 +116,7 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_0 '00' to OP_1 '51' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "00", "51", 1) - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) @@ -125,7 +124,7 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '20' to '19' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0020", "0019", 1) - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) } @@ -143,17 +142,17 @@ func TestDecodeP2WPKHVout(t *testing.T) { require.Len(t, rawResult.Vout, 3) // decode vout 0, nonce mark 148 - receiver, err := bitcoin.DecodeScriptP2WPKH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2WPKH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) // decode vout 1, payment 0.00012000 BTC - receiver, err = bitcoin.DecodeScriptP2WPKH(rawResult.Vout[1].ScriptPubKey.Hex, net) + receiver, err = common.DecodeScriptP2WPKH(rawResult.Vout[1].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qpsdlklfcmlcfgm77c43x65ddtrt7n0z57hsyjp", receiver) // decode vout 2, change 0.39041489 BTC - receiver, err = bitcoin.DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) + receiver, err = common.DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) } @@ -172,14 +171,14 @@ func TestDecodeP2WPKHVoutErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 22 - _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WPKH script") }) @@ -187,7 +186,7 @@ func TestDecodeP2WPKHVoutErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0014", "0013", 1) - _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WPKH script") }) } @@ -203,7 +202,7 @@ func TestDecodeVoutP2SH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2SH - receiver, err := bitcoin.DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", receiver) } @@ -219,21 +218,21 @@ func TestDecodeVoutP2SHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 23 - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_HASH160 'a9' to OP_HASH256 'aa' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a9", "aa", 1) - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) @@ -241,14 +240,14 @@ func TestDecodeVoutP2SHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a914", "a913", 1) - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) t.Run("should return error on invalid OP_EQUAL", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "87", "88", 1) - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) } @@ -264,7 +263,7 @@ func TestDecodeVoutP2PKH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2PKH - receiver, err := bitcoin.DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", receiver) } @@ -280,14 +279,14 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "76a914" // 3 bytes, should be 25 - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -295,14 +294,14 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_DUP '76' to OP_NIP '77' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76", "77", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_HASH160 'a9' to OP_HASH256 'aa' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a9", "76aa", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -310,7 +309,7 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a914", "76a913", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -318,7 +317,7 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_EQUALVERIFY '88' to OP_RESERVED1 '89' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "89ac", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -326,7 +325,7 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_CHECKSIG 'ac' to OP_CHECKSIGVERIFY 'ad' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "88ad", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) } @@ -370,7 +369,7 @@ func TestDecodeOpReturnMemo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - memo, found, err := bitcoin.DecodeOpReturnMemo(tt.scriptHex) + memo, found, err := common.DecodeOpReturnMemo(tt.scriptHex) require.NoError(t, err) require.Equal(t, tt.found, found) require.True(t, bytes.Equal(tt.expected, memo)) @@ -415,7 +414,7 @@ func TestDecodeOpReturnMemoErrors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - memo, found, err := bitcoin.DecodeOpReturnMemo(tt.scriptHex) + memo, found, err := common.DecodeOpReturnMemo(tt.scriptHex) require.ErrorContains(t, err, tt.errMsg) require.False(t, found) require.Nil(t, memo) @@ -493,7 +492,7 @@ func TestDecodeSenderFromScript(t *testing.T) { } // Decode the sender address from the script - sender, err := bitcoin.DecodeSenderFromScript(pkScript, net) + sender, err := common.DecodeSenderFromScript(pkScript, net) // Validate the results require.NoError(t, err) @@ -511,7 +510,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2TR", txHash) receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(45000), amount) @@ -523,7 +522,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2WSH", txHash) receiverExpected := "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(36557203), amount) @@ -535,7 +534,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2WPKH", txHash) receiverExpected := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(79938), amount) @@ -547,7 +546,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2SH", txHash) receiverExpected := "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(1003881), amount) @@ -559,7 +558,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2PKH", txHash) receiverExpected := "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(1140000), amount) @@ -578,7 +577,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { t.Run("should return error on invalid amount", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.Value = -0.05 // use negative amount - receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(invalidVout, receiverExpected, chain) require.ErrorContains(t, err, "error getting satoshis") require.Empty(t, receiver) require.Zero(t, amount) @@ -588,7 +587,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // use invalid chain invalidChain := chains.Chain{ChainId: 123} - receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, invalidChain) + receiver, amount, err := common.DecodeTSSVout(invalidVout, receiverExpected, invalidChain) require.ErrorContains(t, err, "error GetBTCChainParams") require.Empty(t, receiver) require.Zero(t, amount) @@ -598,7 +597,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // use testnet params to decode mainnet receiver wrongChain := chains.BitcoinTestnet - receiver, amount, err := bitcoin.DecodeTSSVout( + receiver, amount, err := common.DecodeTSSVout( invalidVout, "bc1qulmx8ej27cj0xe20953cztr2excnmsqvuh0s5c", wrongChain, @@ -611,7 +610,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { t.Run("should return error on decoding failure", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(invalidVout, receiverExpected, chain) require.ErrorContains(t, err, "error decoding TSS vout") require.Empty(t, receiver) require.Zero(t, amount) @@ -624,7 +623,7 @@ func TestDecodeScript(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ac00634d0802c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f4c50646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe68" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.Nil(t, err) require.True(t, isFound) @@ -638,7 +637,7 @@ func TestDecodeScript(t *testing.T) { data := "20d6f59371037bf30115d9fd6016f0e3ef552cdfc0367ee20aa9df3158f74aaeb4ac00634c51bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c397160881668" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.Nil(t, err) require.True(t, isFound) @@ -652,7 +651,7 @@ func TestDecodeScript(t *testing.T) { data := "20cabd6ecc0245c40f27ca6299dcd3732287c317f3946734f04e27568fc5334218ac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "should contain more data, but script ended") require.False(t, isFound) require.Nil(t, memo) @@ -663,7 +662,7 @@ func TestDecodeScript(t *testing.T) { data := "1f01a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "public key not found") require.False(t, isFound) require.Nil(t, memo) @@ -674,7 +673,7 @@ func TestDecodeScript(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ab" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "OP_CHECKSIG not found") require.False(t, isFound) require.Nil(t, memo) @@ -683,7 +682,7 @@ func TestDecodeScript(t *testing.T) { t.Run("parsing opcode OP_DATA_32 failed", func(t *testing.T) { data := "01" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "public key not found") require.False(t, isFound) @@ -693,7 +692,7 @@ func TestDecodeScript(t *testing.T) { t.Run("parsing opcode OP_CHECKSIG failed", func(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c701" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "OP_CHECKSIG not found") require.False(t, isFound) diff --git a/zetaclient/chains/bitcoin/utils.go b/zetaclient/chains/bitcoin/common/utils.go similarity index 77% rename from zetaclient/chains/bitcoin/utils.go rename to zetaclient/chains/bitcoin/common/utils.go index d14fb6315a..e6aaccce3a 100644 --- a/zetaclient/chains/bitcoin/utils.go +++ b/zetaclient/chains/bitcoin/common/utils.go @@ -1,7 +1,6 @@ -package bitcoin +package common import ( - "encoding/json" "math" "github.com/btcsuite/btcd/btcutil" @@ -10,19 +9,6 @@ import ( // TODO(revamp): Remove utils.go and move the functions to the appropriate files -// PrettyPrintStruct returns a pretty-printed string representation of a struct -func PrettyPrintStruct(val interface{}) (string, error) { - prettyStruct, err := json.MarshalIndent( - val, - "", - " ", - ) - if err != nil { - return "", err - } - return string(prettyStruct), nil -} - // GetSatoshis converts a bitcoin amount to satoshis func GetSatoshis(btc float64) (int64, error) { // The amount is only considered invalid if it cannot be represented diff --git a/zetaclient/chains/bitcoin/errors.go b/zetaclient/chains/bitcoin/errors.go deleted file mode 100644 index d04d67687d..0000000000 --- a/zetaclient/chains/bitcoin/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package bitcoin - -import "errors" - -// ErrBitcoinNotEnabled is the error returned when bitcoin is not enabled -var ErrBitcoinNotEnabled = errors.New("bitcoin is not enabled") diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 27f0839856..ecaf9f1e7a 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -14,61 +14,12 @@ import ( "github.com/zeta-chain/node/pkg/coin" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/interfaces" - zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/logs" - "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) -// WatchInbound watches Bitcoin chain for inbounds on a ticker -// It starts a ticker and run ObserveInbound -// TODO(revamp): move all ticker related methods in the same file -func (ob *Observer) WatchInbound(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - - ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.ChainParams().InboundTicker) - if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") - return err - } - defer ticker.Stop() - - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) - sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) - - // ticker loop - for { - select { - case <-ticker.C(): - if !app.IsInboundObservationEnabled() { - sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) - continue - } - err := ob.ObserveInbound(ctx) - if err != nil { - // skip showing log for block number 0 as it means Bitcoin node is not enabled - // TODO: prevent this routine from running if Bitcoin node is not enabled - // https://github.com/zeta-chain/node/issues/2790 - if !errors.Is(err, bitcoin.ErrBitcoinNotEnabled) { - ob.logger.Inbound.Error().Err(err).Msg("WatchInbound error observing in tx") - } else { - ob.logger.Inbound.Debug().Err(err).Msg("WatchInbound: Bitcoin node is not enabled") - } - } - ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.StopChannel(): - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) - return nil - } - } -} - // ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore // TODO(revamp): simplify this function into smaller functions func (ob *Observer) ObserveInbound(ctx context.Context) error { @@ -83,9 +34,13 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // 0 will be returned if the node is not synced if currentBlock == 0 { - return errors.Wrap(bitcoin.ErrBitcoinNotEnabled, "observeInboundBTC: current block number 0 is too low") + ob.nodeEnabled.Store(false) + ob.logger.Inbound.Debug().Err(err).Msg("WatchInbound: Bitcoin node is not enabled") + return nil } + ob.nodeEnabled.Store(true) + // #nosec G115 checked positive lastBlock := uint64(currentBlock) if lastBlock < ob.LastBlock() { @@ -156,44 +111,9 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { return nil } -// WatchInboundTracker watches zetacore for bitcoin inbound trackers -// TODO(revamp): move all ticker related methods in the same file -func (ob *Observer) WatchInboundTracker(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - - ticker, err := types.NewDynamicTicker("Bitcoin_WatchInboundTracker", ob.ChainParams().InboundTicker) - if err != nil { - ob.logger.Inbound.Err(err).Msg("error creating ticker") - return err - } - - defer ticker.Stop() - for { - select { - case <-ticker.C(): - if !app.IsInboundObservationEnabled() { - continue - } - err := ob.ProcessInboundTrackers(ctx) - if err != nil { - ob.logger.Inbound.Error(). - Err(err). - Msgf("error observing inbound tracker for chain %d", ob.Chain().ChainId) - } - ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.StopChannel(): - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) - return nil - } - } -} - -// ProcessInboundTrackers processes inbound trackers +// ObserveInboundTrackers processes inbound trackers // TODO(revamp): move inbound tracker logic in a specific file -func (ob *Observer) ProcessInboundTrackers(ctx context.Context) error { +func (ob *Observer) ObserveInboundTrackers(ctx context.Context) error { trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ctx, ob.Chain().ChainId) if err != nil { return err @@ -263,7 +183,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, uint64(blockVb.Height), ob.logger.Inbound, ob.netParams, - bitcoin.CalcDepositorFee, + common.CalcDepositorFee, ) if err != nil { return "", err @@ -303,7 +223,7 @@ func FilterAndParseIncomingTx( continue // the first tx is coinbase; we do not process coinbase tx } - event, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, bitcoin.CalcDepositorFee) + event, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, common.CalcDepositorFee) if err != nil { // unable to parse the tx, the caller should retry return nil, errors.Wrapf(err, "error getting btc event for tx %s in block %d", tx.Txid, blockNumber) @@ -343,7 +263,7 @@ func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *crosscha } // convert the amount to integer (satoshis) - amountSats, err := bitcoin.GetSatoshis(event.Value) + amountSats, err := common.GetSatoshis(event.Value) if err != nil { ob.Logger().Inbound.Error().Err(err).Fields(lf).Msgf("can't convert value %f to satoshis", event.Value) return nil @@ -368,7 +288,7 @@ func GetBtcEvent( blockNumber uint64, logger zerolog.Logger, netParams *chaincfg.Params, - feeCalculator bitcoin.DepositorFeeCalculator, + feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { if netParams.Name == chaincfg.MainNetParams.Name { return GetBtcEventWithoutWitness(rpcClient, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) @@ -386,7 +306,7 @@ func GetBtcEventWithoutWitness( blockNumber uint64, logger zerolog.Logger, netParams *chaincfg.Params, - feeCalculator bitcoin.DepositorFeeCalculator, + feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { var ( found bool @@ -401,7 +321,7 @@ func GetBtcEventWithoutWitness( script := vout0.ScriptPubKey.Hex if len(script) == 44 && script[:4] == "0014" { // P2WPKH output: 0x00 + 20 bytes of pubkey hash - receiver, err := bitcoin.DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) + receiver, err := common.DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) if err != nil { // should never happen return nil, err } @@ -427,7 +347,7 @@ func GetBtcEventWithoutWitness( // 2nd vout must be a valid OP_RETURN memo vout1 := tx.Vout[1] - memo, found, err = bitcoin.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex) + memo, found, err = common.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex) if err != nil { logger.Error().Err(err).Msgf("GetBtcEvent: error decoding OP_RETURN memo: %s", vout1.ScriptPubKey.Hex) return nil, nil @@ -487,5 +407,5 @@ func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, n // decode sender address from previous pkScript pkScript := tx.MsgTx().TxOut[vin.Vout].PkScript - return bitcoin.DecodeSenderFromScript(pkScript, net) + return common.DecodeSenderFromScript(pkScript, net) } diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index b1cfe8d369..b438169b9f 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -16,12 +16,12 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/testutil" "github.com/zeta-chain/node/testutil/sample" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" clientcommon "github.com/zeta-chain/node/zetaclient/common" @@ -32,7 +32,7 @@ import ( ) // mockDepositFeeCalculator returns a mock depositor fee calculator that returns the given fee and error. -func mockDepositFeeCalculator(fee float64, err error) bitcoin.DepositorFeeCalculator { +func mockDepositFeeCalculator(fee float64, err error) common.DepositorFeeCalculator { return func(interfaces.BTCRPCClient, *btcjson.TxRawResult, *chaincfg.Params) (float64, error) { return fee, err } @@ -55,7 +55,7 @@ func TestAvgFeeRateBlock828440(t *testing.T) { path.Join(TestDataDir, testutils.TestDataPathBTC, "block_mempool.space_8332_828440.json"), ) - gasRate, err := bitcoin.CalcBlockAvgFeeRate(&blockVb, &chaincfg.MainNetParams) + gasRate, err := common.CalcBlockAvgFeeRate(&blockVb, &chaincfg.MainNetParams) require.NoError(t, err) require.Equal(t, int64(blockMb.Extras.AvgFeeRate), gasRate) } @@ -71,7 +71,7 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { t.Run("block has no transactions", func(t *testing.T) { emptyVb := btcjson.GetBlockVerboseTxResult{Tx: []btcjson.TxRawResult{}} - _, err := bitcoin.CalcBlockAvgFeeRate(&emptyVb, &chaincfg.MainNetParams) + _, err := common.CalcBlockAvgFeeRate(&emptyVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "block has no transactions") }) @@ -79,32 +79,32 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { coinbaseVb := btcjson.GetBlockVerboseTxResult{Tx: []btcjson.TxRawResult{ blockVb.Tx[0], }} - _, err := bitcoin.CalcBlockAvgFeeRate(&coinbaseVb, &chaincfg.MainNetParams) + _, err := common.CalcBlockAvgFeeRate(&coinbaseVb, &chaincfg.MainNetParams) require.NoError(t, err) }) t.Run("tiny block weight should fail", func(t *testing.T) { invalidVb := blockVb invalidVb.Weight = 3 - _, err := bitcoin.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) + _, err := common.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "block weight 3 too small") }) t.Run("block weight should not be less than coinbase tx weight", func(t *testing.T) { invalidVb := blockVb invalidVb.Weight = blockVb.Tx[0].Weight - 1 - _, err := bitcoin.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) + _, err := common.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "less than coinbase tx weight") }) t.Run("invalid block height should fail", func(t *testing.T) { invalidVb := blockVb invalidVb.Height = 0 - _, err := bitcoin.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) + _, err := common.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "invalid block height") invalidVb.Height = math.MaxInt32 + 1 - _, err = bitcoin.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) + _, err = common.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "invalid block height") }) @@ -112,14 +112,14 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { invalidVb := blockVb invalidVb.Tx = []btcjson.TxRawResult{blockVb.Tx[0], blockVb.Tx[1]} invalidVb.Tx[0].Hex = "invalid hex" - _, err := bitcoin.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) + _, err := common.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "failed to decode coinbase tx") }) t.Run("1st tx is not coinbase", func(t *testing.T) { invalidVb := blockVb invalidVb.Tx = []btcjson.TxRawResult{blockVb.Tx[1], blockVb.Tx[0]} - _, err := bitcoin.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) + _, err := common.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "not coinbase tx") }) @@ -144,7 +144,7 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { err = msgTx.Serialize(&buf) require.NoError(t, err) invalidVb.Tx[0].Hex = hex.EncodeToString(buf.Bytes()) - _, err = bitcoin.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) + _, err = common.CalcBlockAvgFeeRate(&invalidVb, &chaincfg.MainNetParams) require.Error(t, err) require.ErrorContains(t, err, "less than subsidy") }) @@ -283,7 +283,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { net := &chaincfg.MainNetParams // fee rate of above tx is 28 sat/vB - depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) + depositorFee := common.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) feeCalculator := mockDepositFeeCalculator(depositorFee, nil) // expected result @@ -613,7 +613,7 @@ func TestGetBtcEventErrors(t *testing.T) { blockNumber := uint64(835640) // fee rate of above tx is 28 sat/vB - depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) + depositorFee := common.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) feeCalculator := mockDepositFeeCalculator(depositorFee, nil) t.Run("should return error on invalid Vout[0] script", func(t *testing.T) { @@ -688,7 +688,7 @@ func TestGetBtcEvent(t *testing.T) { blockNumber := uint64(835640) net := &chaincfg.MainNetParams // 2.992e-05, see avgFeeRate https://mempool.space/api/v1/blocks/835640 - depositorFee := bitcoin.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier) + depositorFee := common.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier) feeCalculator := mockDepositFeeCalculator(depositorFee, nil) txHash2 := "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8" @@ -721,7 +721,7 @@ func TestGetBtcEvent(t *testing.T) { net := &chaincfg.MainNetParams // fee rate of above tx is 28 sat/vB - depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) + depositorFee := common.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) feeCalculator := mockDepositFeeCalculator(depositorFee, nil) // expected result diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index c3f09b17d4..d345f4da36 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -7,7 +7,7 @@ import ( "math" "math/big" "sort" - "strings" + "sync/atomic" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" @@ -16,11 +16,10 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/zeta-chain/node/pkg/bg" "github.com/zeta-chain/node/pkg/chains" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/db" "github.com/zeta-chain/node/zetaclient/metrics" @@ -41,8 +40,6 @@ const ( BigValueConfirmationCount = 6 ) -var _ interfaces.ChainObserver = (*Observer)(nil) - // Logger contains list of loggers used by Bitcoin chain observer type Logger struct { // base.Logger contains a list of base observer loggers @@ -84,6 +81,10 @@ type Observer struct { // broadcastedTx indexes the outbound hash with the outbound tx identifier broadcastedTx map[string]string + // nodeEnabled indicates whether BTC node is enabled (might be disabled during certain E2E tests) + // We assume it's true by default. The flag is updated on each ObserveInbound call. + nodeEnabled atomic.Bool + // logger contains the loggers used by the bitcoin observer logger Logger } @@ -136,50 +137,23 @@ func NewObserver( }, } + ob.nodeEnabled.Store(true) + // load last scanned block - if err := ob.LoadLastBlockScanned(); err != nil { + if err = ob.LoadLastBlockScanned(); err != nil { return nil, errors.Wrap(err, "unable to load last scanned block") } // load broadcasted transactions - if err := ob.LoadBroadcastedTxMap(); err != nil { + if err = ob.LoadBroadcastedTxMap(); err != nil { return nil, errors.Wrap(err, "unable to load broadcasted tx map") } return ob, nil } -// BtcClient returns the btc client -func (ob *Observer) BtcClient() interfaces.BTCRPCClient { - return ob.btcClient -} - -// Start starts the Go routine processes to observe the Bitcoin chain -func (ob *Observer) Start(ctx context.Context) { - if ok := ob.Observer.Start(); !ok { - ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) - return - } - - ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) - - // watch bitcoin chain for incoming txs and post votes to zetacore - bg.Work(ctx, ob.WatchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) - - // watch bitcoin chain for outgoing txs status - bg.Work(ctx, ob.WatchOutbound, bg.WithName("WatchOutbound"), bg.WithLogger(ob.Logger().Outbound)) - - // watch bitcoin chain for UTXOs owned by the TSS address - bg.Work(ctx, ob.WatchUTXOs, bg.WithName("WatchUTXOs"), bg.WithLogger(ob.Logger().Outbound)) - - // watch bitcoin chain for gas rate and post to zetacore - bg.Work(ctx, ob.WatchGasPrice, bg.WithName("WatchGasPrice"), bg.WithLogger(ob.Logger().GasPrice)) - - // watch zetacore for bitcoin inbound trackers - bg.Work(ctx, ob.WatchInboundTracker, bg.WithName("WatchInboundTracker"), bg.WithLogger(ob.Logger().Inbound)) - - // watch the RPC status of the bitcoin chain - bg.Work(ctx, ob.watchRPCStatus, bg.WithName("watchRPCStatus"), bg.WithLogger(ob.Logger().Chain)) +func (ob *Observer) isNodeEnabled() bool { + return ob.nodeEnabled.Load() } // GetPendingNonce returns the artificial pending nonce @@ -203,43 +177,6 @@ func (ob *Observer) ConfirmationsThreshold(amount *big.Int) int64 { return int64(ob.ChainParams().ConfirmationCount) } -// WatchGasPrice watches Bitcoin chain for gas rate and post to zetacore -// TODO(revamp): move ticker related functions to a specific file -// TODO(revamp): move inner logic in a separate function -func (ob *Observer) WatchGasPrice(ctx context.Context) error { - // report gas price right away as the ticker takes time to kick in - err := ob.PostGasPrice(ctx) - if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) - } - - // start gas price ticker - ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchGasPrice", ob.ChainParams().GasPriceTicker) - if err != nil { - return errors.Wrapf(err, "NewDynamicTicker error") - } - ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.Chain().ChainId, ob.ChainParams().GasPriceTicker) - - defer ticker.Stop() - for { - select { - case <-ticker.C(): - if !ob.ChainParams().IsSupported { - continue - } - err := ob.PostGasPrice(ctx) - if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) - } - ticker.UpdateInterval(ob.ChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.StopChannel(): - ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) - return nil - } - } -} - // PostGasPrice posts gas price to zetacore // TODO(revamp): move to gas price file func (ob *Observer) PostGasPrice(ctx context.Context) error { @@ -268,7 +205,7 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { if *feeResult.FeeRate > math.MaxInt64 { return fmt.Errorf("gas price is too large: %f", *feeResult.FeeRate) } - feeRateEstimated = bitcoin.FeeRateToSatPerByte(*feeResult.FeeRate).Uint64() + feeRateEstimated = common.FeeRateToSatPerByte(*feeResult.FeeRate).Uint64() } // query the current block number @@ -289,42 +226,6 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { return nil } -// WatchUTXOs watches bitcoin chain for UTXOs owned by the TSS address -// TODO(revamp): move ticker related functions to a specific file -func (ob *Observer) WatchUTXOs(ctx context.Context) error { - ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchUTXOs", ob.ChainParams().WatchUtxoTicker) - if err != nil { - ob.logger.UTXOs.Error().Err(err).Msg("error creating ticker") - return err - } - - defer ticker.Stop() - for { - select { - case <-ticker.C(): - if !ob.ChainParams().IsSupported { - continue - } - err := ob.FetchUTXOs(ctx) - if err != nil { - // log debug log if the error if no wallet is loaded - // this is to prevent extensive logging in localnet when the wallet is not loaded for non-Bitcoin test - // TODO: prevent this routine from running if Bitcoin node is not enabled - // https://github.com/zeta-chain/node/issues/2790 - if !strings.Contains(err.Error(), "No wallet is loaded") { - ob.logger.UTXOs.Error().Err(err).Msg("error fetching btc utxos") - } else { - ob.logger.UTXOs.Debug().Err(err).Msg("No wallet is loaded") - } - } - ticker.UpdateInterval(ob.ChainParams().WatchUtxoTicker, ob.logger.UTXOs) - case <-ob.StopChannel(): - ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.Chain().ChainId) - return nil - } - } -} - // FetchUTXOs fetches TSS-owned UTXOs from the Bitcoin node // TODO(revamp): move to UTXO file func (ob *Observer) FetchUTXOs(ctx context.Context) error { @@ -334,24 +235,31 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { } }() + // noop + if !ob.isNodeEnabled() { + return nil + } + // This is useful when a zetaclient's pending nonce lagged behind for whatever reason. ob.refreshPendingNonce(ctx) // get the current block height. bh, err := ob.btcClient.GetBlockCount() if err != nil { - return fmt.Errorf("btc: error getting block height : %v", err) + return errors.Wrap(err, "unable to get block height") } + maxConfirmations := int(bh) // List all unspent UTXOs (160ms) tssAddr, err := ob.TSS().PubKey().AddressBTC(ob.Chain().ChainId) if err != nil { - return fmt.Errorf("error getting bitcoin tss address") + return errors.Wrap(err, "unable to get tss address") } + utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{tssAddr}) if err != nil { - return err + return errors.Wrap(err, "unable to list unspent utxo") } // rigid sort to make utxo list deterministic @@ -369,7 +277,7 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { utxosFiltered := make([]btcjson.ListUnspentResult, 0) for _, utxo := range utxos { // UTXOs big enough to cover the cost of spending themselves - if utxo.Amount < bitcoin.DefaultDepositorFee { + if utxo.Amount < common.DefaultDepositorFee { continue } // we don't want to spend other people's unconfirmed UTXOs as they may not be safe to spend @@ -486,7 +394,7 @@ func (ob *Observer) specialHandleFeeRate() (uint64, error) { // hardcode gas price for regnet return 1, nil case chains.NetworkType_testnet: - feeRateEstimated, err := bitcoin.GetRecentFeeRate(ob.btcClient, ob.netParams) + feeRateEstimated, err := common.GetRecentFeeRate(ob.btcClient, ob.netParams) if err != nil { return 0, errors.Wrapf(err, "error GetRecentFeeRate") } diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 2e0f3dd9b1..7a7a0f372c 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -9,110 +9,71 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/pkg/errors" - "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/compliance" - zctx "github.com/zeta-chain/node/zetaclient/context" - "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) -// WatchOutbound watches Bitcoin chain for outgoing txs status -// TODO(revamp): move ticker functions to a specific file -// TODO(revamp): move into a separate package -func (ob *Observer) WatchOutbound(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return errors.Wrap(err, "unable to get app from context") - } +func (ob *Observer) ObserveOutbound(ctx context.Context) error { + chainID := ob.Chain().ChainId - ticker, err := types.NewDynamicTicker("Bitcoin_WatchOutbound", ob.ChainParams().OutboundTicker) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) if err != nil { - return errors.Wrap(err, "unable to create dynamic ticker") + return errors.Wrap(err, "unable to get all outbound trackers") } - defer ticker.Stop() - - chainID := ob.Chain().ChainId - ob.logger.Outbound.Info().Msgf("WatchOutbound started for chain %d", chainID) - sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) - - for { - select { - case <-ticker.C(): - if !app.IsOutboundObservationEnabled() { - sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) - continue - } - trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) - if err != nil { - ob.logger.Outbound.Error(). - Err(err). - Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", chainID) - continue - } - for _, tracker := range trackers { - // get original cctx parameters - outboundID := ob.OutboundID(tracker.Nonce) - cctx, err := ob.ZetacoreClient().GetCctxByNonce(ctx, chainID, tracker.Nonce) - if err != nil { - ob.logger.Outbound.Info(). - Err(err). - Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", chainID, tracker.Nonce) - break - } - - nonce := cctx.GetCurrentOutboundParam().TssNonce - if tracker.Nonce != nonce { // Tanmay: it doesn't hurt to check - ob.logger.Outbound.Error(). - Msgf("WatchOutbound: tracker nonce %d not match cctx nonce %d", tracker.Nonce, nonce) - break - } + for _, tracker := range trackers { + // get original cctx parameters + outboundID := ob.OutboundID(tracker.Nonce) + cctx, err := ob.ZetacoreClient().GetCctxByNonce(ctx, chainID, tracker.Nonce) + if err != nil { + return errors.Wrapf(err, "unable to get cctx by nonce %d", tracker.Nonce) + } - if len(tracker.HashList) > 1 { - ob.logger.Outbound.Warn(). - Msgf("WatchOutbound: oops, outboundID %s got multiple (%d) outbound hashes", outboundID, len(tracker.HashList)) - } + nonce := cctx.GetCurrentOutboundParam().TssNonce + if tracker.Nonce != nonce { // Tanmay: it doesn't hurt to check + return fmt.Errorf("tracker nonce %d not match cctx nonce %d", tracker.Nonce, nonce) + } - // iterate over all txHashes to find the truly included one. - // we do it this (inefficient) way because we don't rely on the first one as it may be a false positive (for unknown reason). - txCount := 0 - var txResult *btcjson.GetTransactionResult - for _, txHash := range tracker.HashList { - result, inMempool := ob.checkIncludedTx(ctx, cctx, txHash.TxHash) - if result != nil && !inMempool { // included - txCount++ - txResult = result - ob.logger.Outbound.Info(). - Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, chainID, tracker.Nonce) - if txCount > 1 { - ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, chainID, tracker.Nonce, result) - } - } - } + if len(tracker.HashList) > 1 { + ob.logger.Outbound.Warn(). + Msgf("WatchOutbound: oops, outboundID %s got multiple (%d) outbound hashes", outboundID, len(tracker.HashList)) + } - if txCount == 1 { // should be only one txHash included for each nonce - ob.setIncludedTx(tracker.Nonce, txResult) - } else if txCount > 1 { - ob.removeIncludedTx(tracker.Nonce) // we can't tell which txHash is true, so we remove all (if any) to be safe - ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, chainID, tracker.Nonce) + // iterate over all txHashes to find the truly included one. + // we do it this (inefficient) way because we don't rely on the first one as it may be a false positive (for unknown reason). + txCount := 0 + var txResult *btcjson.GetTransactionResult + for _, txHash := range tracker.HashList { + result, inMempool := ob.checkIncludedTx(ctx, cctx, txHash.TxHash) + if result != nil && !inMempool { // included + txCount++ + txResult = result + ob.logger.Outbound.Info(). + Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, chainID, tracker.Nonce) + if txCount > 1 { + ob.logger.Outbound.Error().Msgf( + "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, chainID, tracker.Nonce, result) } } - ticker.UpdateInterval(ob.ChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.StopChannel(): - ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", chainID) - return nil + } + + if txCount == 1 { // should be only one txHash included for each nonce + ob.setIncludedTx(tracker.Nonce, txResult) + } else if txCount > 1 { + ob.removeIncludedTx(tracker.Nonce) // we can't tell which txHash is true, so we remove all (if any) to be safe + ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, chainID, tracker.Nonce) } } + + return nil } // VoteOutboundIfConfirmed checks outbound status and returns (continueKeysign, error) @@ -418,7 +379,7 @@ func (ob *Observer) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { tssAddress := ob.TSSAddressString() amount := chains.NonceMarkAmount(nonce) for i, utxo := range ob.utxos { - sats, err := bitcoin.GetSatoshis(utxo.Amount) + sats, err := common.GetSatoshis(utxo.Amount) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("findNonceMarkUTXO: error getting satoshis for utxo %v", utxo) } @@ -608,7 +569,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ // the 2nd output is the payment to recipient receiverExpected = params.Receiver } - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.Chain()) + receiverVout, amount, err := common.DecodeTSSVout(vout, receiverExpected, ob.Chain()) if err != nil { return err } @@ -662,7 +623,7 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams tssAddress := ob.TSSAddressString() for _, vout := range vouts { // decode receiver and amount from vout - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain()) + receiverVout, amount, err := common.DecodeTSSVout(vout, tssAddress, ob.Chain()) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index 4cd935c88c..f09e3ec32d 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -2,44 +2,30 @@ package observer import ( "context" - "time" + + "github.com/pkg/errors" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" - "github.com/zeta-chain/node/zetaclient/common" ) -// watchRPCStatus watches the RPC status of the Bitcoin chain -func (ob *Observer) watchRPCStatus(_ context.Context) error { - ob.Logger().Chain.Info().Msgf("WatchRPCStatus started for chain %d", ob.Chain().ChainId) - - ticker := time.NewTicker(common.RPCStatusCheckInterval) - for { - select { - case <-ticker.C: - if !ob.ChainParams().IsSupported { - continue - } - - ob.checkRPCStatus() - case <-ob.StopChannel(): - return nil - } - } -} - -// checkRPCStatus checks the RPC status of the Bitcoin chain -func (ob *Observer) checkRPCStatus() { +// CheckRPCStatus checks the RPC status of the Bitcoin chain +func (ob *Observer) CheckRPCStatus(_ context.Context) error { tssAddress, err := ob.TSS().PubKey().AddressBTC(ob.Chain().ChainId) if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("unable to get TSS BTC address") - return + return errors.Wrap(err, "unable to get TSS BTC address") } blockTime, err := rpc.CheckRPCStatus(ob.btcClient, tssAddress) - if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("CheckRPCStatus failed") - return + switch { + case err != nil && !ob.isNodeEnabled(): + // suppress error if node is disabled + ob.logger.Chain.Debug().Err(err).Msg("CheckRPC status failed") + return nil + case err != nil: + return errors.Wrap(err, "unable to check RPC status") } ob.ReportBlockLatency(blockTime) + + return nil } diff --git a/zetaclient/chains/bitcoin/observer/witness.go b/zetaclient/chains/bitcoin/observer/witness.go index bb85bdc47b..69d2726459 100644 --- a/zetaclient/chains/bitcoin/observer/witness.go +++ b/zetaclient/chains/bitcoin/observer/witness.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/interfaces" ) @@ -24,7 +24,7 @@ func GetBtcEventWithWitness( blockNumber uint64, logger zerolog.Logger, netParams *chaincfg.Params, - feeCalculator bitcoin.DepositorFeeCalculator, + feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { if len(tx.Vout) < 1 { logger.Debug().Msgf("no output %s", tx.Txid) @@ -137,7 +137,7 @@ func tryExtractOpRet(tx btcjson.TxRawResult, logger zerolog.Logger) []byte { return nil } - memo, found, err := bitcoin.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex) + memo, found, err := common.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex) if err != nil { logger.Error().Err(err).Msgf("tryExtractOpRet: error decoding OP_RETURN memo: %s", tx.Vout[1].ScriptPubKey.Hex) return nil @@ -159,7 +159,7 @@ func tryExtractInscription(tx btcjson.TxRawResult, logger zerolog.Logger) []byte logger.Debug().Msgf("potential witness script, tx %s, input idx %d", tx.Txid, i) - memo, found, err := bitcoin.DecodeScript(script) + memo, found, err := common.DecodeScript(script) if err != nil || !found { logger.Debug().Msgf("invalid witness script, tx %s, input idx %d", tx.Txid, i) continue @@ -187,7 +187,7 @@ func isValidRecipient( tssAddress string, netParams *chaincfg.Params, ) error { - receiver, err := bitcoin.DecodeScriptP2WPKH(script, netParams) + receiver, err := common.DecodeScriptP2WPKH(script, netParams) if err != nil { return fmt.Errorf("invalid p2wpkh script detected, %s", err) } diff --git a/zetaclient/chains/bitcoin/observer/witness_test.go b/zetaclient/chains/bitcoin/observer/witness_test.go index 745f2003a9..34b676c7ac 100644 --- a/zetaclient/chains/bitcoin/observer/witness_test.go +++ b/zetaclient/chains/bitcoin/observer/witness_test.go @@ -10,9 +10,9 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" clientcommon "github.com/zeta-chain/node/zetaclient/common" "github.com/zeta-chain/node/zetaclient/testutils" @@ -60,7 +60,7 @@ func TestGetBtcEventWithWitness(t *testing.T) { net := &chaincfg.MainNetParams // fee rate of above tx is 28 sat/vB - depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) + depositorFee := common.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) feeCalculator := mockDepositFeeCalculator(depositorFee, nil) t.Run("decode OP_RETURN ok", func(t *testing.T) { diff --git a/zetaclient/chains/bitcoin/rpc/rpc.go b/zetaclient/chains/bitcoin/rpc/rpc.go index d29291c582..a553945a7e 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc.go +++ b/zetaclient/chains/bitcoin/rpc/rpc.go @@ -186,30 +186,29 @@ func CheckRPCStatus(client interfaces.BTCRPCClient, tssAddress btcutil.Address) // query latest block number bn, err := client.GetBlockCount() if err != nil { - return time.Time{}, errors.Wrap(err, "RPC failed on GetBlockCount, RPC down?") + return time.Time{}, errors.Wrap(err, "unable to get block count") } // query latest block header hash, err := client.GetBlockHash(bn) if err != nil { - return time.Time{}, errors.Wrap(err, "RPC failed on GetBlockHash, RPC down?") + return time.Time{}, errors.Wrapf(err, "unable to get hash for block %d", bn) } // query latest block header thru hash header, err := client.GetBlockHeader(hash) if err != nil { - return time.Time{}, errors.Wrap(err, "RPC failed on GetBlockHeader, RPC down?") + return time.Time{}, errors.Wrapf(err, "unable to get block header (%s)", hash.String()) } // should be able to list utxos owned by TSS address res, err := client.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddress}) - if err != nil { - return time.Time{}, errors.Wrap(err, "can't list utxos of TSS address; TSS address is not imported?") - } - // TSS address should have utxos - if len(res) == 0 { - return time.Time{}, errors.New("TSS address has no utxos; TSS address is not imported?") + switch { + case err != nil: + return time.Time{}, errors.Wrap(err, "unable to list TSS UTXOs") + case len(res) == 0: + return time.Time{}, errors.New("no UTXOs found for TSS") } return header.Timestamp, nil diff --git a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go index f3fdf5f12d..61369991d9 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go @@ -18,9 +18,9 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" + btc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/node/zetaclient/common" @@ -363,7 +363,7 @@ func compareAvgFeeRate(t *testing.T, client *rpcclient.Client, startBlock int, e if testnet { netParams = &chaincfg.TestNet3Params } - gasRate, err := bitcoin.CalcBlockAvgFeeRate(blockVb, netParams) + gasRate, err := btc.CalcBlockAvgFeeRate(blockVb, netParams) require.NoError(t, err) // compare with mempool.space @@ -415,7 +415,7 @@ func LiveTest_GetRecentFeeRate(t *testing.T) { require.NoError(t, err) // get fee rate from recent blocks - feeRate, err := bitcoin.GetRecentFeeRate(client, &chaincfg.TestNet3Params) + feeRate, err := btc.GetRecentFeeRate(client, &chaincfg.TestNet3Params) require.NoError(t, err) require.Greater(t, feeRate, uint64(0)) } @@ -596,19 +596,19 @@ func LiveTest_CalcDepositorFee(t *testing.T) { require.NoError(t, err) t.Run("should return default depositor fee", func(t *testing.T) { - depositorFee, err := bitcoin.CalcDepositorFee(client, rawResult, &chaincfg.RegressionNetParams) + depositorFee, err := btc.CalcDepositorFee(client, rawResult, &chaincfg.RegressionNetParams) require.NoError(t, err) - require.Equal(t, bitcoin.DefaultDepositorFee, depositorFee) + require.Equal(t, btc.DefaultDepositorFee, depositorFee) }) t.Run("should return correct depositor fee for a given tx", func(t *testing.T) { - depositorFee, err := bitcoin.CalcDepositorFee(client, rawResult, &chaincfg.MainNetParams) + depositorFee, err := btc.CalcDepositorFee(client, rawResult, &chaincfg.MainNetParams) require.NoError(t, err) // the actual fee rate is 860 sat/vByte // #nosec G115 always in range expectedRate := int64(float64(860) * common.BTCOutboundGasPriceMultiplier) - expectedFee := bitcoin.DepositorFee(expectedRate) + expectedFee := btc.DepositorFee(expectedRate) require.Equal(t, expectedFee, depositorFee) }) } diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index c142756f2d..34c0f592a7 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -16,7 +16,6 @@ import ( "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/zeta-chain/node/pkg/chains" @@ -25,7 +24,7 @@ import ( "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/compliance" @@ -48,8 +47,6 @@ const ( broadcastRetries = 5 ) -var _ interfaces.ChainSigner = (*Signer)(nil) - // Signer deals with signing BTC transactions and implements the ChainSigner interface type Signer struct { *base.Signer @@ -88,37 +85,6 @@ func NewSigner( }, nil } -// TODO: get rid of below four get/set functions for Bitcoin, as they are not needed in future -// https://github.com/zeta-chain/node/issues/2532 -// SetZetaConnectorAddress does nothing for BTC -func (signer *Signer) SetZetaConnectorAddress(_ ethcommon.Address) { -} - -// SetERC20CustodyAddress does nothing for BTC -func (signer *Signer) SetERC20CustodyAddress(_ ethcommon.Address) { -} - -// GetZetaConnectorAddress returns dummy address -func (signer *Signer) GetZetaConnectorAddress() ethcommon.Address { - return ethcommon.Address{} -} - -// GetERC20CustodyAddress returns dummy address -func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { - return ethcommon.Address{} -} - -// SetGatewayAddress does nothing for BTC -// Note: TSS address will be used as gateway address for Bitcoin -func (signer *Signer) SetGatewayAddress(_ string) { -} - -// GetGatewayAddress returns empty address -// Note: same as SetGatewayAddress -func (signer *Signer) GetGatewayAddress() string { - return "" -} - // AddWithdrawTxOutputs adds the 3 outputs to the withdraw tx // 1st output: the nonce-mark btc to TSS itself // 2nd output: the payment to the recipient @@ -133,14 +99,14 @@ func (signer *Signer) AddWithdrawTxOutputs( cancelTx bool, ) error { // convert withdraw amount to satoshis - amountSatoshis, err := bitcoin.GetSatoshis(amount) + amountSatoshis, err := common.GetSatoshis(amount) if err != nil { return err } // calculate remaining btc (the change) to TSS self remaining := total - amount - remainingSats, err := bitcoin.GetSatoshis(remaining) + remainingSats, err := common.GetSatoshis(remaining) if err != nil { return err } @@ -200,16 +166,12 @@ func (signer *Signer) SignWithdrawTx( chain chains.Chain, cancelTx bool, ) (*wire.MsgTx, error) { - estimateFee := float64(gasPrice.Uint64()*bitcoin.OutboundBytesMax) / 1e8 + estimateFee := float64(gasPrice.Uint64()*common.OutboundBytesMax) / 1e8 nonceMark := chains.NonceMarkAmount(nonce) // refresh unspent UTXOs and continue with keysign regardless of error - err := observer.FetchUTXOs(ctx) - if err != nil { - signer.Logger(). - Std.Error(). - Err(err). - Msgf("SignGasWithdraw: FetchUTXOs error: nonce %d chain %d", nonce, chain.ChainId) + if err := observer.FetchUTXOs(ctx); err != nil { + signer.Logger().Std.Error().Err(err).Uint64("nonce", nonce).Msg("SignWithdrawTx: FetchUTXOs failed") } // select N UTXOs to cover the total expense @@ -222,7 +184,7 @@ func (signer *Signer) SignWithdrawTx( false, ) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to select UTXOs") } // build tx with selected unspents @@ -230,8 +192,9 @@ func (signer *Signer) SignWithdrawTx( for _, prevOut := range prevOuts { hash, err := chainhash.NewHashFromStr(prevOut.TxID) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to construct hash") } + outpoint := wire.NewOutPoint(hash, prevOut.Vout) txIn := wire.NewTxIn(outpoint, nil, nil) tx.AddTxIn(txIn) @@ -239,23 +202,23 @@ func (signer *Signer) SignWithdrawTx( // size checking // #nosec G115 always positive - txSize, err := bitcoin.EstimateOutboundSize(uint64(len(prevOuts)), []btcutil.Address{to}) + txSize, err := common.EstimateOutboundSize(uint64(len(prevOuts)), []btcutil.Address{to}) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to estimate tx size") } - if sizeLimit < bitcoin.BtcOutboundBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user + if sizeLimit < common.BtcOutboundBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user signer.Logger().Std.Info(). Msgf("sizeLimit %d is less than BtcOutboundBytesWithdrawer %d for nonce %d", sizeLimit, txSize, nonce) } - if txSize < bitcoin.OutboundBytesMin { // outbound shouldn't be blocked a low sizeLimit + if txSize < common.OutboundBytesMin { // outbound shouldn't be blocked a low sizeLimit signer.Logger().Std.Warn(). - Msgf("txSize %d is less than outboundBytesMin %d; use outboundBytesMin", txSize, bitcoin.OutboundBytesMin) - txSize = bitcoin.OutboundBytesMin + Msgf("txSize %d is less than outboundBytesMin %d; use outboundBytesMin", txSize, common.OutboundBytesMin) + txSize = common.OutboundBytesMin } - if txSize > bitcoin.OutboundBytesMax { // in case of accident + if txSize > common.OutboundBytesMax { // in case of accident signer.Logger().Std.Warn(). - Msgf("txSize %d is greater than outboundBytesMax %d; use outboundBytesMax", txSize, bitcoin.OutboundBytesMax) - txSize = bitcoin.OutboundBytesMax + Msgf("txSize %d is greater than outboundBytesMax %d; use outboundBytesMax", txSize, common.OutboundBytesMax) + txSize = common.OutboundBytesMax } // fee calculation @@ -269,14 +232,14 @@ func (signer *Signer) SignWithdrawTx( // add tx outputs err = signer.AddWithdrawTxOutputs(tx, to, total, amount, nonceMark, fees, cancelTx) if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to add withdrawal tx outputs") } // sign the tx sigHashes := txscript.NewTxSigHashes(tx, txscript.NewCannedPrevOutputFetcher([]byte{}, 0)) witnessHashes := make([][]byte, len(tx.TxIn)) for ix := range tx.TxIn { - amt, err := bitcoin.GetSatoshis(prevOuts[ix].Amount) + amt, err := common.GetSatoshis(prevOuts[ix].Amount) if err != nil { return nil, err } @@ -292,7 +255,7 @@ func (signer *Signer) SignWithdrawTx( sig65Bs, err := signer.TSS().SignBatch(ctx, witnessHashes, height, nonce, chain.ChainId) if err != nil { - return nil, fmt.Errorf("SignBatch error: %v", err) + return nil, errors.Wrap(err, "unable to batch sign") } for ix := range tx.TxIn { @@ -314,22 +277,21 @@ func (signer *Signer) SignWithdrawTx( // Broadcast sends the signed transaction to the network func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error { - fmt.Printf("BTCSigner: Broadcasting: %s\n", signedTx.TxHash().String()) - var outBuff bytes.Buffer - err := signedTx.Serialize(&outBuff) - if err != nil { - return err + if err := signedTx.Serialize(&outBuff); err != nil { + return errors.Wrap(err, "unable to serialize tx") } - str := hex.EncodeToString(outBuff.Bytes()) - fmt.Printf("BTCSigner: Transaction Data: %s\n", str) - hash, err := signer.client.SendRawTransaction(signedTx, true) + signer.Logger().Std.Info(). + Stringer("signer.tx_hash", signedTx.TxHash()). + Str("signer.tx_payload", hex.EncodeToString(outBuff.Bytes())). + Msg("Broadcasting transaction") + + _, err := signer.client.SendRawTransaction(signedTx, true) if err != nil { - return err + return errors.Wrap(err, "unable to broadcast raw tx") } - signer.Logger().Std.Info().Msgf("Broadcasting BTC tx , hash %s ", hash) return nil } @@ -340,7 +302,7 @@ func (signer *Signer) TryProcessOutbound( cctx *types.CrossChainTx, outboundProcessor *outboundprocessor.Processor, outboundID string, - chainObserver interfaces.ChainObserver, + observer *observer.Observer, zetacoreClient interfaces.ZetacoreClient, height uint64, ) { @@ -369,14 +331,7 @@ func (signer *Signer) TryProcessOutbound( return } - // convert chain observer to BTC observer - btcObserver, ok := chainObserver.(*observer.Observer) - if !ok { - logger.Error().Msg("chain observer is not a bitcoin observer") - return - } - - chain := btcObserver.Chain() + chain := observer.Chain() outboundTssNonce := params.TssNonce signerAddress, err := zetacoreClient.GetKeys().GetAddress() if err != nil { @@ -411,7 +366,7 @@ func (signer *Signer) TryProcessOutbound( logger.Error().Err(err).Msgf("cannot get bitcoin network info") return } - satPerByte := bitcoin.FeeRateToSatPerByte(networkInfo.RelayFee) + satPerByte := common.FeeRateToSatPerByte(networkInfo.RelayFee) gasprice.Add(gasprice, satPerByte) // compliance check @@ -440,7 +395,7 @@ func (signer *Signer) TryProcessOutbound( amount, gasprice, sizelimit, - btcObserver, + observer, height, outboundTssNonce, chain, @@ -487,7 +442,7 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Fields(lf).Msgf("Add Bitcoin outbound tracker successfully") // Save successfully broadcasted transaction to btc chain observer - btcObserver.SaveBroadcastedTx(outboundHash, outboundTssNonce) + observer.SaveBroadcastedTx(outboundHash, outboundTssNonce) break // successful broadcast; no need to retry } diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index 4f35953ce0..26cab104c3 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -64,7 +64,9 @@ func (cr *ChainRegistry) Get(chainID int64) (Chain, error) { // All returns all chains in the registry sorted by chain ID. func (cr *ChainRegistry) All() []Chain { + cr.mu.Lock() items := maps.Values(cr.chains) + cr.mu.Unlock() slices.SortFunc(items, func(a, b Chain) int { return cmp.Compare(a.ID(), b.ID()) @@ -143,6 +145,10 @@ func (c Chain) Name() string { return c.chainInfo.Name } +func (c Chain) LogFields() map[string]any { + return c.RawChain().LogFields() +} + func (c Chain) Params() *observer.ChainParams { return c.observerParams } diff --git a/zetaclient/context/context.go b/zetaclient/context/context.go index 4d0b06866a..ee45eae58c 100644 --- a/zetaclient/context/context.go +++ b/zetaclient/context/context.go @@ -8,7 +8,7 @@ import ( type appContextKey struct{} -var ErrNotSet = errors.New("AppContext is not set in the context.Context") +var ErrNotSet = errors.New("unable to get AppContext from context.Context") // WithAppContext applied AppContext to standard Go context.Context. func WithAppContext(ctx goctx.Context, app *AppContext) goctx.Context { diff --git a/zetaclient/maintenance/shutdown_listener_test.go b/zetaclient/maintenance/shutdown_listener_test.go index c85c70cde0..f8ae414ef3 100644 --- a/zetaclient/maintenance/shutdown_listener_test.go +++ b/zetaclient/maintenance/shutdown_listener_test.go @@ -15,7 +15,7 @@ import ( func assertChannelNotClosed[T any](t *testing.T, ch <-chan T) { select { case <-ch: - t.FailNow() + t.Errorf("Failed: channel was closed") default: } } diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 3228610e76..9ab18415ae 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -15,9 +15,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/zetaclient/chains/base" - btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" - btcsigner "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" evmobserver "github.com/zeta-chain/node/zetaclient/chains/evm/observer" evmsigner "github.com/zeta-chain/node/zetaclient/chains/evm/signer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -139,19 +136,9 @@ func syncSignerMap( addSigner(chainID, signer) case chain.IsBitcoin(): - cfg, found := app.Config().GetBTCConfig(chainID) - if !found { - logger.Std.Warn().Msgf("Unable to find BTC config for chain %d signer", chainID) - continue - } - - signer, err := btcsigner.NewSigner(*rawChain, tss, logger, cfg) - if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to construct signer for BTC chain %d", chainID) - continue - } + // managed by orchestrator V2 + continue - addSigner(chainID, signer) case chain.IsSolana(): cfg, found := app.Config().GetSolanaConfig() if !found { @@ -345,40 +332,9 @@ func syncObserverMap( addObserver(chainID, observer) case chain.IsBitcoin(): - cfg, found := app.Config().GetBTCConfig(chainID) - if !found { - logger.Std.Warn().Msgf("Unable to find BTC config for chain %d observer", chainID) - continue - } - - btcRPC, err := rpc.NewRPCClient(cfg) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to create rpc client for BTC chain %d", chainID) - continue - } - - database, err := db.NewFromSqlite(dbpath, btcDatabaseFileName(*rawChain), true) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to open database for BTC chain %d", chainID) - continue - } - - btcObserver, err := btcobserver.NewObserver( - *rawChain, - btcRPC, - *params, - client, - tss, - database, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for BTC chain %d", chainID) - continue - } + // managed by orchestrator V2 + continue - addObserver(chainID, btcObserver) case chain.IsSolana(): cfg, found := app.Config().GetSolanaConfig() if !found { diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstrap_test.go similarity index 78% rename from zetaclient/orchestrator/bootstap_test.go rename to zetaclient/orchestrator/bootstrap_test.go index 71b3ec744b..c6f44acf9a 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstrap_test.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/ptr" @@ -36,9 +37,6 @@ func TestCreateSignerMap(t *testing.T) { t.Run("CreateSignerMap", func(t *testing.T) { // ARRANGE - // Given a BTC server - _, btcConfig := testrpc.NewBtcServer(t) - // Given a zetaclient config with ETH, MATIC, and BTC chains cfg := config.New(false) @@ -50,8 +48,6 @@ func TestCreateSignerMap(t *testing.T) { Endpoint: testutils.MockEVMRPCEndpoint, } - cfg.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = btcConfig - // Given AppContext app := zctx.New(cfg, nil, log) ctx := zctx.WithAppContext(context.Background(), app) @@ -69,15 +65,14 @@ func TestCreateSignerMap(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, signers) - // Okay, now we want to check that signers for EVM and BTC were created - assert.Equal(t, 2, len(signers)) + // Okay, now we want to check that signer for EVM was created + assert.Equal(t, 1, len(signers)) hasSigner(t, signers, chains.Ethereum.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) t.Run("Add polygon in the runtime", func(t *testing.T) { // ARRANGE mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, chains.BitcoinMainnet, chains.Polygon, + chains.Ethereum, chains.Polygon, }) // ACT @@ -90,7 +85,6 @@ func TestCreateSignerMap(t *testing.T) { hasSigner(t, signers, chains.Ethereum.ChainId) hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) }) t.Run("Disable ethereum in the runtime", func(t *testing.T) { @@ -109,57 +103,14 @@ func TestCreateSignerMap(t *testing.T) { missesSigner(t, signers, chains.Ethereum.ChainId) hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Re-enable ethereum in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, - chains.Polygon, - chains.BitcoinMainnet, - }) - - // ACT - added, removed, err := syncSignerMap(ctx, tss, baseLogger, &signers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 1, added) - assert.Equal(t, 0, removed) - - hasSigner(t, signers, chains.Ethereum.ChainId) - hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Disable btc in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, - chains.Polygon, - }) - - // ACT - added, removed, err := syncSignerMap(ctx, tss, baseLogger, &signers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 0, added) - assert.Equal(t, 1, removed) - - hasSigner(t, signers, chains.Ethereum.ChainId) - hasSigner(t, signers, chains.Polygon.ChainId) missesSigner(t, signers, chains.BitcoinMainnet.ChainId) }) - t.Run("Re-enable btc in the runtime", func(t *testing.T) { + t.Run("Re-enable ethereum in the runtime", func(t *testing.T) { // ARRANGE - // Given updated data from zetacore containing polygon chain mustUpdateAppContextChainParams(t, app, []chains.Chain{ chains.Ethereum, chains.Polygon, - chains.BitcoinMainnet, }) // ACT @@ -172,7 +123,6 @@ func TestCreateSignerMap(t *testing.T) { hasSigner(t, signers, chains.Ethereum.ChainId) hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) }) t.Run("No changes", func(t *testing.T) { @@ -201,6 +151,8 @@ func TestCreateChainObserverMap(t *testing.T) { dbPath = db.SqliteInMemory ) + mockZetacore(client) + t.Run("CreateChainObserverMap", func(t *testing.T) { // ARRANGE // Given a BTC server @@ -238,11 +190,12 @@ func TestCreateChainObserverMap(t *testing.T) { ctx := zctx.WithAppContext(context.Background(), app) // Given chain & chainParams "fetched" from zetacore - // (note that slice LACKS polygon & SOL chains on purpose) + // note that slice LACKS polygon & SOL chains on purpose + // also note that BTC is handled by orchestrator v2 mustUpdateAppContextChainParams(t, app, []chains.Chain{ chains.Ethereum, - chains.BitcoinMainnet, chains.TONMainnet, + chains.BitcoinMainnet, }) // ACT @@ -253,10 +206,10 @@ func TestCreateChainObserverMap(t *testing.T) { assert.NotEmpty(t, observers) // Okay, now we want to check that signers for EVM and BTC were created - assert.Equal(t, 3, len(observers)) + assert.Equal(t, 2, len(observers)) hasObserver(t, observers, chains.Ethereum.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) hasObserver(t, observers, chains.TONMainnet.ChainId) + missesObserver(t, observers, chains.BitcoinMainnet.ChainId) t.Run("Add polygon and remove TON in the runtime", func(t *testing.T) { // ARRANGE @@ -274,7 +227,6 @@ func TestCreateChainObserverMap(t *testing.T) { hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) }) t.Run("Add solana in the runtime", func(t *testing.T) { @@ -296,7 +248,6 @@ func TestCreateChainObserverMap(t *testing.T) { hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) hasObserver(t, observers, chains.SolanaMainnet.ChainId) }) @@ -317,7 +268,6 @@ func TestCreateChainObserverMap(t *testing.T) { missesObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) missesObserver(t, observers, chains.SolanaMainnet.ChainId) }) @@ -337,45 +287,6 @@ func TestCreateChainObserverMap(t *testing.T) { hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Disable btc in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, chains.Polygon, - }) - - // ACT - added, removed, err := syncObserverMap(ctx, client, tss, dbPath, baseLogger, ts, &observers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 0, added) - assert.Equal(t, 1, removed) - - hasObserver(t, observers, chains.Ethereum.ChainId) - hasObserver(t, observers, chains.Polygon.ChainId) - missesObserver(t, observers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Re-enable btc in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.BitcoinMainnet, chains.Ethereum, chains.Polygon, - }) - - // ACT - added, removed, err := syncObserverMap(ctx, client, tss, dbPath, baseLogger, ts, &observers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 1, added) - assert.Equal(t, 0, removed) - - hasObserver(t, observers, chains.Ethereum.ChainId) - hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) }) t.Run("No changes", func(t *testing.T) { @@ -503,6 +414,8 @@ func missesSigner(t *testing.T, signers map[int64]interfaces.ChainSigner, chainI } func hasObserver(t *testing.T, observer map[int64]interfaces.ChainObserver, chainId int64) { + t.Helper() + signer, ok := observer[chainId] assert.True(t, ok, "missing observer for chain %d", chainId) assert.NotEmpty(t, signer) @@ -512,3 +425,13 @@ func missesObserver(t *testing.T, observer map[int64]interfaces.ChainObserver, c _, ok := observer[chainId] assert.False(t, ok, "unexpected observer for chain %d", chainId) } + +// observer&signers have background tasks that rely on mocked calls. +// Ignorance results in FLAKY tests which fail silently with exit code 1. +func mockZetacore(client *mocks.ZetacoreClient) { + // ctx context.Context, chain chains.Chain, gasPrice uint64, priorityFee uint64, blockNum uint64 + client. + On("PostVoteGasPrice", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return("", nil). + Maybe() +} diff --git a/zetaclient/orchestrator/contextupdater.go b/zetaclient/orchestrator/contextupdater.go index 071ded772c..3c806a8eed 100644 --- a/zetaclient/orchestrator/contextupdater.go +++ b/zetaclient/orchestrator/contextupdater.go @@ -8,7 +8,6 @@ import ( "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/ticker" observertypes "github.com/zeta-chain/node/x/observer/types" zctx "github.com/zeta-chain/node/zetaclient/context" ) @@ -26,39 +25,6 @@ type Zetacore interface { var ErrUpgradeRequired = errors.New("upgrade required") -func (oc *Orchestrator) runAppContextUpdater(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - - interval := ticker.DurationFromUint64Seconds(app.Config().ConfigUpdateTicker) - - oc.logger.Info().Msg("UpdateAppContext worker started") - - task := func(ctx context.Context, t *ticker.Ticker) error { - err := UpdateAppContext(ctx, app, oc.zetacoreClient, oc.logger.Sampled) - switch { - case errors.Is(err, ErrUpgradeRequired): - oc.onUpgradeDetected(err) - t.Stop() - return nil - case err != nil: - oc.logger.Err(err).Msg("UpdateAppContext failed") - } - - return nil - } - - return ticker.Run( - ctx, - interval, - task, - ticker.WithLogger(oc.logger.Logger, "UpdateAppContext"), - ticker.WithStopChan(oc.stop), - ) -} - // UpdateAppContext fetches latest data from Zetacore and updates the AppContext. // Also detects if an upgrade is required. If an upgrade is required, it returns ErrUpgradeRequired. func UpdateAppContext(ctx context.Context, app *zctx.AppContext, zc Zetacore, logger zerolog.Logger) error { @@ -149,12 +115,3 @@ func checkForZetacoreUpgrade(ctx context.Context, zetaHeight int64, zc Zetacore) return nil } - -// onUpgradeDetected is called when an upgrade is detected. -func (oc *Orchestrator) onUpgradeDetected(errDetected error) { - const msg = "Upgrade detected." + - " Kill the process, replace the binary with upgraded version, and restart zetaclientd" - - oc.logger.Warn().Str("upgrade", errDetected.Error()).Msg(msg) - oc.Stop() -} diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 3e475fe442..122454db3b 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "sync" + "sync/atomic" "time" sdkmath "cosmossdk.io/math" @@ -19,9 +20,7 @@ import ( zetamath "github.com/zeta-chain/node/pkg/math" "github.com/zeta-chain/node/pkg/ticker" "github.com/zeta-chain/node/x/crosschain/types" - observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" - btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" solanaobserver "github.com/zeta-chain/node/zetaclient/chains/solana/observer" tonobserver "github.com/zeta-chain/node/zetaclient/chains/ton/observer" @@ -72,10 +71,11 @@ type Orchestrator struct { signerBlockTimeOffset time.Duration // misc - logger multiLogger - ts *metrics.TelemetryServer - stop chan struct{} - mu sync.RWMutex + logger multiLogger + ts *metrics.TelemetryServer + stop chan struct{} + stopped atomic.Bool + mu sync.RWMutex } type multiLogger struct { @@ -137,7 +137,6 @@ func (oc *Orchestrator) Start(ctx context.Context) error { bg.Work(ctx, oc.runScheduler, bg.WithName("runScheduler"), bg.WithLogger(oc.logger.Logger)) bg.Work(ctx, oc.runObserverSignerSync, bg.WithName("runObserverSignerSync"), bg.WithLogger(oc.logger.Logger)) - bg.Work(ctx, oc.runAppContextUpdater, bg.WithName("runAppContextUpdater"), bg.WithLogger(oc.logger.Logger)) bg.Work( ctx, oc.runSyncObserverOperationalFlags, @@ -149,7 +148,15 @@ func (oc *Orchestrator) Start(ctx context.Context) error { } func (oc *Orchestrator) Stop() { + // noop + if oc.stopped.Load() { + oc.logger.Warn().Msg("Already stopped") + return + } + close(oc.stop) + + oc.stopped.Store(true) } // returns signer with updated chain parameters. @@ -211,18 +218,7 @@ func (oc *Orchestrator) resolveObserver(app *zctx.AppContext, chainID int64) (in } // update chain observer chain parameters - var ( - curParams = observer.ChainParams() - freshParams = chain.Params() - ) - - if !observertypes.ChainParamsEqual(curParams, *freshParams) { - observer.SetChainParams(*freshParams) - oc.logger.Info(). - Int64("observer.chain_id", chainID). - Interface("observer.chain_params", *freshParams). - Msg("updated chain params") - } + observer.SetChainParams(*chain.Params()) return observer, nil } @@ -366,6 +362,13 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { continue } + // managed by V2 + if chain.IsBitcoin() { + continue + } + + // todo move metrics to v2 + chainID := chain.ID() // update chain parameters for signer and chain observer @@ -407,7 +410,8 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { case chain.IsEVM(): oc.ScheduleCctxEVM(ctx, zetaHeight, chainID, cctxList, ob, signer) case chain.IsBitcoin(): - oc.ScheduleCctxBTC(ctx, zetaHeight, chainID, cctxList, ob, signer) + // Managed by orchestrator V2 + continue case chain.IsSolana(): oc.ScheduleCctxSolana(ctx, zetaHeight, chainID, cctxList, ob, signer) case chain.IsTON(): @@ -523,81 +527,6 @@ func (oc *Orchestrator) ScheduleCctxEVM( } } -// ScheduleCctxBTC schedules bitcoin outbound keysign on each ZetaChain block (the ticker) -// 1. schedule at most one keysign per ticker -// 2. schedule keysign only when nonce-mark UTXO is available -// 3. stop keysign when lookahead is reached -func (oc *Orchestrator) ScheduleCctxBTC( - ctx context.Context, - zetaHeight uint64, - chainID int64, - cctxList []*types.CrossChainTx, - observer interfaces.ChainObserver, - signer interfaces.ChainSigner, -) { - btcObserver, ok := observer.(*btcobserver.Observer) - if !ok { // should never happen - oc.logger.Error().Msgf("ScheduleCctxBTC: chain observer is not a bitcoin observer") - return - } - // #nosec G115 positive - interval := uint64(observer.ChainParams().OutboundScheduleInterval) - lookahead := observer.ChainParams().OutboundScheduleLookahead - - // schedule at most one keysign per ticker - for idx, cctx := range cctxList { - params := cctx.GetCurrentOutboundParam() - nonce := params.TssNonce - outboundID := outboundprocessor.ToOutboundID(cctx.Index, params.ReceiverChainId, nonce) - - if params.ReceiverChainId != chainID { - oc.logger.Error(). - Msgf("ScheduleCctxBTC: outbound %s chainid mismatch: want %d, got %d", outboundID, chainID, params.ReceiverChainId) - continue - } - // try confirming the outbound - continueKeysign, err := btcObserver.VoteOutboundIfConfirmed(ctx, cctx) - if err != nil { - oc.logger.Error(). - Err(err). - Msgf("ScheduleCctxBTC: VoteOutboundIfConfirmed failed for chain %d nonce %d", chainID, nonce) - continue - } - if !continueKeysign { - oc.logger.Info(). - Msgf("ScheduleCctxBTC: outbound %s already processed; do not schedule keysign", outboundID) - continue - } - - // stop if the nonce being processed is higher than the pending nonce - if nonce > btcObserver.GetPendingNonce() { - break - } - // stop if lookahead is reached - if int64( - idx, - ) >= lookahead { // 2 bitcoin confirmations span is 20 minutes on average. We look ahead up to 100 pending cctx to target TPM of 5. - oc.logger.Warn(). - Msgf("ScheduleCctxBTC: lookahead reached, signing %d, earliest pending %d", nonce, cctxList[0].GetCurrentOutboundParam().TssNonce) - break - } - // schedule a TSS keysign - if nonce%interval == zetaHeight%interval && !oc.outboundProc.IsOutboundActive(outboundID) { - oc.outboundProc.StartTryProcess(outboundID) - oc.logger.Debug().Msgf("ScheduleCctxBTC: sign outbound %s with value %d", outboundID, params.Amount) - go signer.TryProcessOutbound( - ctx, - cctx, - oc.outboundProc, - outboundID, - observer, - oc.zetacoreClient, - zetaHeight, - ) - } - } -} - // ScheduleCctxSolana schedules solana outbound keysign on each ZetaChain block (the ticker) func (oc *Orchestrator) ScheduleCctxSolana( ctx context.Context, diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go new file mode 100644 index 0000000000..1962433c1c --- /dev/null +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -0,0 +1,73 @@ +package orchestrator + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" + btcsigner "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/db" +) + +func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin.Bitcoin, error) { + // should not happen + if !chain.IsBitcoin() { + return nil, errors.New("chain is not bitcoin") + } + + app, err := zctx.FromContext(ctx) + if err != nil { + return nil, err + } + + cfg, found := app.Config().GetBTCConfig(chain.ID()) + if !found { + return nil, errors.Wrap(errSkipChain, "unable to find btc config") + } + + rpcClient, err := rpc.NewRPCClient(cfg) + if err != nil { + return nil, errors.Wrap(err, "unable to create rpc client") + } + + var ( + rawChain = chain.RawChain() + rawChainParams = chain.Params() + ) + + dbName := btcDatabaseFileName(*rawChain) + + database, err := db.NewFromSqlite(oc.deps.DBPath, dbName, true) + if err != nil { + return nil, errors.Wrapf(err, "unable to open database %s", dbName) + } + + // TODO extract base observer + // TODO extract base signer + // https://github.com/zeta-chain/node/issues/3331 + + observer, err := btcobserver.NewObserver( + *rawChain, + rpcClient, + *rawChainParams, + oc.deps.Zetacore, + oc.deps.TSS, + database, + oc.logger.base, + oc.deps.Telemetry, + ) + if err != nil { + return nil, errors.Wrap(err, "unable to create observer") + } + + signer, err := btcsigner.NewSigner(*rawChain, oc.deps.TSS, oc.logger.base, cfg) + if err != nil { + return nil, errors.Wrap(err, "unable to create signer") + } + + return bitcoin.New(oc.scheduler, observer, signer), nil +} diff --git a/zetaclient/orchestrator/v2_bootstrap_test.go b/zetaclient/orchestrator/v2_bootstrap_test.go new file mode 100644 index 0000000000..c1e64df23d --- /dev/null +++ b/zetaclient/orchestrator/v2_bootstrap_test.go @@ -0,0 +1,78 @@ +package orchestrator + +import ( + "testing" + "time" + + cometbfttypes "github.com/cometbft/cometbft/types" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/scheduler" + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/testutils/testrpc" +) + +func TestBootstrap(t *testing.T) { + t.Run("Bitcoin", func(t *testing.T) { + // ARRANGE + // Given orchestrator + ts := newTestSuite(t) + + // Given BTC client + btcServer, btcConfig := testrpc.NewBtcServer(t) + + ts.UpdateConfig(func(cfg *config.Config) { + cfg.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = btcConfig + }) + + mockBitcoinCalls(ts, btcServer) + + // ACT + // Start the orchestrator and wait for BTC observerSigner to bootstrap + require.NoError(t, ts.Start(ts.ctx)) + + // ASSERT + // Check that btc observerSigner is bootstrapped. + check := func() bool { + ts.V2.mu.RLock() + defer ts.V2.mu.RUnlock() + + _, ok := ts.V2.chains[chains.BitcoinMainnet.ChainId] + return ok + } + + assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) + + // Check that the scheduler has some tasks for this + tasksHaveGroup(t, ts.scheduler.Tasks(), "btc:8332") + + assert.Contains(t, ts.Log.String(), `"chain":8332,"chain_network":"btc","message":"Added observer-signer"`) + }) +} + +func tasksHaveGroup(t *testing.T, tasks map[uuid.UUID]*scheduler.Task, group string) { + var found bool + for _, task := range tasks { + // t.Logf("Task %s:%s", task.Group(), task.Name()) + if !found && task.Group() == scheduler.Group(group) { + found = true + } + } + + assert.True(t, found, "Group %s not found in tasks", group) +} + +func mockBitcoinCalls(ts *testSuite, client *testrpc.BtcServer) { + client.SetBlockCount(100) + + blockChan := make(chan cometbfttypes.EventDataNewBlock) + ts.zetacore.On("NewBlockSubscriber", mock.Anything).Return(blockChan, nil) + + ts.zetacore.On("GetInboundTrackersForChain", mock.Anything, mock.Anything).Return(nil, nil) + ts.zetacore.On("GetPendingNoncesByChain", mock.Anything, mock.Anything).Return(observertypes.PendingNonces{}, nil) + ts.zetacore.On("GetAllOutboundTrackerByChain", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) +} diff --git a/zetaclient/orchestrator/v2_orchestrator.go b/zetaclient/orchestrator/v2_orchestrator.go new file mode 100644 index 0000000000..4f970dddb7 --- /dev/null +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -0,0 +1,307 @@ +package orchestrator + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/scheduler" + "github.com/zeta-chain/node/pkg/ticker" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/logs" + "github.com/zeta-chain/node/zetaclient/metrics" +) + +// V2 represents the orchestrator V2 while they co-exist with Orchestrator. +type V2 struct { + deps *Dependencies + scheduler *scheduler.Scheduler + + chains map[int64]ObserverSigner + mu sync.RWMutex + + logger loggers +} + +type loggers struct { + zerolog.Logger + sampled zerolog.Logger + base base.Logger +} + +const schedulerGroup = scheduler.Group("orchestrator") + +type ObserverSigner interface { + Chain() chains.Chain + Start(ctx context.Context) error + Stop() +} + +type Dependencies struct { + Zetacore interfaces.ZetacoreClient + TSS interfaces.TSSSigner + DBPath string + Telemetry *metrics.TelemetryServer +} + +func NewV2(scheduler *scheduler.Scheduler, deps *Dependencies, logger base.Logger) (*V2, error) { + if err := validateConstructor(scheduler, deps); err != nil { + return nil, errors.Wrap(err, "invalid args") + } + + return &V2{ + scheduler: scheduler, + deps: deps, + chains: make(map[int64]ObserverSigner), + logger: newLoggers(logger), + }, nil +} + +func (oc *V2) Start(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + // syntax sugar + opts := func(name string, opts ...scheduler.Opt) []scheduler.Opt { + return append(opts, scheduler.GroupName(schedulerGroup), scheduler.Name(name)) + } + + contextInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(app.Config().ConfigUpdateTicker) + }) + + // every other block + syncInterval := scheduler.Interval(2 * constant.ZetaBlockTime) + + oc.scheduler.Register(ctx, oc.UpdateContext, opts("update_context", contextInterval)...) + oc.scheduler.Register(ctx, oc.SyncChains, opts("sync_chains", syncInterval)...) + + return nil +} + +func (oc *V2) Stop() { + oc.logger.Info().Msg("Stopping orchestrator") + + // stops *all* scheduler tasks + oc.scheduler.Stop() +} + +func (oc *V2) UpdateContext(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + err = UpdateAppContext(ctx, app, oc.deps.Zetacore, oc.logger.Logger) + + switch { + case errors.Is(err, ErrUpgradeRequired): + const msg = "Upgrade detected. Kill the process, " + + "replace the binary with upgraded version, and restart zetaclientd" + + oc.logger.Warn().Str("upgrade", err.Error()).Msg(msg) + + // stop the orchestrator + go oc.Stop() + + return nil + case err != nil: + return errors.Wrap(err, "unable to update app context") + default: + return nil + } +} + +var errSkipChain = errors.New("skip chain") + +func (oc *V2) SyncChains(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + var ( + added, removed int + presentChainIDs = make([]int64, 0) + ) + + for _, chain := range app.ListChains() { + // skip zetachain + if chain.IsZeta() { + continue + } + + presentChainIDs = append(presentChainIDs, chain.ID()) + + // skip existing chain + if oc.hasChain(chain.ID()) { + continue + } + + var observerSigner ObserverSigner + + switch { + case chain.IsBitcoin(): + observerSigner, err = oc.bootstrapBitcoin(ctx, chain) + case chain.IsEVM(): + // TODO + // https://github.com/zeta-chain/node/issues/3302 + continue + case chain.IsSolana(): + // TODO + // https://github.com/zeta-chain/node/issues/3301 + continue + case chain.IsTON(): + // TODO + // https://github.com/zeta-chain/node/issues/3300 + continue + } + + switch { + case errors.Is(errSkipChain, err): + // TODO use throttled logger instead of sampled one. + // https://github.com/zeta-chain/node/issues/3336 + oc.logger.sampled.Warn().Err(err).Fields(chain.LogFields()).Msg("Skipping observer-signer") + continue + case err != nil: + oc.logger.Error().Err(err).Fields(chain.LogFields()).Msg("Failed to bootstrap observer-signer") + continue + case observerSigner == nil: + // should not happen + oc.logger.Error().Fields(chain.LogFields()).Msg("Nil observer-signer") + continue + } + + if err = observerSigner.Start(ctx); err != nil { + oc.logger.Error().Err(err).Fields(chain.LogFields()).Msg("Failed to start observer-signer") + continue + } + + oc.addChain(observerSigner) + added++ + } + + removed = oc.removeMissingChains(presentChainIDs) + + if (added + removed) > 0 { + oc.logger.Info(). + Int("chains.added", added). + Int("chains.removed", removed). + Msg("Synced observer-signers") + } + + return nil +} + +func (oc *V2) hasChain(chainID int64) bool { + oc.mu.RLock() + defer oc.mu.RUnlock() + + _, ok := oc.chains[chainID] + return ok +} + +func (oc *V2) chainIDs() []int64 { + oc.mu.RLock() + defer oc.mu.RUnlock() + + ids := make([]int64, 0, len(oc.chains)) + for chainID := range oc.chains { + ids = append(ids, chainID) + } + + return ids +} + +func (oc *V2) addChain(observerSigner ObserverSigner) { + chain := observerSigner.Chain() + + oc.mu.Lock() + defer oc.mu.Unlock() + + // noop + if _, ok := oc.chains[chain.ChainId]; ok { + return + } + + oc.chains[chain.ChainId] = observerSigner + oc.logger.Info().Fields(chain.LogFields()).Msg("Added observer-signer") +} + +func (oc *V2) removeChain(chainID int64) { + // noop, should not happen + if !oc.hasChain(chainID) { + return + } + + // blocking call + oc.chains[chainID].Stop() + + oc.mu.Lock() + delete(oc.chains, chainID) + oc.mu.Unlock() + + oc.logger.Info().Int64(logs.FieldChain, chainID).Msg("Removed observer-signer") +} + +// removeMissingChains stops and deletes chains +// that are not present in the list of chainIDs (e.g. after governance proposal) +func (oc *V2) removeMissingChains(presentChainIDs []int64) int { + presentChainsSet := make(map[int64]struct{}) + for _, chainID := range presentChainIDs { + presentChainsSet[chainID] = struct{}{} + } + + existingIDs := oc.chainIDs() + removed := 0 + + for _, chainID := range existingIDs { + if _, ok := presentChainsSet[chainID]; ok { + // all good, chain is present + continue + } + + oc.removeChain(chainID) + removed++ + } + + return removed +} + +func validateConstructor(s *scheduler.Scheduler, dep *Dependencies) error { + switch { + case s == nil: + return errors.New("scheduler is nil") + case dep == nil: + return errors.New("dependencies are nil") + case dep.Zetacore == nil: + return errors.New("zetacore is nil") + case dep.TSS == nil: + return errors.New("tss is nil") + case dep.Telemetry == nil: + return errors.New("telemetry is nil") + case dep.DBPath == "": + return errors.New("db path is empty") + } + + return nil +} + +func newLoggers(baseLogger base.Logger) loggers { + std := baseLogger.Std.With().Str(logs.FieldModule, "orchestrator").Logger() + + return loggers{ + Logger: std, + sampled: std.Sample(&zerolog.BasicSampler{N: 10}), + base: baseLogger, + } +} diff --git a/zetaclient/orchestrator/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go new file mode 100644 index 0000000000..c10c11481f --- /dev/null +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -0,0 +1,267 @@ +package orchestrator + +import ( + "context" + "reflect" + "sync" + "testing" + "time" + "unsafe" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/scheduler" + "github.com/zeta-chain/node/testutil/sample" + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/config" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/db" + "github.com/zeta-chain/node/zetaclient/metrics" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "github.com/zeta-chain/node/zetaclient/testutils/testlog" + "github.com/zeta-chain/node/zetaclient/testutils/testrpc" +) + +func TestOrchestratorV2(t *testing.T) { + t.Run("updates app context", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // ACT #1 + // Start orchestrator + err := ts.Start(ts.ctx) + + // Mimic zetacore update + ts.MockChainParams(chains.Ethereum, mocks.MockChainParams(chains.Ethereum.ChainId, 100)) + + // ASSERT #1 + require.NoError(t, err) + + // Check that eventually appContext would contain only desired chains + check := func() bool { + list := ts.appContext.ListChains() + return len(list) == 1 && chainsContain(list, chains.Ethereum.ChainId) + } + + assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) + + assert.Contains(t, ts.Log.String(), "Chain list changed at the runtime!") + assert.Contains(t, ts.Log.String(), `"chains.new":[1]`) + + // ACT #2 + // Mimic zetacore update that adds bitcoin chain with chain params + ts.MockChainParams( + chains.Ethereum, + mocks.MockChainParams(chains.Ethereum.ChainId, 100), + chains.BitcoinMainnet, + mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 100), + ) + + check = func() bool { + list := ts.appContext.ListChains() + return len(list) == 2 && chainsContain(list, chains.Ethereum.ChainId, chains.BitcoinMainnet.ChainId) + } + + assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) + + assert.Contains(t, ts.Log.String(), `"chains.new":[1,8332],"message":"Chain list changed at the runtime!"`) + }) +} + +type testSuite struct { + *V2 + *testlog.Log + + t *testing.T + + ctx context.Context + appContext *zctx.AppContext + + chains []chains.Chain + chainParams []*observertypes.ChainParams + + zetacore *mocks.ZetacoreClient + scheduler *scheduler.Scheduler + tss *mocks.TSS + + mu sync.Mutex +} + +var defaultChainsWithParams = []any{ + chains.Ethereum, + chains.BitcoinMainnet, + chains.SolanaMainnet, + chains.TONMainnet, + + mocks.MockChainParams(chains.Ethereum.ChainId, 100), + mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 3), + mocks.MockChainParams(chains.SolanaMainnet.ChainId, 10), + mocks.MockChainParams(chains.TONMainnet.ChainId, 1), +} + +func newTestSuite(t *testing.T) *testSuite { + logger := testlog.New(t) + baseLogger := base.Logger{ + Std: logger.Logger, + Compliance: logger.Logger, + } + + testrpc.NewBtcServer(t) + + chainList, chainParams := parseChainsWithParams(t, defaultChainsWithParams...) + + ctx, appCtx := newAppContext(t, logger.Logger, chainList, chainParams) + + ctx, cancel := context.WithCancel(ctx) + t.Cleanup(cancel) + + // Services + var ( + schedulerService = scheduler.New(logger.Logger) + zetacore = mocks.NewZetacoreClient(t) + tss = mocks.NewTSS(t) + ) + + deps := &Dependencies{ + Zetacore: zetacore, + TSS: tss, + DBPath: db.SqliteInMemory, + Telemetry: metrics.NewTelemetryServer(), + } + + v2, err := NewV2(schedulerService, deps, baseLogger) + require.NoError(t, err) + + ts := &testSuite{ + V2: v2, + Log: logger, + + t: t, + + ctx: ctx, + appContext: appCtx, + + chains: chainList, + chainParams: chainParams, + + scheduler: schedulerService, + zetacore: zetacore, + tss: tss, + } + + // Mock basic zetacore methods + zetacore.On("GetBlockHeight", mock.Anything).Return(int64(123), nil).Maybe() + zetacore.On("GetUpgradePlan", mock.Anything).Return(nil, nil).Maybe() + zetacore.On("GetAdditionalChains", mock.Anything).Return(nil, nil).Maybe() + zetacore.On("GetCrosschainFlags", mock.Anything).Return(appCtx.GetCrossChainFlags(), nil).Maybe() + + // Mock chain-related methods as dynamic getters + zetacore.On("GetSupportedChains", mock.Anything).Return(ts.getSupportedChains).Maybe() + zetacore.On("GetChainParams", mock.Anything).Return(ts.getChainParams).Maybe() + + t.Cleanup(ts.Stop) + + return ts +} + +func (ts *testSuite) MockChainParams(newValues ...any) { + chainList, chainParams := parseChainsWithParams(ts.t, newValues...) + + ts.mu.Lock() + defer ts.mu.Unlock() + + ts.chains = chainList + ts.chainParams = chainParams +} + +func (ts *testSuite) getSupportedChains(_ context.Context) ([]chains.Chain, error) { + ts.mu.Lock() + defer ts.mu.Unlock() + return ts.chains, nil +} + +func (ts *testSuite) getChainParams(_ context.Context) ([]*observertypes.ChainParams, error) { + ts.mu.Lock() + defer ts.mu.Unlock() + return ts.chainParams, nil +} + +// UpdateConfig updates "global" config.Config for test suite. +func (ts *testSuite) UpdateConfig(fn func(cfg *config.Config)) { + cfg := ts.appContext.Config() + fn(&cfg) + + // The config is sealed i.e. we can't alter it after starting zetaclientd. + // But for test purposes we use `reflect` to mimic + // that it was set by the validator *before* starting the app. + field := reflect.ValueOf(ts.appContext).Elem().FieldByName("config") + ptr := unsafe.Pointer(field.UnsafeAddr()) + configPtr := (*config.Config)(ptr) + + *configPtr = cfg +} + +func newAppContext( + t *testing.T, + logger zerolog.Logger, + chainList []chains.Chain, + chainParams []*observertypes.ChainParams, +) (context.Context, *zctx.AppContext) { + // Mock config + cfg := config.New(false) + + cfg.ConfigUpdateTicker = 1 + + for _, c := range chainList { + switch { + case chains.IsEVMChain(c.ChainId, nil): + cfg.EVMChainConfigs[c.ChainId] = config.EVMConfig{Endpoint: "localhost"} + case chains.IsBitcoinChain(c.ChainId, nil): + cfg.BTCChainConfigs[c.ChainId] = config.BTCConfig{RPCHost: "localhost"} + case chains.IsSolanaChain(c.ChainId, nil): + cfg.SolanaConfig = config.SolanaConfig{Endpoint: "localhost"} + case chains.IsTONChain(c.ChainId, nil): + cfg.TONConfig = config.TONConfig{LiteClientConfigURL: "localhost"} + default: + t.Fatalf("create app context: unsupported chain %d", c.ChainId) + } + } + + // chain params + params := map[int64]*observertypes.ChainParams{} + for i := range chainParams { + cp := chainParams[i] + params[cp.ChainId] = cp + } + + // new AppContext + appContext := zctx.New(cfg, nil, logger) + + ccFlags := sample.CrosschainFlags() + + err := appContext.Update(chainList, nil, params, *ccFlags) + require.NoError(t, err, "failed to update app context") + + ctx := zctx.WithAppContext(context.Background(), appContext) + + return ctx, appContext +} + +func chainsContain(list []zctx.Chain, ids ...int64) bool { + set := make(map[int64]struct{}, len(list)) + for _, chain := range list { + set[chain.ID()] = struct{}{} + } + + for _, chainID := range ids { + if _, found := set[chainID]; !found { + return false + } + } + + return true +} diff --git a/zetaclient/testutils/mocks/chain_params.go b/zetaclient/testutils/mocks/chain_params.go index 15568c6e61..c421097c5a 100644 --- a/zetaclient/testutils/mocks/chain_params.go +++ b/zetaclient/testutils/mocks/chain_params.go @@ -32,7 +32,7 @@ func MockChainParams(chainID int64, confirmation uint64) observertypes.ChainPara Erc20CustodyContractAddress: erc20CustodyAddr, InboundTicker: 12, OutboundTicker: 15, - WatchUtxoTicker: 0, + WatchUtxoTicker: 1, GasPriceTicker: 30, OutboundScheduleInterval: 30, OutboundScheduleLookahead: 60, diff --git a/zetaclient/testutils/testlog/log.go b/zetaclient/testutils/testlog/log.go new file mode 100644 index 0000000000..b3d9555a90 --- /dev/null +++ b/zetaclient/testutils/testlog/log.go @@ -0,0 +1,50 @@ +package testlog + +import ( + "bytes" + "io" + "sync" + "testing" + + "github.com/rs/zerolog" +) + +type Log struct { + zerolog.Logger + buf *concurrentBytesBuffer +} + +type concurrentBytesBuffer struct { + buf *bytes.Buffer + mu sync.RWMutex +} + +// New creates a new Log instance with a buffer and a test writer. +func New(t *testing.T) *Log { + buf := &concurrentBytesBuffer{ + buf: &bytes.Buffer{}, + mu: sync.RWMutex{}, + } + + log := zerolog.New(io.MultiWriter(zerolog.NewTestWriter(t), buf)) + + return &Log{Logger: log, buf: buf} +} + +func (log *Log) String() string { + return log.buf.string() +} + +func (b *concurrentBytesBuffer) Write(p []byte) (n int, err error) { + b.mu.Lock() + defer b.mu.Unlock() + + return b.buf.Write(p) +} + +func (b *concurrentBytesBuffer) string() string { + b.mu.RLock() + defer b.mu.RUnlock() + + return b.buf.String() +} diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index 7a8391ff89..4938376fff 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -23,6 +23,7 @@ import ( // KeySigner signs messages using TSS (subset of go-tss) type KeySigner interface { KeySign(req keysign.Request) (keysign.Response, error) + Stop() } // Zetacore zeta core client. @@ -225,6 +226,12 @@ func (s *Service) SignBatch( return sigs, nil } +func (s *Service) Stop() { + s.logger.Info().Msg("Stopping TSS service") + s.tss.Stop() + s.logger.Info().Msg("TSS service stopped") +} + var ( signLabelsSuccess = prometheus.Labels{"result": "success"} signLabelsError = prometheus.Labels{"result": "error"} diff --git a/zetaclient/tss/service_test.go b/zetaclient/tss/service_test.go index e0ccde6954..87e3c86db7 100644 --- a/zetaclient/tss/service_test.go +++ b/zetaclient/tss/service_test.go @@ -153,6 +153,8 @@ func newKeySignerMock(t *testing.T) *keySignerMock { } } +func (*keySignerMock) Stop() { return } + func (m *keySignerMock) PubKeyBech32() string { cosmosPrivateKey := &secp256k1.PrivKey{Key: m.privateKey.D.Bytes()} pk := cosmosPrivateKey.PubKey() diff --git a/zetaclient/zetacore/client.go b/zetaclient/zetacore/client.go index df5b6dbeb6..a883aca855 100644 --- a/zetaclient/zetacore/client.go +++ b/zetaclient/zetacore/client.go @@ -8,6 +8,7 @@ import ( cometbftrpc "github.com/cometbft/cometbft/rpc/client" cometbfthttp "github.com/cometbft/cometbft/rpc/client/http" + ctypes "github.com/cometbft/cometbft/types" cosmosclient "github.com/cosmos/cosmos-sdk/client" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/pkg/errors" @@ -19,6 +20,7 @@ import ( "github.com/zeta-chain/node/app" "github.com/zeta-chain/node/pkg/authz" "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/fanout" zetacorerpc "github.com/zeta-chain/node/pkg/rpc" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/config" @@ -47,6 +49,9 @@ type Client struct { chainID string chain chains.Chain + // blocksFanout that receives new block events from Zetacore via websockets + blocksFanout *fanout.FanOut[ctypes.EventDataNewBlock] + mu sync.RWMutex } diff --git a/zetaclient/zetacore/client_subscriptions.go b/zetaclient/zetacore/client_subscriptions.go index cb4229b31b..7bf5ea1c25 100644 --- a/zetaclient/zetacore/client_subscriptions.go +++ b/zetaclient/zetacore/client_subscriptions.go @@ -3,33 +3,97 @@ package zetacore import ( "context" - cometbfttypes "github.com/cometbft/cometbft/types" + "cosmossdk.io/errors" + ctypes "github.com/cometbft/cometbft/types" + + "github.com/zeta-chain/node/pkg/fanout" ) -// NewBlockSubscriber subscribes to cometbft new block events -func (c *Client) NewBlockSubscriber(ctx context.Context) (chan cometbfttypes.EventDataNewBlock, error) { - rawBlockEventChan, err := c.cometBFTClient.Subscribe(ctx, "", cometbfttypes.EventQueryNewBlock.String()) +// NewBlockSubscriber subscribes to comet bft new block events. +// Subscribes share the same websocket connection but their channels are independent (fanout) +func (c *Client) NewBlockSubscriber(ctx context.Context) (chan ctypes.EventDataNewBlock, error) { + blockSubscriber, err := c.resolveBlockSubscriber() if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to resolve block subscriber") } - blockEventChan := make(chan cometbfttypes.EventDataNewBlock) + // we need a "proxy" chan instead of directly returning blockSubscriber.Add() + // to support context cancellation + blocksChan := make(chan ctypes.EventDataNewBlock) go func() { + consumer := blockSubscriber.Add() + for { select { case <-ctx.Done(): + // fixme: MEMORY LEAK: this might be dangerous because the consumer is not closed. + // Fanout will spawn "zombie" goroutines to push to the chan, but nobody is reading from it, + // Will be addressed in future orchestrator V2 PRs (not urgent as of now) return - case event := <-rawBlockEventChan: - newBlockEvent, ok := event.Data.(cometbfttypes.EventDataNewBlock) - if !ok { - c.logger.Error().Msgf("expecting new block event, got %T", event.Data) - continue - } - blockEventChan <- newBlockEvent + case block := <-consumer: + blocksChan <- block } } }() - return blockEventChan, nil + return blocksChan, nil +} + +// resolveBlockSubscriber returns the block subscriber channel +// or subscribes to it for the first time. +func (c *Client) resolveBlockSubscriber() (*fanout.FanOut[ctypes.EventDataNewBlock], error) { + // noop + if blocksFanout := c.blockFanOutThreadSafe(); blocksFanout != nil { + c.logger.Info().Msg("Resolved existing block subscriber") + return blocksFanout, nil + } + + // we need this lock to prevent 2 Subscribe calls at the same time + c.mu.Lock() + defer c.mu.Unlock() + + c.logger.Info().Msg("Subscribing to block events") + + // Subscribe to comet bft events + eventsChan, err := c.cometBFTClient.Subscribe(context.Background(), "", ctypes.EventQueryNewBlock.String()) + if err != nil { + return nil, errors.Wrap(err, "unable to subscribe to new block events") + } + + c.logger.Info().Msg("Subscribed to block events") + + // Create block chan + blockChan := make(chan ctypes.EventDataNewBlock) + + // Spin up a pipeline to forward block events to the blockChan + go func() { + for event := range eventsChan { + newBlockEvent, ok := event.Data.(ctypes.EventDataNewBlock) + if !ok { + c.logger.Error().Msgf("expecting new block event, got %T", event.Data) + continue + } + + c.logger.Info().Int64("height", newBlockEvent.Block.Height).Msg("Received new block event") + + blockChan <- newBlockEvent + } + }() + + // Create a fanout + // It allows a "global" chan (i.e. blockChan) to stream to multiple consumers independently. + fo := fanout.New[ctypes.EventDataNewBlock](blockChan, fanout.DefaultBuffer) + fo.Start() + + c.blocksFanout = fo + + return fo, nil +} + +func (c *Client) blockFanOutThreadSafe() *fanout.FanOut[ctypes.EventDataNewBlock] { + c.mu.Lock() + defer c.mu.Unlock() + + return c.blocksFanout }