From 04fa0916c1c54fae5bdae1e2a647bf40490a71d2 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 24 Dec 2024 17:44:02 +0100 Subject: [PATCH 01/26] Minor fixes --- pkg/scheduler/opts.go | 5 ++++- zetaclient/context/chain.go | 2 ++ zetaclient/context/context.go | 2 +- zetaclient/maintenance/shutdown_listener_test.go | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/scheduler/opts.go b/pkg/scheduler/opts.go index 8e5d54e370..1da0ada4e8 100644 --- a/pkg/scheduler/opts.go +++ b/pkg/scheduler/opts.go @@ -36,7 +36,10 @@ func Skipper(skipper func() bool) Opt { // IntervalUpdater sets interval updater function. func IntervalUpdater(intervalUpdater func() time.Duration) Opt { - return func(_ *Task, opts *taskOpts) { opts.intervalUpdater = intervalUpdater } + return func(_ *Task, opts *taskOpts) { + opts.interval = intervalUpdater() + opts.intervalUpdater = intervalUpdater + } } // BlockTicker makes Task to listen for new zeta blocks diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index 4f35953ce0..ffc3d2a082 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()) 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 2b2c9128d1..ea52b90ea3 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: } } From 0c46e64cb44dc5d2ad95a59857e60abbc10e1fe2 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 24 Dec 2024 17:44:57 +0100 Subject: [PATCH 02/26] Add orchestrator V2. Move context updater to v2 --- .../{bootstap_test.go => bootstrap_test.go} | 0 zetaclient/orchestrator/contextupdater.go | 43 ---- zetaclient/orchestrator/orchestrator.go | 1 - zetaclient/orchestrator/v2_orchestrator.go | 98 ++++++++ .../orchestrator/v2_orchestrator_test.go | 223 ++++++++++++++++++ 5 files changed, 321 insertions(+), 44 deletions(-) rename zetaclient/orchestrator/{bootstap_test.go => bootstrap_test.go} (100%) create mode 100644 zetaclient/orchestrator/v2_orchestrator.go create mode 100644 zetaclient/orchestrator/v2_orchestrator_test.go diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstrap_test.go similarity index 100% rename from zetaclient/orchestrator/bootstap_test.go rename to zetaclient/orchestrator/bootstrap_test.go 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 6e818329b1..253a34e223 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -134,7 +134,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)) return nil } diff --git a/zetaclient/orchestrator/v2_orchestrator.go b/zetaclient/orchestrator/v2_orchestrator.go new file mode 100644 index 0000000000..2dc285dd50 --- /dev/null +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -0,0 +1,98 @@ +package orchestrator + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/pkg/scheduler" + "github.com/zeta-chain/node/pkg/ticker" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/logs" +) + +// V2 represents the orchestrator V2 while they co-exist with Orchestrator. +type V2 struct { + zetacore interfaces.ZetacoreClient + + scheduler *scheduler.Scheduler + + chains map[int64]ObserverSigner + + logger zerolog.Logger +} + +const schedulerGroup = scheduler.Group("orchestrator") + +type ObserverSigner interface { + Start(ctx context.Context) error + Stop() +} + +func NewV2( + zetacore interfaces.ZetacoreClient, + scheduler *scheduler.Scheduler, + logger zerolog.Logger, +) *V2 { + return &V2{ + zetacore: zetacore, + scheduler: scheduler, + chains: make(map[int64]ObserverSigner), + logger: logger.With().Str(logs.FieldModule, "orchestrator").Logger(), + } +} + +func (oc *V2) Start(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + contextUpdaterOpts := []scheduler.Opt{ + scheduler.GroupName(schedulerGroup), + scheduler.Name("update_context"), + scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(app.Config().ConfigUpdateTicker) + }), + } + + oc.scheduler.Register(ctx, oc.UpdateContext, contextUpdaterOpts...) + + 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.zetacore, oc.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 + } +} diff --git a/zetaclient/orchestrator/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go new file mode 100644 index 0000000000..b12e6fbc1b --- /dev/null +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -0,0 +1,223 @@ +package orchestrator_test + +import ( + "bytes" + "context" + "io" + "sync" + "testing" + "time" + + "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/config" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/orchestrator" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" +) + +func TestV2(t *testing.T) { + t.Run("updates app context", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // ACT + err := ts.Start(ts.ctx) + + // Mimic zetacore update + ts.MockChainParams(chains.Ethereum, mocks.MockChainParams(chains.Ethereum.ChainId, 100)) + + // ASSERT + require.NoError(t, err) + + // Check that eventually appContext would contain only desired chains + check := func() bool { + listChains := ts.appContext.ListChains() + if len(listChains) != 1 { + return false + } + + return listChains[0].ID() == chains.Ethereum.ChainId + } + + assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) + + assert.Contains(t, ts.logBuffer.String(), "Chain list changed at the runtime!") + assert.Contains(t, ts.logBuffer.String(), `"chains.new":[1]`) + }) +} + +type testSuite struct { + *orchestrator.V2 + + t *testing.T + + ctx context.Context + appContext *zctx.AppContext + + chains []chains.Chain + chainParams []*observertypes.ChainParams + + zetacore *mocks.ZetacoreClient + scheduler *scheduler.Scheduler + + logBuffer *bytes.Buffer + logger zerolog.Logger + + 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 { + var ( + logBuffer = &bytes.Buffer{} + logger = zerolog.New(io.MultiWriter(zerolog.NewTestWriter(t), logBuffer)) + + chainList, chainParams = parseChainsWithParams(t, defaultChainsWithParams...) + ctx, appCtx = newAppContext(t, logger, chainList, chainParams) + + schedulerService = scheduler.New(logger) + zetacore = mocks.NewZetacoreClient(t) + ) + + ts := &testSuite{ + V2: orchestrator.NewV2(zetacore, schedulerService, logger), + + t: t, + + ctx: ctx, + appContext: appCtx, + + chains: chainList, + chainParams: chainParams, + + scheduler: schedulerService, + zetacore: zetacore, + + logBuffer: logBuffer, + logger: logger, + } + + // 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) + zetacore.On("GetChainParams", mock.Anything).Return(ts.getChainParams) + + 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 +} + +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 parseChainsWithParams(t *testing.T, chainsOrParams ...any) ([]chains.Chain, []*observertypes.ChainParams) { + var ( + supportedChains = make([]chains.Chain, 0, len(chainsOrParams)) + obsParams = make([]*observertypes.ChainParams, 0, len(chainsOrParams)) + ) + + for _, something := range chainsOrParams { + switch tt := something.(type) { + case *chains.Chain: + supportedChains = append(supportedChains, *tt) + case chains.Chain: + supportedChains = append(supportedChains, tt) + case *observertypes.ChainParams: + obsParams = append(obsParams, tt) + case observertypes.ChainParams: + obsParams = append(obsParams, &tt) + default: + t.Errorf("parse chains and params: unsupported type %T (%+v)", tt, tt) + } + } + + return supportedChains, obsParams +} From 63961eb44420b3085fedf2177c56fe53db2d6997 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:51:46 +0100 Subject: [PATCH 03/26] Fix orchestrator_v2 test cases --- go.mod | 6 +-- .../orchestrator/v2_orchestrator_test.go | 45 ++++++------------- 2 files changed, 16 insertions(+), 35 deletions(-) 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/zetaclient/orchestrator/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go index b12e6fbc1b..d055e88eef 100644 --- a/zetaclient/orchestrator/v2_orchestrator_test.go +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -1,4 +1,4 @@ -package orchestrator_test +package orchestrator import ( "bytes" @@ -18,11 +18,16 @@ import ( observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" - "github.com/zeta-chain/node/zetaclient/orchestrator" "github.com/zeta-chain/node/zetaclient/testutils/mocks" ) -func TestV2(t *testing.T) { +// [x] todo: fix UNEXPECTED test failure (exit code 1) +// [ ] log helper for debugging (zerolog) +// [ ] todo: add v2 to start.go +// [ ] todo: run e2e tests. +// [ ] todo: add btc observer&signer + +func TestOrchestratorV2(t *testing.T) { t.Run("updates app context", func(t *testing.T) { // ARRANGE ts := newTestSuite(t) @@ -54,7 +59,7 @@ func TestV2(t *testing.T) { } type testSuite struct { - *orchestrator.V2 + *V2 t *testing.T @@ -98,7 +103,7 @@ func newTestSuite(t *testing.T) *testSuite { ) ts := &testSuite{ - V2: orchestrator.NewV2(zetacore, schedulerService, logger), + V2: NewV2(zetacore, schedulerService, logger), t: t, @@ -121,9 +126,9 @@ func newTestSuite(t *testing.T) *testSuite { 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) - zetacore.On("GetChainParams", mock.Anything).Return(ts.getChainParams) + //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) @@ -197,27 +202,3 @@ func newAppContext( return ctx, appContext } - -func parseChainsWithParams(t *testing.T, chainsOrParams ...any) ([]chains.Chain, []*observertypes.ChainParams) { - var ( - supportedChains = make([]chains.Chain, 0, len(chainsOrParams)) - obsParams = make([]*observertypes.ChainParams, 0, len(chainsOrParams)) - ) - - for _, something := range chainsOrParams { - switch tt := something.(type) { - case *chains.Chain: - supportedChains = append(supportedChains, *tt) - case chains.Chain: - supportedChains = append(supportedChains, tt) - case *observertypes.ChainParams: - obsParams = append(obsParams, tt) - case observertypes.ChainParams: - obsParams = append(obsParams, &tt) - default: - t.Errorf("parse chains and params: unsupported type %T (%+v)", tt, tt) - } - } - - return supportedChains, obsParams -} From df214517079876d38dde84ea91624503399427cb Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:31:11 +0100 Subject: [PATCH 04/26] Fix flaky test cases during concurrent runs (spoiler: goroutines) --- pkg/ticker/ticker.go | 11 +++- zetaclient/orchestrator/bootstrap_test.go | 13 +++++ .../orchestrator/v2_orchestrator_test.go | 30 ++++------- zetaclient/testutils/testlog/log.go | 50 +++++++++++++++++++ 4 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 zetaclient/testutils/testlog/log.go diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index 9ec0d4cb06..3d1662a9c0 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() @@ -219,6 +219,13 @@ func (t *Ticker) setStopState() { 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/orchestrator/bootstrap_test.go b/zetaclient/orchestrator/bootstrap_test.go index 71b3ec744b..5ccfd39297 100644 --- a/zetaclient/orchestrator/bootstrap_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" @@ -201,6 +202,8 @@ func TestCreateChainObserverMap(t *testing.T) { dbPath = db.SqliteInMemory ) + mockZetacore(client) + t.Run("CreateChainObserverMap", func(t *testing.T) { // ARRANGE // Given a BTC server @@ -512,3 +515,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/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go index d055e88eef..8987a7f022 100644 --- a/zetaclient/orchestrator/v2_orchestrator_test.go +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -1,9 +1,7 @@ package orchestrator import ( - "bytes" "context" - "io" "sync" "testing" "time" @@ -19,14 +17,9 @@ import ( "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "github.com/zeta-chain/node/zetaclient/testutils/testlog" ) -// [x] todo: fix UNEXPECTED test failure (exit code 1) -// [ ] log helper for debugging (zerolog) -// [ ] todo: add v2 to start.go -// [ ] todo: run e2e tests. -// [ ] todo: add btc observer&signer - func TestOrchestratorV2(t *testing.T) { t.Run("updates app context", func(t *testing.T) { // ARRANGE @@ -53,13 +46,14 @@ func TestOrchestratorV2(t *testing.T) { assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) - assert.Contains(t, ts.logBuffer.String(), "Chain list changed at the runtime!") - assert.Contains(t, ts.logBuffer.String(), `"chains.new":[1]`) + assert.Contains(t, ts.Log.String(), "Chain list changed at the runtime!") + assert.Contains(t, ts.Log.String(), `"chains.new":[1]`) }) } type testSuite struct { *V2 + *testlog.Log t *testing.T @@ -72,9 +66,6 @@ type testSuite struct { zetacore *mocks.ZetacoreClient scheduler *scheduler.Scheduler - logBuffer *bytes.Buffer - logger zerolog.Logger - mu sync.Mutex } @@ -92,18 +83,18 @@ var defaultChainsWithParams = []any{ func newTestSuite(t *testing.T) *testSuite { var ( - logBuffer = &bytes.Buffer{} - logger = zerolog.New(io.MultiWriter(zerolog.NewTestWriter(t), logBuffer)) + logger = testlog.New(t) chainList, chainParams = parseChainsWithParams(t, defaultChainsWithParams...) - ctx, appCtx = newAppContext(t, logger, chainList, chainParams) + ctx, appCtx = newAppContext(t, logger.Logger, chainList, chainParams) - schedulerService = scheduler.New(logger) + schedulerService = scheduler.New(logger.Logger) zetacore = mocks.NewZetacoreClient(t) ) ts := &testSuite{ - V2: NewV2(zetacore, schedulerService, logger), + V2: NewV2(zetacore, schedulerService, logger.Logger), + Log: logger, t: t, @@ -115,9 +106,6 @@ func newTestSuite(t *testing.T) *testSuite { scheduler: schedulerService, zetacore: zetacore, - - logBuffer: logBuffer, - logger: logger, } // Mock basic zetacore methods 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() +} From cac2f615cf4c9edfc752e27060985745c036c335 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:51:10 +0100 Subject: [PATCH 05/26] Add V2 to start.go --- cmd/zetaclientd/start.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 2a46dabdbd..9bd6435f2c 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" @@ -155,6 +156,11 @@ func Start(_ *cobra.Command, _ []string) error { // Start orchestrator with all observers and signers graceful.AddService(ctx, maestro) + maestroV2 := orchestrator.NewV2(zetacoreClient, scheduler.New(logger.Std), logger.Std) + + // Start orchestrator V2 with all observers and signers + graceful.AddService(ctx, maestroV2) + // Block current routine until a shutdown signal is received graceful.WaitForShutdown() From eff051718607b23d2e228815975a3b0da33ee0e3 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:56:41 +0100 Subject: [PATCH 06/26] chain sync skeleton --- cmd/zetaclientd/start.go | 17 +- zetaclient/context/chain.go | 8 + zetaclient/orchestrator/v2_bootstrap.go | 74 ++++++ zetaclient/orchestrator/v2_orchestrator.go | 239 ++++++++++++++++-- .../orchestrator/v2_orchestrator_test.go | 26 +- 5 files changed, 339 insertions(+), 25 deletions(-) create mode 100644 zetaclient/orchestrator/v2_bootstrap.go diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 9bd6435f2c..610f47fca6 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -153,12 +153,23 @@ 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) - maestroV2 := orchestrator.NewV2(zetacoreClient, scheduler.New(logger.Std), logger.Std) - - // Start orchestrator V2 with all observers and signers + // Start orchestrator V2 graceful.AddService(ctx, maestroV2) // Block current routine until a shutdown signal is received diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index ffc3d2a082..469f214309 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -11,6 +11,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" observer "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/logs" ) // ChainRegistry is a registry of supported chains @@ -145,6 +146,13 @@ func (c Chain) Name() string { return c.chainInfo.Name } +func (c Chain) LogFields() map[string]any { + return map[string]any{ + logs.FieldChain: c.ID(), + logs.FieldChainNetwork: c.chainInfo.Network.String(), + } +} + func (c Chain) Params() *observer.ChainParams { return c.observerParams } diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go new file mode 100644 index 0000000000..46e3c9c1a7 --- /dev/null +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -0,0 +1,74 @@ +package orchestrator + +import ( + "context" + + "github.com/pkg/errors" + + 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) (ObserverSigner, error) { + var ( + rawChain = chain.RawChain() + rawChainParams = chain.Params() + ) + + // 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") + } + + 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 + + _, 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") + } + + // todo extract base signer + + _, err = btcsigner.NewSigner(*rawChain, oc.deps.TSS, oc.logger.base, cfg) + if err != nil { + return nil, errors.Wrap(err, "unable to create signer") + } + + // todo observer-signer + + return nil, nil +} diff --git a/zetaclient/orchestrator/v2_orchestrator.go b/zetaclient/orchestrator/v2_orchestrator.go index 2dc285dd50..ec28eac40d 100644 --- a/zetaclient/orchestrator/v2_orchestrator.go +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -2,47 +2,65 @@ package orchestrator import ( "context" + "sync" "time" "github.com/pkg/errors" "github.com/rs/zerolog" + "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 { - zetacore interfaces.ZetacoreClient - + deps *Dependencies scheduler *scheduler.Scheduler chains map[int64]ObserverSigner + mu sync.RWMutex + + logger loggers +} - logger zerolog.Logger +type loggers struct { + zerolog.Logger + sampled zerolog.Logger + base base.Logger } const schedulerGroup = scheduler.Group("orchestrator") type ObserverSigner interface { Start(ctx context.Context) error + Chain() zctx.Chain Stop() } -func NewV2( - zetacore interfaces.ZetacoreClient, - scheduler *scheduler.Scheduler, - logger zerolog.Logger, -) *V2 { +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{ - zetacore: zetacore, scheduler: scheduler, + deps: deps, chains: make(map[int64]ObserverSigner), - logger: logger.With().Str(logs.FieldModule, "orchestrator").Logger(), - } + logger: newLoggers(logger), + }, nil } func (oc *V2) Start(ctx context.Context) error { @@ -51,15 +69,20 @@ func (oc *V2) Start(ctx context.Context) error { return err } - contextUpdaterOpts := []scheduler.Opt{ - scheduler.GroupName(schedulerGroup), - scheduler.Name("update_context"), - scheduler.IntervalUpdater(func() time.Duration { - return ticker.DurationFromUint64Seconds(app.Config().ConfigUpdateTicker) - }), + // syntax sugar + opts := func(name string, opts ...scheduler.Opt) []scheduler.Opt { + return append(opts, scheduler.GroupName(schedulerGroup), scheduler.Name(name)) } - oc.scheduler.Register(ctx, oc.UpdateContext, contextUpdaterOpts...) + 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 } @@ -77,7 +100,7 @@ func (oc *V2) UpdateContext(ctx context.Context) error { return err } - err = UpdateAppContext(ctx, app, oc.zetacore, oc.logger) + err = UpdateAppContext(ctx, app, oc.deps.Zetacore, oc.logger.Logger) switch { case errors.Is(err, ErrUpgradeRequired): @@ -96,3 +119,181 @@ func (oc *V2) UpdateContext(ctx context.Context) error { 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): + 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() + oc.chains[chain.ID()] = observerSigner + oc.mu.Unlock() + + 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 index 8987a7f022..88f7b448f5 100644 --- a/zetaclient/orchestrator/v2_orchestrator_test.go +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -14,8 +14,11 @@ import ( "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" ) @@ -65,6 +68,7 @@ type testSuite struct { zetacore *mocks.ZetacoreClient scheduler *scheduler.Scheduler + tss *mocks.TSS mu sync.Mutex } @@ -83,17 +87,32 @@ var defaultChainsWithParams = []any{ func newTestSuite(t *testing.T) *testSuite { var ( - logger = testlog.New(t) + logger = testlog.New(t) + baseLogger = base.Logger{ + Std: logger.Logger, + Compliance: logger.Logger, + } chainList, chainParams = parseChainsWithParams(t, defaultChainsWithParams...) ctx, appCtx = newAppContext(t, logger.Logger, chainList, chainParams) 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: NewV2(zetacore, schedulerService, logger.Logger), + V2: v2, Log: logger, t: t, @@ -106,6 +125,7 @@ func newTestSuite(t *testing.T) *testSuite { scheduler: schedulerService, zetacore: zetacore, + tss: tss, } // Mock basic zetacore methods @@ -114,7 +134,7 @@ func newTestSuite(t *testing.T) *testSuite { 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 + // 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() From 7232f5359157ae246002cb8d74aeb82b8805e86a Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:37:14 +0100 Subject: [PATCH 07/26] Move common btc stuff to common/ to fix import cycle --- .../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 +- pkg/chains/chain.go | 9 ++ pkg/scheduler/scheduler.go | 5 + zetaclient/chains/bitcoin/bitcoin.go | 71 ++++++++++++ .../chains/bitcoin/{ => common}/errors.go | 2 +- 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/observer/inbound.go | 22 ++-- .../chains/bitcoin/observer/inbound_test.go | 32 +++--- .../chains/bitcoin/observer/observer.go | 17 +-- .../chains/bitcoin/observer/outbound.go | 8 +- zetaclient/chains/bitcoin/observer/witness.go | 10 +- .../chains/bitcoin/observer/witness_test.go | 4 +- .../chains/bitcoin/rpc/rpc_live_test.go | 14 +-- zetaclient/chains/bitcoin/signer/signer.go | 28 ++--- zetaclient/context/chain.go | 6 +- zetaclient/orchestrator/v2_bootstrap.go | 11 +- zetaclient/orchestrator/v2_orchestrator.go | 5 +- 28 files changed, 234 insertions(+), 174 deletions(-) create mode 100644 zetaclient/chains/bitcoin/bitcoin.go rename zetaclient/chains/bitcoin/{ => common}/errors.go (90%) 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%) 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/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/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 2328cbddd7..f1367e6a8e 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -132,6 +132,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)) diff --git a/zetaclient/chains/bitcoin/bitcoin.go b/zetaclient/chains/bitcoin/bitcoin.go new file mode 100644 index 0000000000..7f7ad8b103 --- /dev/null +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -0,0 +1,71 @@ +package bitcoin + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/scheduler" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" +) + +type Bitcoin struct { + scheduler *scheduler.Scheduler + observer *observer.Observer + signer *signer.Signer +} + +func New( + scheduler *scheduler.Scheduler, + observer *observer.Observer, + signer *signer.Signer, +) *Bitcoin { + return &Bitcoin{ + scheduler: scheduler, + observer: observer, + signer: signer, + } +} + +func (b *Bitcoin) Chain() chains.Chain { + return b.observer.Chain() +} + +func (b *Bitcoin) Start(_ context.Context) error { + if ok := b.observer.Observer.Start(); !ok { + return errors.New("observer is already started") + } + + // // 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)) + + // todo start & schedule + 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), + ) +} diff --git a/zetaclient/chains/bitcoin/errors.go b/zetaclient/chains/bitcoin/common/errors.go similarity index 90% rename from zetaclient/chains/bitcoin/errors.go rename to zetaclient/chains/bitcoin/common/errors.go index d04d67687d..9ad07900f4 100644 --- a/zetaclient/chains/bitcoin/errors.go +++ b/zetaclient/chains/bitcoin/common/errors.go @@ -1,4 +1,4 @@ -package bitcoin +package common import "errors" 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/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 27f0839856..a121ceca9a 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -14,7 +14,7 @@ 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" @@ -55,7 +55,7 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { // 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) { + if !errors.Is(err, common.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") @@ -83,7 +83,7 @@ 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") + return errors.Wrap(common.ErrBitcoinNotEnabled, "observeInboundBTC: current block number 0 is too low") } // #nosec G115 checked positive @@ -263,7 +263,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 +303,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 +343,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 +368,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 +386,7 @@ func GetBtcEventWithoutWitness( blockNumber uint64, logger zerolog.Logger, netParams *chaincfg.Params, - feeCalculator bitcoin.DepositorFeeCalculator, + feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { var ( found bool @@ -401,7 +401,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 +427,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 +487,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..f73510bc68 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -20,7 +20,7 @@ import ( "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" @@ -137,23 +137,18 @@ func NewObserver( } // 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 { @@ -268,7 +263,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 @@ -369,7 +364,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 +481,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..2787852bc6 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -15,7 +15,7 @@ import ( "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" @@ -418,7 +418,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 +608,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 +662,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/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_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..2b305db1ef 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -25,7 +25,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" @@ -133,14 +133,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,7 +200,7 @@ 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 @@ -239,23 +239,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 } - 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 @@ -276,7 +276,7 @@ func (signer *Signer) SignWithdrawTx( 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 } @@ -411,7 +411,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 diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index 469f214309..26cab104c3 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -11,7 +11,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" observer "github.com/zeta-chain/node/x/observer/types" - "github.com/zeta-chain/node/zetaclient/logs" ) // ChainRegistry is a registry of supported chains @@ -147,10 +146,7 @@ func (c Chain) Name() string { } func (c Chain) LogFields() map[string]any { - return map[string]any{ - logs.FieldChain: c.ID(), - logs.FieldChainNetwork: c.chainInfo.Network.String(), - } + return c.RawChain().LogFields() } func (c Chain) Params() *observer.ChainParams { diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go index 46e3c9c1a7..76c95986d4 100644 --- a/zetaclient/orchestrator/v2_bootstrap.go +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -5,6 +5,7 @@ import ( "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" @@ -12,7 +13,7 @@ import ( "github.com/zeta-chain/node/zetaclient/db" ) -func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (ObserverSigner, error) { +func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin.Bitcoin, error) { var ( rawChain = chain.RawChain() rawChainParams = chain.Params() @@ -47,7 +48,7 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (ObserverS // todo extract base observer - _, err = btcobserver.NewObserver( + observer, err := btcobserver.NewObserver( *rawChain, rpcClient, *rawChainParams, @@ -63,12 +64,10 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (ObserverS // todo extract base signer - _, err = btcsigner.NewSigner(*rawChain, oc.deps.TSS, oc.logger.base, cfg) + signer, err := btcsigner.NewSigner(*rawChain, oc.deps.TSS, oc.logger.base, cfg) if err != nil { return nil, errors.Wrap(err, "unable to create signer") } - // todo observer-signer - - return nil, nil + return bitcoin.New(oc.scheduler, observer, signer), nil } diff --git a/zetaclient/orchestrator/v2_orchestrator.go b/zetaclient/orchestrator/v2_orchestrator.go index ec28eac40d..f44e37a8af 100644 --- a/zetaclient/orchestrator/v2_orchestrator.go +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -8,6 +8,7 @@ import ( "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" @@ -38,8 +39,8 @@ type loggers struct { const schedulerGroup = scheduler.Group("orchestrator") type ObserverSigner interface { + Chain() chains.Chain Start(ctx context.Context) error - Chain() zctx.Chain Stop() } @@ -223,7 +224,7 @@ func (oc *V2) addChain(observerSigner ObserverSigner) { chain := observerSigner.Chain() oc.mu.Lock() - oc.chains[chain.ID()] = observerSigner + oc.chains[chain.ChainId] = observerSigner oc.mu.Unlock() oc.logger.Info().Fields(chain.LogFields()).Msg("Added observer-signer") From e13d64c90a80e496823713f3b32cb761ad86a8d5 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:37:38 +0100 Subject: [PATCH 08/26] Implement BTC observerSigner --- pkg/scheduler/opts.go | 2 +- zetaclient/chains/base/observer.go | 6 + zetaclient/chains/bitcoin/bitcoin.go | 198 ++++++++++++++++-- zetaclient/chains/bitcoin/observer/inbound.go | 91 +------- .../chains/bitcoin/observer/observer.go | 110 +--------- .../chains/bitcoin/observer/outbound.go | 123 ++++------- .../chains/bitcoin/observer/rpc_status.go | 35 +--- zetaclient/orchestrator/bootstrap.go | 52 +---- zetaclient/orchestrator/orchestrator.go | 93 +------- 9 files changed, 253 insertions(+), 457 deletions(-) diff --git a/pkg/scheduler/opts.go b/pkg/scheduler/opts.go index 1da0ada4e8..53a0511ac1 100644 --- a/pkg/scheduler/opts.go +++ b/pkg/scheduler/opts.go @@ -29,7 +29,7 @@ func Interval(interval time.Duration) Opt { return func(_ *Task, opts *taskOpts) { opts.interval = 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 } } 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 index 7f7ad8b103..5e0100931d 100644 --- a/zetaclient/chains/bitcoin/bitcoin.go +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -3,18 +3,25 @@ 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( @@ -22,10 +29,15 @@ func New( 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, } } @@ -33,30 +45,70 @@ func (b *Bitcoin) Chain() chains.Chain { return b.observer.Chain() } -func (b *Bitcoin) Start(_ context.Context) error { +func (b *Bitcoin) Start(ctx context.Context) error { if ok := b.observer.Observer.Start(); !ok { return errors.New("observer is already started") } - // // 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)) - - // todo start & schedule + app, err := zctx.FromContext(ctx) + if err != nil { + return errors.Wrap(err, "unable to get app from context") + } + + // todo: should we share & fan-out the same chan across all chains? + 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, "ObserveInbound", optInboundInterval, optInboundSkipper) + register(b.observer.ObserveInboundTrackers, "ObserveInboundTrackers", optInboundInterval, optInboundSkipper) + register(b.observer.FetchUTXOs, "FetchUTXOs", optUTXOInterval, optGenericSkipper) + register(b.observer.PostGasPrice, "PostGasPrice", optGasInterval, optGenericSkipper) + register(b.observer.CheckRPCStatus, "CheckRPCStatus") + register(b.observer.ObserveOutbound, "ObserveOutbound", optOutboundInterval, optOutboundSkipper) + + // CCTX Scheduler + register(b.scheduleCCTX, "ScheduleCCTX", scheduler.BlockTicker(newBlockChan), optOutboundSkipper) + return nil } @@ -64,8 +116,114 @@ 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") + } + + 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/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index a121ceca9a..f24fb8ca20 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -16,59 +16,10 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "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, common.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,7 +34,8 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // 0 will be returned if the node is not synced if currentBlock == 0 { - return errors.Wrap(common.ErrBitcoinNotEnabled, "observeInboundBTC: current block number 0 is too low") + ob.logger.Inbound.Debug().Err(err).Msg("WatchInbound: Bitcoin node is not enabled") + return nil } // #nosec G115 checked positive @@ -156,44 +108,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 diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index f73510bc68..b7103b137b 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -7,7 +7,6 @@ import ( "math" "math/big" "sort" - "strings" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" @@ -16,7 +15,6 @@ 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" @@ -150,31 +148,8 @@ func NewObserver( } // 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) Start(_ context.Context) { + // todo drop } // GetPendingNonce returns the artificial pending nonce @@ -198,43 +173,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 { @@ -284,42 +222,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 { @@ -335,18 +237,20 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { // 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 diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 2787852bc6..7a7a0f372c 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -9,7 +9,6 @@ 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" @@ -19,100 +18,62 @@ import ( "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) diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index 4cd935c88c..ec44ef2bc4 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -2,44 +2,25 @@ 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 + return errors.Wrap(err, "unable to check RPC status") } ob.ReportBlockLatency(blockTime) + + return nil } 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/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 253a34e223..fc159bc3f8 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -19,9 +19,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" @@ -201,18 +199,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 } @@ -390,7 +377,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(): @@ -506,81 +494,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, From 91bab84fa1235e1f479b2a5a9c1dd7e30f11ae88 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:37:52 +0100 Subject: [PATCH 09/26] Drop redundant code --- cmd/zetaclientd/inbound.go | 30 ++++++------ .../chains/bitcoin/observer/observer.go | 7 --- zetaclient/chains/bitcoin/signer/signer.go | 49 ++----------------- zetaclient/orchestrator/v2_bootstrap.go | 4 +- 4 files changed, 21 insertions(+), 69 deletions(-) diff --git a/cmd/zetaclientd/inbound.go b/cmd/zetaclientd/inbound.go index ee602357fa..a2000ed5d7 100644 --- a/cmd/zetaclientd/inbound.go +++ b/cmd/zetaclientd/inbound.go @@ -15,7 +15,6 @@ import ( "github.com/zeta-chain/node/pkg/coin" "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" 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,20 +155,21 @@ 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) - } - - btcObserver, ok := observer.(*btcobserver.Observer) - if !ok { - return fmt.Errorf("observer is not btc observer for chain %d", chainID) - } - - ballotIdentifier, err = btcObserver.CheckReceiptForBtcTxHash(ctx, inboundHash, false) - if err != nil { - return err - } + return fmt.Errorf("not implemented") + //observer, ok := observers[chainID] + //if !ok { + // return fmt.Errorf("observer not found for btc chain %d", chainID) + //} + // + //btcObserver, ok := observer.(*btcobserver.Observer) + //if !ok { + // return fmt.Errorf("observer is not btc observer for chain %d", chainID) + //} + // + //ballotIdentifier, err = btcObserver.CheckReceiptForBtcTxHash(ctx, inboundHash, false) + //if err != nil { + // return err + //} } fmt.Println("BallotIdentifier: ", ballotIdentifier) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index b7103b137b..fac4af5546 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -39,8 +39,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 @@ -147,11 +145,6 @@ func NewObserver( return ob, nil } -// Start starts the Go routine processes to observe the Bitcoin chain -func (ob *Observer) Start(_ context.Context) { - // todo drop -} - // GetPendingNonce returns the artificial pending nonce // Note: pending nonce is accessed concurrently func (ob *Observer) GetPendingNonce() uint64 { diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 2b305db1ef..5aaded87ed 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" @@ -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 @@ -340,7 +306,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 +335,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 { @@ -440,7 +399,7 @@ func (signer *Signer) TryProcessOutbound( amount, gasprice, sizelimit, - btcObserver, + observer, height, outboundTssNonce, chain, @@ -487,7 +446,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/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go index 76c95986d4..5bf5d5766c 100644 --- a/zetaclient/orchestrator/v2_bootstrap.go +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -47,6 +47,8 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin. } // todo extract base observer + // todo extract base signer + // https://github.com/zeta-chain/node/issues/3331 observer, err := btcobserver.NewObserver( *rawChain, @@ -62,8 +64,6 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin. return nil, errors.Wrap(err, "unable to create observer") } - // todo extract base signer - signer, err := btcsigner.NewSigner(*rawChain, oc.deps.TSS, oc.logger.base, cfg) if err != nil { return nil, errors.Wrap(err, "unable to create signer") From 91dd276bd7047981447bd5d6ec991bf932741527 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 3 Jan 2025 19:18:04 +0100 Subject: [PATCH 10/26] Fix ticker concurrency bug --- pkg/ticker/ticker.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index 3d1662a9c0..3fb98551d1 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -214,7 +214,9 @@ 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") } From 991b751d9434dc6ef48c87728607e08593c20e21 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 3 Jan 2025 19:18:21 +0100 Subject: [PATCH 11/26] Add scheduler.Tasks() --- pkg/scheduler/scheduler.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index f1367e6a8e..59b72ec1ba 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 s.tasks +} + // Stop stops all tasks. func (s *Scheduler) Stop() { s.StopGroup("") @@ -166,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() From 22d8a519dcde3b89bd098bbdfbc872664b52fe61 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 3 Jan 2025 19:19:26 +0100 Subject: [PATCH 12/26] Add v2 btc observer-signer 101 test cases. Drop redundant tests --- zetaclient/chains/bitcoin/bitcoin.go | 15 +-- zetaclient/orchestrator/bootstrap_test.go | 112 ++---------------- zetaclient/orchestrator/v2_bootstrap.go | 10 +- zetaclient/orchestrator/v2_bootstrap_test.go | 78 ++++++++++++ zetaclient/orchestrator/v2_orchestrator.go | 2 +- .../orchestrator/v2_orchestrator_test.go | 87 +++++++++++--- zetaclient/testutils/mocks/chain_params.go | 2 +- 7 files changed, 175 insertions(+), 131 deletions(-) create mode 100644 zetaclient/orchestrator/v2_bootstrap_test.go diff --git a/zetaclient/chains/bitcoin/bitcoin.go b/zetaclient/chains/bitcoin/bitcoin.go index 5e0100931d..128ef03ac6 100644 --- a/zetaclient/chains/bitcoin/bitcoin.go +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -99,15 +99,15 @@ func (b *Bitcoin) Start(ctx context.Context) error { } // Observers - register(b.observer.ObserveInbound, "ObserveInbound", optInboundInterval, optInboundSkipper) - register(b.observer.ObserveInboundTrackers, "ObserveInboundTrackers", optInboundInterval, optInboundSkipper) - register(b.observer.FetchUTXOs, "FetchUTXOs", optUTXOInterval, optGenericSkipper) - register(b.observer.PostGasPrice, "PostGasPrice", optGasInterval, optGenericSkipper) - register(b.observer.CheckRPCStatus, "CheckRPCStatus") - register(b.observer.ObserveOutbound, "ObserveOutbound", optOutboundInterval, optOutboundSkipper) + 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, "ScheduleCCTX", scheduler.BlockTicker(newBlockChan), optOutboundSkipper) + register(b.scheduleCCTX, "schedule_cctx", scheduler.BlockTicker(newBlockChan), optOutboundSkipper) return nil } @@ -142,6 +142,7 @@ func (b *Bitcoin) scheduleCCTX(ctx context.Context) error { 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) diff --git a/zetaclient/orchestrator/bootstrap_test.go b/zetaclient/orchestrator/bootstrap_test.go index 5ccfd39297..c6f44acf9a 100644 --- a/zetaclient/orchestrator/bootstrap_test.go +++ b/zetaclient/orchestrator/bootstrap_test.go @@ -37,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) @@ -51,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) @@ -70,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 @@ -91,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) { @@ -110,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 @@ -173,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) { @@ -241,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 @@ -256,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 @@ -277,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) { @@ -299,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) }) @@ -320,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) }) @@ -340,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) { @@ -506,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) diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go index 5bf5d5766c..1a985dd56c 100644 --- a/zetaclient/orchestrator/v2_bootstrap.go +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -14,11 +14,6 @@ import ( ) func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin.Bitcoin, error) { - var ( - rawChain = chain.RawChain() - rawChainParams = chain.Params() - ) - // should not happen if !chain.IsBitcoin() { return nil, errors.New("chain is not bitcoin") @@ -39,6 +34,11 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin. 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) 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 index f44e37a8af..bfb4b3bb8e 100644 --- a/zetaclient/orchestrator/v2_orchestrator.go +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -194,7 +194,7 @@ func (oc *V2) SyncChains(ctx context.Context) error { oc.logger.Info(). Int("chains.added", added). Int("chains.removed", removed). - Msg("synced observer-signers") + Msg("Synced observer-signers") } return nil diff --git a/zetaclient/orchestrator/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go index 88f7b448f5..c10c11481f 100644 --- a/zetaclient/orchestrator/v2_orchestrator_test.go +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -2,9 +2,11 @@ package orchestrator import ( "context" + "reflect" "sync" "testing" "time" + "unsafe" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -21,6 +23,7 @@ import ( "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) { @@ -28,29 +31,44 @@ func TestOrchestratorV2(t *testing.T) { // ARRANGE ts := newTestSuite(t) - // ACT + // ACT #1 + // Start orchestrator err := ts.Start(ts.ctx) // Mimic zetacore update ts.MockChainParams(chains.Ethereum, mocks.MockChainParams(chains.Ethereum.ChainId, 100)) - // ASSERT + // ASSERT #1 require.NoError(t, err) // Check that eventually appContext would contain only desired chains check := func() bool { - listChains := ts.appContext.ListChains() - if len(listChains) != 1 { - return false - } - - return listChains[0].ID() == chains.Ethereum.ChainId + 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!"`) }) } @@ -86,16 +104,23 @@ var defaultChainsWithParams = []any{ } func newTestSuite(t *testing.T) *testSuite { - var ( - logger = testlog.New(t) - baseLogger = base.Logger{ - Std: logger.Logger, - Compliance: logger.Logger, - } + 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) - 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) @@ -165,6 +190,21 @@ func (ts *testSuite) getChainParams(_ context.Context) ([]*observertypes.ChainPa 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, @@ -210,3 +250,18 @@ func newAppContext( 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, From e81db518ed28273eb390152b063485e9454d1b7a Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:45:28 +0100 Subject: [PATCH 13/26] Address PR comments --- cmd/zetaclientd/start.go | 2 ++ pkg/scheduler/scheduler.go | 2 +- zetaclient/chains/bitcoin/bitcoin.go | 4 ++-- zetaclient/orchestrator/v2_bootstrap.go | 4 ++-- zetaclient/orchestrator/v2_orchestrator.go | 15 ++++++++++----- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 610f47fca6..dfd1b7a0e0 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -170,6 +170,8 @@ func Start(_ *cobra.Command, _ []string) error { 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 diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 59b72ec1ba..4eca8877af 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -116,7 +116,7 @@ func (s *Scheduler) Tasks() map[uuid.UUID]*Task { copied[k] = v } - return s.tasks + return copied } // Stop stops all tasks. diff --git a/zetaclient/chains/bitcoin/bitcoin.go b/zetaclient/chains/bitcoin/bitcoin.go index 128ef03ac6..9ebd04f917 100644 --- a/zetaclient/chains/bitcoin/bitcoin.go +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -29,7 +29,7 @@ func New( observer *observer.Observer, signer *signer.Signer, ) *Bitcoin { - // todo move this to base signer + // TODO move this to base signer // https://github.com/zeta-chain/node/issues/3330 proc := outboundprocessor.NewProcessor(observer.Logger().Outbound) @@ -55,7 +55,7 @@ func (b *Bitcoin) Start(ctx context.Context) error { return errors.Wrap(err, "unable to get app from context") } - // todo: should we share & fan-out the same chan across all chains? + // TODO: should we share & fan-out the same chan across all chains? newBlockChan, err := b.observer.ZetacoreClient().NewBlockSubscriber(ctx) if err != nil { return errors.Wrap(err, "unable to create new block subscriber") diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go index 1a985dd56c..1962433c1c 100644 --- a/zetaclient/orchestrator/v2_bootstrap.go +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -46,8 +46,8 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin. return nil, errors.Wrapf(err, "unable to open database %s", dbName) } - // todo extract base observer - // todo extract base signer + // TODO extract base observer + // TODO extract base signer // https://github.com/zeta-chain/node/issues/3331 observer, err := btcobserver.NewObserver( diff --git a/zetaclient/orchestrator/v2_orchestrator.go b/zetaclient/orchestrator/v2_orchestrator.go index bfb4b3bb8e..01b086a2c9 100644 --- a/zetaclient/orchestrator/v2_orchestrator.go +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -153,15 +153,15 @@ func (oc *V2) SyncChains(ctx context.Context) error { case chain.IsBitcoin(): observerSigner, err = oc.bootstrapBitcoin(ctx, chain) case chain.IsEVM(): - // todo + // TODO // https://github.com/zeta-chain/node/issues/3302 continue case chain.IsSolana(): - // todo + // TODO // https://github.com/zeta-chain/node/issues/3301 continue case chain.IsTON(): - // todo + // TODO // https://github.com/zeta-chain/node/issues/3300 continue } @@ -224,9 +224,14 @@ func (oc *V2) addChain(observerSigner ObserverSigner) { chain := observerSigner.Chain() oc.mu.Lock() - oc.chains[chain.ChainId] = observerSigner - oc.mu.Unlock() + 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") } From c637cee092aa01445b80e2b0e6e669f4dba0b6f8 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:03:20 +0100 Subject: [PATCH 14/26] Add issue --- zetaclient/orchestrator/v2_orchestrator.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zetaclient/orchestrator/v2_orchestrator.go b/zetaclient/orchestrator/v2_orchestrator.go index 01b086a2c9..4f970dddb7 100644 --- a/zetaclient/orchestrator/v2_orchestrator.go +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -168,6 +168,8 @@ func (oc *V2) SyncChains(ctx context.Context) error { 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: From bc5dd8f301ed384aca2faefbe5aff0fff7fe47ee Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:01:10 +0100 Subject: [PATCH 15/26] fix inbound debug cmd --- cmd/zetaclientd/inbound.go | 52 ++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/cmd/zetaclientd/inbound.go b/cmd/zetaclientd/inbound.go index a2000ed5d7..ee1ac98a05 100644 --- a/cmd/zetaclientd/inbound.go +++ b/cmd/zetaclientd/inbound.go @@ -6,15 +6,17 @@ 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" "github.com/zeta-chain/node/pkg/coin" "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" @@ -155,21 +157,39 @@ func InboundGetBallot(_ *cobra.Command, args []string) error { } fmt.Println("CoinType : ", coinType) } else if chain.IsBitcoin() { - return fmt.Errorf("not implemented") - //observer, ok := observers[chainID] - //if !ok { - // return fmt.Errorf("observer not found for btc chain %d", chainID) - //} - // - //btcObserver, ok := observer.(*btcobserver.Observer) - //if !ok { - // return fmt.Errorf("observer is not btc observer for chain %d", chainID) - //} - // - //ballotIdentifier, err = btcObserver.CheckReceiptForBtcTxHash(ctx, inboundHash, false) - //if err != nil { - // return err - //} + bitcoinConfig, found := appContext.Config().GetBTCConfig(chain.ID()) + if !found { + return fmt.Errorf("unable to find btc config") + } + + 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 = observer.CheckReceiptForBtcTxHash(ctx, inboundHash, false) + if err != nil { + return err + } } fmt.Println("BallotIdentifier: ", ballotIdentifier) From ee5b997d1fcdcdb8f3a17769a8e5ad02f7427ff4 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:12:44 +0100 Subject: [PATCH 16/26] Add tss graceful shutdown --- cmd/zetaclientd/start.go | 2 ++ zetaclient/tss/service.go | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index dfd1b7a0e0..6e880133e1 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -109,6 +109,8 @@ func Start(_ *cobra.Command, _ []string) error { return nil } + graceful.AddStopper(tss.Stop) + // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, logger.Std).Listen(ctx, func() { 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"} From 5e89631f955bc7904c1a6e7cc6daaafc0ca39ddd Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:14:30 +0100 Subject: [PATCH 17/26] Update changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 5bb11d40cf..616f882995 100644 --- a/changelog.md +++ b/changelog.md @@ -20,6 +20,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 From d2d98da9cd449612a3e43332c506aa7077c34d9e Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:42:28 +0100 Subject: [PATCH 18/26] fix tss tests --- zetaclient/tss/service_test.go | 2 ++ 1 file changed, 2 insertions(+) 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() From ffcd731df808f6d371cb51af45ad32c4ce9e11ce Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:00:59 +0100 Subject: [PATCH 19/26] Fix IntervalUpdater --- pkg/scheduler/opts.go | 6 +++--- pkg/scheduler/scheduler.go | 9 +++++++++ pkg/scheduler/scheduler_test.go | 9 +++++++-- pkg/scheduler/tickers.go | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pkg/scheduler/opts.go b/pkg/scheduler/opts.go index 53a0511ac1..44ec8493c8 100644 --- a/pkg/scheduler/opts.go +++ b/pkg/scheduler/opts.go @@ -26,7 +26,7 @@ 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. If it returns true, the task is skipped. @@ -34,10 +34,10 @@ 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.interval = intervalUpdater() + opts.interval = normalizeInterval(intervalUpdater()) opts.intervalUpdater = intervalUpdater } } diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 4eca8877af..c6bbb5c241 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -234,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 From a04244b25c1bfb9a4ca9520902c9f1b2ffe4fd99 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:10:50 +0100 Subject: [PATCH 20/26] Mitigate errors when BTC node is disabled --- zetaclient/chains/bitcoin/common/errors.go | 6 ------ zetaclient/chains/bitcoin/observer/inbound.go | 3 +++ zetaclient/chains/bitcoin/observer/observer.go | 16 ++++++++++++++++ .../chains/bitcoin/observer/rpc_status.go | 7 ++++++- zetaclient/chains/bitcoin/rpc/rpc.go | 17 ++++++++--------- 5 files changed, 33 insertions(+), 16 deletions(-) delete mode 100644 zetaclient/chains/bitcoin/common/errors.go diff --git a/zetaclient/chains/bitcoin/common/errors.go b/zetaclient/chains/bitcoin/common/errors.go deleted file mode 100644 index 9ad07900f4..0000000000 --- a/zetaclient/chains/bitcoin/common/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package common - -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 f24fb8ca20..ecaf9f1e7a 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -34,10 +34,13 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // 0 will be returned if the node is not synced if currentBlock == 0 { + 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() { diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index fac4af5546..d345f4da36 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -7,6 +7,7 @@ import ( "math" "math/big" "sort" + "sync/atomic" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" @@ -80,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 } @@ -132,6 +137,8 @@ func NewObserver( }, } + ob.nodeEnabled.Store(true) + // load last scanned block if err = ob.LoadLastBlockScanned(); err != nil { return nil, errors.Wrap(err, "unable to load last scanned block") @@ -145,6 +152,10 @@ func NewObserver( return ob, nil } +func (ob *Observer) isNodeEnabled() bool { + return ob.nodeEnabled.Load() +} + // GetPendingNonce returns the artificial pending nonce // Note: pending nonce is accessed concurrently func (ob *Observer) GetPendingNonce() uint64 { @@ -224,6 +235,11 @@ 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) diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index ec44ef2bc4..f09e3ec32d 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -16,7 +16,12 @@ func (ob *Observer) CheckRPCStatus(_ context.Context) error { } blockTime, err := rpc.CheckRPCStatus(ob.btcClient, tssAddress) - if err != nil { + 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") } 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 From c91aeea8e0723f760e20342bcad6bdf829b4472a Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 8 Jan 2025 19:29:56 +0100 Subject: [PATCH 21/26] Implement pkg/fanout --- pkg/fanout/fanout.go | 66 +++++++++++++++++++++++++++++++++++ pkg/fanout/fanout_test.go | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 pkg/fanout/fanout.go create mode 100644 pkg/fanout/fanout_test.go 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) +} From 3b6cb6d3c043c075ee29225fee62172c5eb9b577 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:30:00 +0100 Subject: [PATCH 22/26] Apply fanout to block subscriber --- zetaclient/chains/bitcoin/bitcoin.go | 1 - zetaclient/orchestrator/orchestrator.go | 7 ++ zetaclient/zetacore/client.go | 5 ++ zetaclient/zetacore/client_subscriptions.go | 78 +++++++++++++++++---- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/zetaclient/chains/bitcoin/bitcoin.go b/zetaclient/chains/bitcoin/bitcoin.go index 9ebd04f917..410f2892e8 100644 --- a/zetaclient/chains/bitcoin/bitcoin.go +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -55,7 +55,6 @@ func (b *Bitcoin) Start(ctx context.Context) error { return errors.Wrap(err, "unable to get app from context") } - // TODO: should we share & fan-out the same chan across all chains? newBlockChan, err := b.observer.ZetacoreClient().NewBlockSubscriber(ctx) if err != nil { return errors.Wrap(err, "unable to create new block subscriber") diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 0fcfc54c73..9600d23d11 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -352,6 +352,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 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..971b1edfa2 100644 --- a/zetaclient/zetacore/client_subscriptions.go +++ b/zetaclient/zetacore/client_subscriptions.go @@ -3,33 +3,85 @@ package zetacore import ( "context" - cometbfttypes "github.com/cometbft/cometbft/types" + 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 } - 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(): 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 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, ok := c.getBlockFanoutChan(); ok { + c.logger.Info().Msg("Resolved existing block subscriber") + return blocksFanout, nil + } + + // Subscribe to comet bft events + eventsChan, err := c.cometBFTClient.Subscribe(context.Background(), "", ctypes.EventQueryNewBlock.String()) + if err != nil { + return nil, err + } + + c.logger.Info().Msg("Subscribed to new 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 } + + blockChan <- newBlockEvent } }() - return blockEventChan, nil + // Create a fanout + // It allows a "global" chan (i.e. blockChan) to stream to multiple consumers independently. + c.mu.Lock() + defer c.mu.Unlock() + c.blocksFanout = fanout.New[ctypes.EventDataNewBlock](blockChan, fanout.DefaultBuffer) + + c.blocksFanout.Start() + + return c.blocksFanout, nil +} + +func (c *Client) getBlockFanoutChan() (*fanout.FanOut[ctypes.EventDataNewBlock], bool) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.blocksFanout, c.blocksFanout != nil } From d7e5d55516d16f6bc574f5e0a183b6456c067a8e Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 9 Jan 2025 19:13:18 +0100 Subject: [PATCH 23/26] Fix typo --- zetaclient/chains/bitcoin/bitcoin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/bitcoin.go b/zetaclient/chains/bitcoin/bitcoin.go index 410f2892e8..0cfcbe1bad 100644 --- a/zetaclient/chains/bitcoin/bitcoin.go +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -183,7 +183,7 @@ func (b *Bitcoin) scheduleCCTX(ctx context.Context) error { Uint64("outbound.earliest_pending_nonce", cctxList[0].GetCurrentOutboundParam().TssNonce). Msg("Schedule CCTX: lookahead reached") return nil - case !b.proc.IsOutboundActive(outboundID): + case b.proc.IsOutboundActive(outboundID): // outbound is already being processed continue } From f35ea11175e9301062bc4c3353062e4d1303892b Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 10 Jan 2025 19:36:36 +0100 Subject: [PATCH 24/26] Minor btc signer improvements --- zetaclient/chains/bitcoin/signer/signer.go | 38 ++++++++++------------ 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 5aaded87ed..34c0f592a7 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -170,12 +170,8 @@ func (signer *Signer) SignWithdrawTx( 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 @@ -188,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 @@ -196,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) @@ -207,7 +204,7 @@ func (signer *Signer) SignWithdrawTx( // #nosec G115 always positive 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 < common.BtcOutboundBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user signer.Logger().Std.Info(). @@ -235,7 +232,7 @@ 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 @@ -258,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 { @@ -280,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 } From 849bdde061ea3d4594d97456bb905c93df953226 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:29:24 +0100 Subject: [PATCH 25/26] Make V1.Stop() safe to call multiple times --- zetaclient/orchestrator/orchestrator.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 9600d23d11..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" @@ -70,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 { @@ -146,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. From c1c0078c2af27cce4e8c264af5763fa281a09d8f Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:39:10 +0100 Subject: [PATCH 26/26] FIX DATA RACE --- zetaclient/zetacore/client_subscriptions.go | 38 ++++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/zetaclient/zetacore/client_subscriptions.go b/zetaclient/zetacore/client_subscriptions.go index 971b1edfa2..7bf5ea1c25 100644 --- a/zetaclient/zetacore/client_subscriptions.go +++ b/zetaclient/zetacore/client_subscriptions.go @@ -3,6 +3,7 @@ package zetacore import ( "context" + "cosmossdk.io/errors" ctypes "github.com/cometbft/cometbft/types" "github.com/zeta-chain/node/pkg/fanout" @@ -13,7 +14,7 @@ import ( 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") } // we need a "proxy" chan instead of directly returning blockSubscriber.Add() @@ -26,6 +27,9 @@ func (c *Client) NewBlockSubscriber(ctx context.Context) (chan ctypes.EventDataN 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 block := <-consumer: blocksChan <- block @@ -40,18 +44,24 @@ func (c *Client) NewBlockSubscriber(ctx context.Context) (chan ctypes.EventDataN // or subscribes to it for the first time. func (c *Client) resolveBlockSubscriber() (*fanout.FanOut[ctypes.EventDataNewBlock], error) { // noop - if blocksFanout, ok := c.getBlockFanoutChan(); ok { + 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, err + return nil, errors.Wrap(err, "unable to subscribe to new block events") } - c.logger.Info().Msg("Subscribed to new block events") + c.logger.Info().Msg("Subscribed to block events") // Create block chan blockChan := make(chan ctypes.EventDataNewBlock) @@ -65,23 +75,25 @@ func (c *Client) resolveBlockSubscriber() (*fanout.FanOut[ctypes.EventDataNewBlo 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. - c.mu.Lock() - defer c.mu.Unlock() - c.blocksFanout = fanout.New[ctypes.EventDataNewBlock](blockChan, fanout.DefaultBuffer) + fo := fanout.New[ctypes.EventDataNewBlock](blockChan, fanout.DefaultBuffer) + fo.Start() - c.blocksFanout.Start() + c.blocksFanout = fo - return c.blocksFanout, nil + return fo, nil } -func (c *Client) getBlockFanoutChan() (*fanout.FanOut[ctypes.EventDataNewBlock], bool) { - c.mu.RLock() - defer c.mu.RUnlock() - return c.blocksFanout, c.blocksFanout != nil +func (c *Client) blockFanOutThreadSafe() *fanout.FanOut[ctypes.EventDataNewBlock] { + c.mu.Lock() + defer c.mu.Unlock() + + return c.blocksFanout }