diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 3cac451e88..6c9213a902 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -33,7 +33,7 @@ runs: - uses: actions/setup-go@v5 if: ${{ inputs.skip_go == 'false' }} with: - go-version: '1.22' + go-version: '1.23' cache: false - uses: actions/setup-python@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a44e995a6..e46446dd7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,7 +104,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' - name: go get node working-directory: contrib/rpcimportable run: go get github.com/zeta-chain/node@${{github.event.pull_request.head.sha || github.sha}} diff --git a/.github/workflows/sast-linters.yml b/.github/workflows/sast-linters.yml index 7b09472298..5005d552f3 100644 --- a/.github/workflows/sast-linters.yml +++ b/.github/workflows/sast-linters.yml @@ -43,12 +43,12 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.59 + version: v1.63.4 skip-cache: true nosec_alert: diff --git a/.github/workflows/sim.yaml b/.github/workflows/sim.yaml index 11e6858a81..963a1ca6f9 100644 --- a/.github/workflows/sim.yaml +++ b/.github/workflows/sim.yaml @@ -84,7 +84,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.22' + go-version: '1.23' - name: Install dependencies run: make runsim diff --git a/.golangci.yml b/.golangci.yml index 8fb8a0084a..8ed83e2f52 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,7 +13,6 @@ linters: - misspell - goimports - govet - - gosec - stylecheck - typecheck - misspell diff --git a/Dockerfile-localnet b/Dockerfile-localnet index 09e8c15a10..dd329a712d 100644 --- a/Dockerfile-localnet +++ b/Dockerfile-localnet @@ -1,6 +1,6 @@ # syntax=ghcr.io/zeta-chain/docker-dockerfile:1.9-labs # check=error=true -FROM ghcr.io/zeta-chain/golang:1.22.7-bookworm AS base-build +FROM ghcr.io/zeta-chain/golang:1.23.3-bookworm AS base-build ENV GOPATH=/go ENV GOOS=linux @@ -27,10 +27,10 @@ RUN --mount=type=cache,target="/root/.cache/go-build" \ NODE_COMMIT=${NODE_COMMIT} \ make install install-zetae2e -FROM ghcr.io/zeta-chain/golang:1.22.7-bookworm AS cosmovisor-build -RUN go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.6.0 +FROM ghcr.io/zeta-chain/golang:1.23.3-bookworm AS cosmovisor-build +RUN go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.7.0 -FROM ghcr.io/zeta-chain/golang:1.22.7-bookworm AS base-runtime +FROM ghcr.io/zeta-chain/golang:1.23.3-bookworm AS base-runtime RUN apt update && \ apt install -yq jq yq curl tmux python3 openssh-server iputils-ping iproute2 bind9-host && \ diff --git a/Makefile b/Makefile index 02ac397d51..910791e5c7 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ GOFLAGS := "" GOPATH ?= '$(HOME)/go' # common goreaser command definition -GOLANG_CROSS_VERSION ?= v1.22.7@sha256:24b2d75007f0ec8e35d01f3a8efa40c197235b200a1a91422d78b851f67ecce4 +GOLANG_CROSS_VERSION ?= v1.23.3@sha256:380420abb74844aaebca5bf9e2d00b1d7c78f59ce9e6d47cdb3276281702ca23 GORELEASER := $(DOCKER) run \ --rm \ --privileged \ @@ -409,7 +409,7 @@ test-sim-fullappsimulation: $(call run-sim-test,"TestFullAppSimulation",TestFullAppSimulation,100,200,30m) test-sim-import-export: - $(call run-sim-test,"test-import-export",TestAppImportExport,50,100,30m) + $(call run-sim-test,"test-import-export",TestAppImportExport,100,200,30m) test-sim-after-import: $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,100,200,30m) @@ -430,6 +430,12 @@ test-sim-after-import-long: runsim @echo "Running application simulation-after-import. This may take several minute" @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 500 50 TestAppSimulationAfterImport +# Use to run all simulation tests quickly (for example, before a creating a PR) +test-sim-quick: + $(call run-sim-test,"test-full-app-sim",TestFullAppSimulation,10,20,30m) + $(call run-sim-test,"test-import-export",TestAppImportExport,10,20,30m) + $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,10,20,30m) + .PHONY: \ test-sim-nondeterminism \ test-sim-fullappsimulation \ diff --git a/app/ante/authz.go b/app/ante/authz.go index d310943771..f6c8f1a857 100644 --- a/app/ante/authz.go +++ b/app/ante/authz.go @@ -2,6 +2,7 @@ package ante import ( "fmt" + "slices" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -94,11 +95,5 @@ func (ald AuthzLimiterDecorator) checkDisabledMsgs(msgs []sdk.Msg, isAuthzInnerM // isDisabledMsg returns true if the given message is in the list of restricted // messages from the AnteHandler. func (ald AuthzLimiterDecorator) isDisabledMsg(msgTypeURL string) bool { - for _, disabledType := range ald.disabledMsgTypes { - if msgTypeURL == disabledType { - return true - } - } - - return false + return slices.Contains(ald.disabledMsgTypes, msgTypeURL) } diff --git a/app/app.go b/app/app.go index 1132e89cb3..a66e2b7ef8 100644 --- a/app/app.go +++ b/app/app.go @@ -143,8 +143,9 @@ func init() { } var ( - NodeDir = ".zetacored" - DefaultNodeHome = os.ExpandEnv("$HOME/") + NodeDir + NodeDir = ".zetacored" + DefaultNodeHome = os.ExpandEnv("$HOME/") + NodeDir + TransactionGasLimit uint64 = 10_000_000 ) func getGovProposalHandlers() []govclient.ProposalHandler { @@ -784,7 +785,6 @@ func New( app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - maxGasWanted := cast.ToUint64(appOpts.Get(srvflags.EVMMaxTxGasWanted)) options := ante.HandlerOptions{ AccountKeeper: app.AccountKeeper, BankKeeper: app.BankKeeper, @@ -792,7 +792,7 @@ func New( FeeMarketKeeper: app.FeeMarketKeeper, SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), SigGasConsumer: evmante.DefaultSigVerificationGasConsumer, - MaxTxGasWanted: maxGasWanted, + MaxTxGasWanted: TransactionGasLimit, DisabledAuthzMsgs: []string{ sdk.MsgTypeURL( &evmtypes.MsgEthereumTx{}, diff --git a/changelog.md b/changelog.md index 0cfa806354..7ee1dbc02c 100644 --- a/changelog.md +++ b/changelog.md @@ -26,12 +26,14 @@ * [3254](https://github.com/zeta-chain/node/pull/3254) - rename v2 E2E tests as evm tests and rename old evm tests as legacy * [3095](https://github.com/zeta-chain/node/pull/3095) - initialize simulation tests for custom zetachain modules * [3276](https://github.com/zeta-chain/node/pull/3276) - add Solana E2E performance tests and improve Solana outbounds performance +* [3207](https://github.com/zeta-chain/node/pull/3207) - add simulation test operations for all messages in crosschain and observer module ### Refactor * [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient * [3291](https://github.com/zeta-chain/node/pull/3291) - revamp zetaclient initialization (+ graceful shutdown) * [3319](https://github.com/zeta-chain/node/pull/3319) - implement scheduler for zetaclient +* [3332](https://github.com/zeta-chain/node/pull/3332) - implement orchestrator V2. Move BTC observer-signer to V2 ### Fixes diff --git a/cmd/zetaclientd/inbound.go b/cmd/zetaclientd/inbound.go index ee602357fa..ee1ac98a05 100644 --- a/cmd/zetaclientd/inbound.go +++ b/cmd/zetaclientd/inbound.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -16,6 +16,7 @@ import ( "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/chains/base" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + btcrpc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" evmobserver "github.com/zeta-chain/node/zetaclient/chains/evm/observer" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" @@ -156,17 +157,36 @@ func InboundGetBallot(_ *cobra.Command, args []string) error { } fmt.Println("CoinType : ", coinType) } else if chain.IsBitcoin() { - observer, ok := observers[chainID] - if !ok { - return fmt.Errorf("observer not found for btc chain %d", chainID) + bitcoinConfig, found := appContext.Config().GetBTCConfig(chain.ID()) + if !found { + return fmt.Errorf("unable to find btc config") } - btcObserver, ok := observer.(*btcobserver.Observer) - if !ok { - return fmt.Errorf("observer is not btc observer for chain %d", chainID) + rpcClient, err := btcrpc.NewRPCClient(bitcoinConfig) + if err != nil { + return errors.Wrap(err, "unable to create rpc client") + } + + database, err := db.NewFromSqliteInMemory(true) + if err != nil { + return errors.Wrap(err, "unable to open database") + } + + observer, err := btcobserver.NewObserver( + *chain.RawChain(), + rpcClient, + *chain.Params(), + client, + nil, + database, + baseLogger, + nil, + ) + if err != nil { + return errors.Wrap(err, "unable to create btc observer") } - ballotIdentifier, err = btcObserver.CheckReceiptForBtcTxHash(ctx, inboundHash, false) + ballotIdentifier, err = observer.CheckReceiptForBtcTxHash(ctx, inboundHash, false) if err != nil { return err } diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index ae7da7e6ac..3f2a9f6e4b 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -14,6 +14,7 @@ import ( "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/graceful" zetaos "github.com/zeta-chain/node/pkg/os" + "github.com/zeta-chain/node/pkg/scheduler" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" @@ -115,6 +116,8 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to setup TSS service") } + graceful.AddStopper(tss.Stop) + // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, logger.Std).Listen(ctx, func() { @@ -159,9 +162,27 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to create orchestrator") } + taskScheduler := scheduler.New(logger.Std) + maestroV2Deps := &orchestrator.Dependencies{ + Zetacore: zetacoreClient, + TSS: tss, + DBPath: dbPath, + Telemetry: telemetry, + } + + maestroV2, err := orchestrator.NewV2(taskScheduler, maestroV2Deps, logger) + if err != nil { + return errors.Wrap(err, "unable to create orchestrator V2") + } + // Start orchestrator with all observers and signers graceful.AddService(ctx, maestro) + // Start orchestrator V2 + // V2 will co-exist with V1 until all types of chains will be refactored (BTC, EVM, SOL, TON). + // (currently it's only BTC) + graceful.AddService(ctx, maestroV2) + // Block current routine until a shutdown signal is received graceful.WaitForShutdown() diff --git a/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go b/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go index c469063f00..c94e6cb6b7 100644 --- a/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go +++ b/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go @@ -6,7 +6,7 @@ import ( "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/testutil/sample" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinDepositAndCallRevert(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go b/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go index 9e3606759b..c7ae665423 100644 --- a/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go +++ b/e2e/e2etests/test_bitcoin_deposit_and_call_revert_with_dust.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/testutil/sample" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) // TestBitcoinDepositAndCallRevertWithDust sends a Bitcoin deposit that reverts with a dust amount in the revert outbound. diff --git a/e2e/e2etests/test_bitcoin_deposit_call.go b/e2e/e2etests/test_bitcoin_deposit_call.go index 415dc780f0..c73bc3d6b0 100644 --- a/e2e/e2etests/test_bitcoin_deposit_call.go +++ b/e2e/e2etests/test_bitcoin_deposit_call.go @@ -9,7 +9,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { @@ -20,7 +20,7 @@ func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { // Given amount to send require.Len(r, args, 1) amount := utils.ParseFloat(r, args[0]) - amountTotal := amount + zetabitcoin.DefaultDepositorFee + amountTotal := amount + common.DefaultDepositorFee // Given a list of UTXOs utxos, err := r.ListDeployerUTXOs() @@ -45,7 +45,7 @@ func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // check if example contract has been called, 'bar' value should be set to amount - amountSats, err := zetabitcoin.GetSatoshis(amount) + amountSats, err := common.GetSatoshis(amount) require.NoError(r, err) utils.MustHaveCalledExampleContract(r, contract, big.NewInt(amountSats)) } diff --git a/e2e/e2etests/test_bitcoin_donation.go b/e2e/e2etests/test_bitcoin_donation.go index 203914545a..ccddb91c51 100644 --- a/e2e/e2etests/test_bitcoin_donation.go +++ b/e2e/e2etests/test_bitcoin_donation.go @@ -9,7 +9,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/constant" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinDonation(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_bitcoin_std_deposit.go b/e2e/e2etests/test_bitcoin_std_deposit.go index fefd5ae039..ff8e9de4cd 100644 --- a/e2e/e2etests/test_bitcoin_std_deposit.go +++ b/e2e/e2etests/test_bitcoin_std_deposit.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinStdMemoDeposit(r *runner.E2ERunner, args []string) { @@ -54,7 +54,7 @@ func TestBitcoinStdMemoDeposit(r *runner.E2ERunner, args []string) { // the runner balance should be increased by the deposit amount amountIncreased := new(big.Int).Sub(balanceAfter, balanceBefore) - amountSatoshis, err := bitcoin.GetSatoshis(amount) + amountSatoshis, err := common.GetSatoshis(amount) require.NoError(r, err) require.Positive(r, amountSatoshis) // #nosec G115 always positive diff --git a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go index d223fa6afd..e1d897fca5 100644 --- a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go +++ b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/pkg/memo" testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinStdMemoDepositAndCall(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go index 162cf7c123..92f907a20a 100644 --- a/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go +++ b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go @@ -10,7 +10,7 @@ import ( "github.com/zeta-chain/node/pkg/memo" testcontract "github.com/zeta-chain/node/testutil/contracts" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" ) func TestBitcoinStdMemoInscribedDepositAndCall(r *runner.E2ERunner, args []string) { @@ -53,7 +53,7 @@ func TestBitcoinStdMemoInscribedDepositAndCall(r *runner.E2ERunner, args []strin utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // check if example contract has been called, 'bar' value should be set to correct amount - depositFeeSats, err := zetabitcoin.GetSatoshis(zetabitcoin.DefaultDepositorFee) + depositFeeSats, err := common.GetSatoshis(common.DefaultDepositorFee) require.NoError(r, err) receiveAmount := depositAmount - depositFeeSats utils.MustHaveCalledExampleContract(r, contract, big.NewInt(receiveAmount)) diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index ab322532a8..619844a30c 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -23,7 +23,7 @@ import ( "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + zetabtc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" ) @@ -100,7 +100,7 @@ func (r *E2ERunner) DepositBTCWithAmount(amount float64, memo *memo.InboundMemo) r.Logger.Info("Now sending two txs to TSS address...") // add depositor fee so that receiver gets the exact given 'amount' in ZetaChain - amount += zetabitcoin.DefaultDepositorFee + amount += zetabtc.DefaultDepositorFee // deposit to TSS address var txHash *chainhash.Hash @@ -148,7 +148,7 @@ func (r *E2ERunner) DepositBTC(receiver common.Address) { r.Logger.Info("Now sending two txs to TSS address and tester ZEVM address...") // send initial BTC to the tester ZEVM address - amount := 1.15 + zetabitcoin.DefaultDepositorFee + amount := 1.15 + zetabtc.DefaultDepositorFee txHash, err := r.DepositBTCWithLegacyMemo(amount, utxos[:2], receiver) require.NoError(r, err) @@ -241,7 +241,7 @@ func (r *E2ERunner) sendToAddrFromDeployerWithMemo( // use static fee 0.0005 BTC to calculate change feeSats := btcutil.Amount(0.0005 * btcutil.SatoshiPerBitcoin) - amountInt, err := zetabitcoin.GetSatoshis(amount) + amountInt, err := zetabtc.GetSatoshis(amount) require.NoError(r, err) amountSats := btcutil.Amount(amountInt) change := inputSats - feeSats - amountSats @@ -351,7 +351,7 @@ func (r *E2ERunner) InscribeToTSSFromDeployerWithMemo( // parameters to build the reveal transaction commitOutputIdx := uint32(0) - commitAmount, err := zetabitcoin.GetSatoshis(amount) + commitAmount, err := zetabtc.GetSatoshis(amount) require.NoError(r, err) // build the reveal transaction to spend above funds @@ -412,7 +412,7 @@ func (r *E2ERunner) QueryOutboundReceiverAndAmount(txid string) (string, int64) // parse receiver address from pkScript txOutput := revertTx.MsgTx().TxOut[1] pkScript := txOutput.PkScript - receiver, err := zetabitcoin.DecodeScriptP2WPKH(hex.EncodeToString(pkScript), r.BitcoinParams) + receiver, err := zetabtc.DecodeScriptP2WPKH(hex.EncodeToString(pkScript), r.BitcoinParams) require.NoError(r, err) return receiver, txOutput.Value diff --git a/go.mod b/go.mod index e521f0db46..12b967a5ed 100644 --- a/go.mod +++ b/go.mod @@ -163,7 +163,7 @@ require ( github.com/google/gopacket v1.1.19 // indirect github.com/google/orderedcode v0.0.1 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/uuid v1.6.0 github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect @@ -249,8 +249,8 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/client_model v0.4.0 + github.com/prometheus/common v0.42.0 github.com/prometheus/procfs v0.9.0 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index e0d8478fca..68e7db1194 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -8,6 +8,8 @@ import ( "github.com/btcsuite/btcd/chaincfg" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/tonkeeper/tongo/ton" + + "github.com/zeta-chain/node/zetaclient/logs" ) // Validate checks whether the chain is valid @@ -108,6 +110,13 @@ func (chain Chain) IsTONChain() bool { return chain.Consensus == Consensus_catchain_consensus } +func (chain Chain) LogFields() map[string]any { + return map[string]any{ + logs.FieldChain: chain.ChainId, + logs.FieldChainNetwork: chain.Network.String(), + } +} + // DecodeAddressFromChainID decode the address string to bytes // additionalChains is a list of additional chains to search from // in practice, it is used in the protocol to dynamically support new chains without doing an upgrade diff --git a/pkg/chains/chain_filters.go b/pkg/chains/chain_filters.go index 3235fb98b1..c7961860e1 100644 --- a/pkg/chains/chain_filters.go +++ b/pkg/chains/chain_filters.go @@ -18,6 +18,11 @@ func FilterByConsensus(cs Consensus) ChainFilter { return func(chain Chain) bool { return chain.Consensus == cs } } +// FilterByVM filters chains by VM type +func FilterByVM(vm Vm) ChainFilter { + return func(chain Chain) bool { return chain.Vm == vm } +} + // FilterChains applies a list of filters to a list of chains func FilterChains(chainList []Chain, filters ...ChainFilter) []Chain { // Apply each filter to the list of supported chains diff --git a/pkg/chains/chain_filters_test.go b/pkg/chains/chain_filters_test.go index 6254600452..d1fc057572 100644 --- a/pkg/chains/chain_filters_test.go +++ b/pkg/chains/chain_filters_test.go @@ -14,42 +14,57 @@ func TestFilterChains(t *testing.T) { expected func() []chains.Chain }{ { - name: "Filter external chains", + name: "filter external chains", filters: []chains.ChainFilter{chains.FilterExternalChains}, expected: func() []chains.Chain { return chains.ExternalChainList([]chains.Chain{}) }, }, { - name: "Filter gateway observer chains", + name: "filter gateway observer chains", filters: []chains.ChainFilter{chains.FilterByGateway(chains.CCTXGateway_observers)}, expected: func() []chains.Chain { return chains.ChainListByGateway(chains.CCTXGateway_observers, []chains.Chain{}) }, }, { - name: "Filter consensus ethereum chains", + name: "filter consensus ethereum chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_ethereum)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_ethereum, []chains.Chain{}) }, }, { - name: "Filter consensus bitcoin chains", + name: "filter consensus bitcoin chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_bitcoin)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_bitcoin, []chains.Chain{}) }, }, { - name: "Filter consensus solana chains", + name: "filter consensus solana chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_solana_consensus)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_solana_consensus, []chains.Chain{}) }, }, { - name: "Apply multiple filters external chains and gateway observer", + name: "filter evm chains", + filters: []chains.ChainFilter{ + chains.FilterByVM(chains.Vm_evm), + }, + expected: func() []chains.Chain { + var chainList []chains.Chain + for _, chain := range chains.ExternalChainList([]chains.Chain{}) { + if chain.Vm == chains.Vm_evm { + chainList = append(chainList, chain) + } + } + return chainList + }, + }, + { + name: "apply multiple filters external chains and gateway observer", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -66,7 +81,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Apply multiple filters external chains with gateway observer and consensus ethereum", + name: "apply multiple filters external chains with gateway observer and consensus ethereum", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -85,7 +100,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Apply multiple filters external chains with gateway observer and consensus bitcoin", + name: "apply multiple filters external chains with gateway observer and consensus bitcoin", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -116,7 +131,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Test multiple filters in random order", + name: "test multiple filters in random order", filters: []chains.ChainFilter{ chains.FilterByGateway(chains.CCTXGateway_observers), chains.FilterByConsensus(chains.Consensus_ethereum), diff --git a/pkg/coin/coin.go b/pkg/coin/coin.go index 223c2cbed9..e66095ebad 100644 --- a/pkg/coin/coin.go +++ b/pkg/coin/coin.go @@ -34,3 +34,7 @@ func GetAzetaDecFromAmountInZeta(zetaAmount string) (sdk.Dec, error) { zetaToAzetaConvertionFactor := sdk.NewDecFromInt(sdk.NewInt(1000000000000000000)) return zetaDec.Mul(zetaToAzetaConvertionFactor), nil } + +func (c CoinType) SupportsRefund() bool { + return c == CoinType_ERC20 || c == CoinType_Gas || c == CoinType_Zeta +} diff --git a/pkg/coin/coin_test.go b/pkg/coin/coin_test.go index 9f930039f7..504a743b2c 100644 --- a/pkg/coin/coin_test.go +++ b/pkg/coin/coin_test.go @@ -1,14 +1,15 @@ -package coin +package coin_test import ( "testing" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/coin" ) func Test_AzetaPerZeta(t *testing.T) { - require.Equal(t, sdk.NewDec(1e18), AzetaPerZeta()) + require.Equal(t, sdk.NewDec(1e18), coin.AzetaPerZeta()) } func Test_GetAzetaDecFromAmountInZeta(t *testing.T) { @@ -57,7 +58,7 @@ func Test_GetAzetaDecFromAmountInZeta(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - azeta, err := GetAzetaDecFromAmountInZeta(tc.zetaAmount) + azeta, err := coin.GetAzetaDecFromAmountInZeta(tc.zetaAmount) tc.err(t, err) if err == nil { require.Equal(t, tc.azetaAmount, azeta) @@ -71,31 +72,31 @@ func TestGetCoinType(t *testing.T) { tests := []struct { name string coin string - want CoinType + want coin.CoinType wantErr bool }{ { name: "valid coin type 0", coin: "0", - want: CoinType(0), + want: coin.CoinType(0), wantErr: false, }, { name: "valid coin type 1", coin: "1", - want: CoinType(1), + want: coin.CoinType(1), wantErr: false, }, { name: "valid coin type 2", coin: "2", - want: CoinType(2), + want: coin.CoinType(2), wantErr: false, }, { name: "valid coin type 3", coin: "3", - want: CoinType(3), + want: coin.CoinType(3), wantErr: false, }, { @@ -106,7 +107,7 @@ func TestGetCoinType(t *testing.T) { { name: "invalid coin type large number", coin: "4", - want: CoinType(4), + want: coin.CoinType(4), }, { name: "invalid coin type non-integer", @@ -117,7 +118,7 @@ func TestGetCoinType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetCoinType(tt.coin) + got, err := coin.GetCoinType(tt.coin) if tt.wantErr { require.Error(t, err) } else { @@ -127,3 +128,24 @@ func TestGetCoinType(t *testing.T) { }) } } + +func TestCoinType_SupportsRefund(t *testing.T) { + tests := []struct { + name string + c coin.CoinType + want bool + }{ + {"should support refund for ERC20", coin.CoinType_ERC20, true}, + {"should support refund forGas", coin.CoinType_Gas, true}, + {"should support refund forZeta", coin.CoinType_Zeta, true}, + {"should not support refund forCmd", coin.CoinType_Cmd, false}, + {"should not support refund forUnknown", coin.CoinType(100), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.SupportsRefund(); got != tt.want { + t.Errorf("CoinType.SupportsRefund() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/fanout/fanout.go b/pkg/fanout/fanout.go new file mode 100644 index 0000000000..7a5f277842 --- /dev/null +++ b/pkg/fanout/fanout.go @@ -0,0 +1,66 @@ +// Package fanout provides a fan-out pattern implementation. +// It allows one channel to stream data to multiple independent channels. +// Note that context handling is out of the scope of this package. +package fanout + +import "sync" + +const DefaultBuffer = 8 + +// FanOut is a fan-out pattern implementation. +// It is NOT a worker pool, so use it wisely. +type FanOut[T any] struct { + input <-chan T + outputs []chan T + + // outputBuffer chan buffer size for outputs channels. + // This helps with writing to chan in case of slow consumers. + outputBuffer int + + mu sync.RWMutex +} + +// New constructs FanOut +func New[T any](source <-chan T, buf int) *FanOut[T] { + return &FanOut[T]{ + input: source, + outputs: make([]chan T, 0), + outputBuffer: buf, + } +} + +func (f *FanOut[T]) Add() <-chan T { + out := make(chan T, f.outputBuffer) + + f.mu.Lock() + defer f.mu.Unlock() + + f.outputs = append(f.outputs, out) + + return out +} + +// Start starts the fan-out process +func (f *FanOut[T]) Start() { + go func() { + // loop for new data + for data := range f.input { + f.mu.RLock() + for _, output := range f.outputs { + // note that this might spawn lots of goroutines. + // it is a naive approach, but should be more than enough for our use cases. + go func(output chan<- T) { output <- data }(output) + } + f.mu.RUnlock() + } + + // at this point, the input was closed + f.mu.Lock() + defer f.mu.Unlock() + for _, out := range f.outputs { + close(out) + } + + f.outputs = nil + }() +} diff --git a/pkg/fanout/fanout_test.go b/pkg/fanout/fanout_test.go new file mode 100644 index 0000000000..884d122e30 --- /dev/null +++ b/pkg/fanout/fanout_test.go @@ -0,0 +1,72 @@ +package fanout + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestFanOut(t *testing.T) { + // ARRANGE + // Given an input + input := make(chan int) + + // Given a fanout + f := New(input, DefaultBuffer) + + // That has 3 outputs + out1 := f.Add() + out2 := f.Add() + out3 := f.Add() + + // Given a wait group + wg := sync.WaitGroup{} + wg.Add(3) + + // Given a sample number + var total int32 + + // Given a consumer + consumer := func(out <-chan int, name string, lag time.Duration) { + defer wg.Done() + var local int32 + for i := range out { + // simulate some work + time.Sleep(lag) + + local += int32(i) + t.Logf("%s: received %d", name, i) + } + + // add only if input was closed + atomic.AddInt32(&total, local) + } + + // ACT + f.Start() + + // Write to the channel + go func() { + for i := 1; i <= 10; i++ { + input <- i + t.Logf("fan-out: sent %d", i) + time.Sleep(50 * time.Millisecond) + } + + close(input) + }() + + go consumer(out1, "out1: fast consumer", 10*time.Millisecond) + go consumer(out2, "out2: average consumer", 60*time.Millisecond) + go consumer(out3, "out3: slow consumer", 150*time.Millisecond) + + wg.Wait() + + // ASSERT + // Check that total is valid + // total == sum(1...10) * 3 = n(n+1)/2 * 3 = 55 * 3 = 165 + require.Equal(t, int32(165), total) +} diff --git a/pkg/graceful/graceful.go b/pkg/graceful/graceful.go index 6f9ffe4cf4..3cfdc3e602 100644 --- a/pkg/graceful/graceful.go +++ b/pkg/graceful/graceful.go @@ -147,9 +147,9 @@ func (p *Process) ShutdownNow() { } } -// panicToErr converts panic to error WITH exact line of panic. +// panicToErr converts recoverVal to error WITH exact line of panic. // Note the offset should be determined empirically. -func panicToErr(panic any, offset int) error { +func panicToErr(recoverVal any, offset int) error { stack := string(debug.Stack()) lines := strings.Split(stack, "\n") line := "" @@ -158,7 +158,7 @@ func panicToErr(panic any, offset int) error { line = strings.TrimSpace(lines[offset]) } - return fmt.Errorf("panic: %v at %s", panic, line) + return fmt.Errorf("panic: %v at %s", recoverVal, line) } // NewSigChan creates a new signal channel. diff --git a/pkg/memo/memo_test.go b/pkg/memo/memo_test.go index 4eabd6a18f..4979b29d41 100644 --- a/pkg/memo/memo_test.go +++ b/pkg/memo/memo_test.go @@ -2,11 +2,13 @@ package memo_test import ( "encoding/hex" + mathrand "math/rand" "testing" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/testutil/sample" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) @@ -306,6 +308,9 @@ func Test_Memo_DecodeFromBytes(t *testing.T) { func Test_DecodeLegacyMemoHex(t *testing.T) { expectedShortMsgResult, err := hex.DecodeString("1a2b3c4d5e6f708192a3b4c5d6e7f808") + r := mathrand.New(mathrand.NewSource(42)) + address, data, memoHex := sample.MemoFromRand(r) + require.NoError(t, err) tests := []struct { name string @@ -324,6 +329,8 @@ func Test_DecodeLegacyMemoHex(t *testing.T) { {"empty msg", "", common.Address{}, nil, false}, {"invalid hex", "invalidHex", common.Address{}, nil, true}, {"short msg", "1a2b3c4d5e6f708192a3b4c5d6e7f808", common.Address{}, expectedShortMsgResult, false}, + {"random message", sample.EthAddress().String(), common.Address{}, nil, true}, + {"random message with hex encoding", memoHex, address, data, false}, } for _, tt := range tests { @@ -339,3 +346,21 @@ func Test_DecodeLegacyMemoHex(t *testing.T) { }) } } + +func Test_DecodeLegacyMemoHex_Random(t *testing.T) { + r := mathrand.New(mathrand.NewSource(42)) + + // Generate a random memo hex + randomMemo := common.BytesToAddress([]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}). + Hex() + randomData := []byte(sample.StringRandom(r, 10)) + randomMemoHex := hex.EncodeToString(append(common.FromHex(randomMemo), randomData...)) + + // Decode the random memo hex + addr, data, err := memo.DecodeLegacyMemoHex(randomMemoHex) + + // Validate the results + require.NoError(t, err) + require.Equal(t, common.HexToAddress(randomMemo), addr) + require.Equal(t, randomData, data) +} diff --git a/pkg/scheduler/opts.go b/pkg/scheduler/opts.go index 8e5d54e370..44ec8493c8 100644 --- a/pkg/scheduler/opts.go +++ b/pkg/scheduler/opts.go @@ -26,17 +26,20 @@ func LogFields(fields map[string]any) Opt { // Interval sets initial task interval. func Interval(interval time.Duration) Opt { - return func(_ *Task, opts *taskOpts) { opts.interval = interval } + return func(_ *Task, opts *taskOpts) { opts.interval = normalizeInterval(interval) } } -// Skipper sets task skipper function +// Skipper sets task skipper function. If it returns true, the task is skipped. func Skipper(skipper func() bool) Opt { return func(t *Task, _ *taskOpts) { t.skipper = skipper } } -// IntervalUpdater sets interval updater function. +// IntervalUpdater sets interval updater function. Overrides Interval. func IntervalUpdater(intervalUpdater func() time.Duration) Opt { - return func(_ *Task, opts *taskOpts) { opts.intervalUpdater = intervalUpdater } + return func(_ *Task, opts *taskOpts) { + opts.interval = normalizeInterval(intervalUpdater()) + opts.intervalUpdater = intervalUpdater + } } // BlockTicker makes Task to listen for new zeta blocks diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 2328cbddd7..c6bbb5c241 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -107,6 +107,18 @@ func (s *Scheduler) Register(ctx context.Context, exec Executable, opts ...Opt) return task } +func (s *Scheduler) Tasks() map[uuid.UUID]*Task { + s.mu.RLock() + defer s.mu.RUnlock() + + copied := make(map[uuid.UUID]*Task, len(s.tasks)) + for k, v := range s.tasks { + copied[k] = v + } + + return copied +} + // Stop stops all tasks. func (s *Scheduler) Stop() { s.StopGroup("") @@ -132,6 +144,11 @@ func (s *Scheduler) StopGroup(group Group) { return } + s.logger.Info(). + Int("tasks", len(selectedTasks)). + Str("group", string(group)). + Msg("Stopping scheduler group") + // Stop all selected tasks concurrently var wg sync.WaitGroup wg.Add(len(selectedTasks)) @@ -161,6 +178,14 @@ func (t *Task) Stop() { t.logger.Info().Int64("time_taken_ms", timeTakenMS).Msg("Stopped scheduler task") } +func (t *Task) Group() Group { + return t.group +} + +func (t *Task) Name() string { + return t.name +} + // execute executes Task with additional logging and metrics. func (t *Task) execute(ctx context.Context) error { startedAt := time.Now().UTC() @@ -209,3 +234,12 @@ func newTickable(task *Task, opts *taskOpts) tickable { task.logger, ) } + +// normalizeInterval ensures that the interval is positive to prevent panics. +func normalizeInterval(dur time.Duration) time.Duration { + if dur > 0 { + return dur + } + + return time.Second +} diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go index a993bc875a..9d82e8f7e3 100644 --- a/pkg/scheduler/scheduler_test.go +++ b/pkg/scheduler/scheduler_test.go @@ -157,12 +157,17 @@ func TestScheduler(t *testing.T) { // Interval updater that increases the interval by 50ms on each counter increment. intervalUpdater := func() time.Duration { - return time.Duration(atomic.LoadInt32(&counter)) * 50 * time.Millisecond + cnt := atomic.LoadInt32(&counter) + if cnt == 0 { + return time.Millisecond + } + + return time.Duration(cnt) * 50 * time.Millisecond } // ACT // Register task and stop it after x1.5 interval. - task := ts.scheduler.Register(ts.ctx, exec, Interval(time.Millisecond), IntervalUpdater(intervalUpdater)) + task := ts.scheduler.Register(ts.ctx, exec, IntervalUpdater(intervalUpdater)) time.Sleep(time.Second) task.Stop() diff --git a/pkg/scheduler/tickers.go b/pkg/scheduler/tickers.go index 613194c44b..228f11f82c 100644 --- a/pkg/scheduler/tickers.go +++ b/pkg/scheduler/tickers.go @@ -31,7 +31,7 @@ func newIntervalTicker( if intervalUpdater != nil { // noop if interval is not changed - t.SetInterval(intervalUpdater()) + t.SetInterval(normalizeInterval(intervalUpdater())) } return nil diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index 9ec0d4cb06..3fb98551d1 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -98,7 +98,7 @@ func Run(ctx context.Context, interval time.Duration, task Task, opts ...Opt) er return New(interval, task, opts...).Start(ctx) } -// Run runs the ticker by blocking current goroutine. It also invokes BEFORE ticker starts. +// Start runs the ticker by blocking current goroutine. It also invokes BEFORE ticker starts. // Stops when (if any): // - context is done (returns ctx.Err()) // - task returns an error or panics @@ -139,7 +139,7 @@ func (t *Ticker) Start(ctx context.Context) (err error) { case <-ctx.Done(): // if task is finished (i.e. last tick completed BEFORE ticker.Stop(), // then we need to return nil) - if t.stopped { + if t.isStopped() { return nil } return ctx.Err() @@ -214,11 +214,20 @@ func (t *Ticker) setStopState() { t.ctxCancel() t.stopped = true - t.ticker.Stop() + if t.ticker != nil { + t.ticker.Stop() + } t.logger.Info().Msgf("Ticker stopped") } +func (t *Ticker) isStopped() bool { + t.stateMu.Lock() + defer t.stateMu.Unlock() + + return t.stopped +} + // DurationFromUint64Seconds converts uint64 of seconds to time.Duration. func DurationFromUint64Seconds(seconds uint64) time.Duration { // #nosec G115 seconds should be in range and is not user controlled diff --git a/rpc/types/utils.go b/rpc/types/utils.go index a17f3af2a2..b7195478d1 100644 --- a/rpc/types/utils.go +++ b/rpc/types/utils.go @@ -303,9 +303,9 @@ func BaseFeeFromEvents(events []abci.Event) *big.Int { // CheckTxFee is an internal function used to check whether the fee of // the given transaction is _reasonable_(under the cap). -func CheckTxFee(gasPrice *big.Int, gas uint64, cap float64) error { +func CheckTxFee(gasPrice *big.Int, gas uint64, feeCap float64) error { // Short circuit if there is no cap for transaction fee at all. - if cap == 0 { + if feeCap == 0 { return nil } totalfee := new(big.Float).SetInt(new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gas))) @@ -315,8 +315,8 @@ func CheckTxFee(gasPrice *big.Int, gas uint64, cap float64) error { feeEth := new(big.Float).Quo(totalfee, oneToken) // no need to check error from parsing feeFloat, _ := feeEth.Float64() - if feeFloat > cap { - return fmt.Errorf("tx fee (%.2f ether) exceeds the configured cap (%.2f ether)", feeFloat, cap) + if feeFloat > feeCap { + return fmt.Errorf("tx fee (%.2f ether) exceeds the configured cap (%.2f ether)", feeFloat, feeCap) } return nil } diff --git a/scripts/fmt.sh b/scripts/fmt.sh index 7d6ef5eb9f..bffad41ab1 100755 --- a/scripts/fmt.sh +++ b/scripts/fmt.sh @@ -6,7 +6,7 @@ set -e if ! command -v golangci-lint &> /dev/null then echo "golangci-lint is not found, installing..." - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1 + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 fi if ! command -v golines &> /dev/null diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 818250cbaf..6309753011 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -42,8 +42,6 @@ func init() { zetasimulation.GetSimulatorFlags() } -// StoreKeysPrefixes defines a struct used in comparing two keys for two different stores -// SkipPrefixes is used to skip certain prefixes when comparing the stores type StoreKeysPrefixes struct { A storetypes.StoreKey B storetypes.StoreKey @@ -66,6 +64,12 @@ func interBlockCacheOpt() func(*baseapp.BaseApp) { } // TestAppStateDeterminism runs a full application simulation , and produces multiple blocks as per the config +// It does the following +// 1. It runs the simulation multiple times with the same seed value +// 2. It checks the apphash at the end of each run +// 3. It compares the apphash at the end of each run to check for determinism +// 4. Repeat steps 1-3 for multiple seeds + // It checks the determinism of the application by comparing the apphash at the end of each run to other runs // The following test certifies that , for the same set of operations ( irrespective of what the operations are ) , // we would reach the same final state if the initial state is the same @@ -179,7 +183,10 @@ func TestAppStateDeterminism(t *testing.T) { } // TestFullAppSimulation runs a full simApp simulation with the provided configuration. -// At the end of the run it tries to export the genesis state to make sure the export works. +// This test does the following +// 1. It runs a full simulation with the provided configuration +// 2. It exports the state and validators +// 3. Verifies that the run and export were successful func TestFullAppSimulation(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -252,6 +259,15 @@ func TestFullAppSimulation(t *testing.T) { zetasimulation.PrintStats(db) } +// TestAppImportExport tests the application simulation after importing the state exported from a previous.At a high level,it does the following +// 1. It runs a full simulation and exports the state +// 2. It creates a new app, and db +// 3. It imports the exported state into the new app +// 4. It compares the key value pairs for the two apps.The comparison function takes a list of keys to skip as an input as well +// a. First app which ran the simulation +// b. Second app which imported the state + +// This can verify the export and import process do not modify the state in anyway irrespective of the operations performed func TestAppImportExport(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -270,7 +286,6 @@ func TestAppImportExport(t *testing.T) { t.Skip("skipping application simulation") } require.NoError(t, err, "simulation setup failed") - t.Cleanup(func() { if err := db.Close(); err != nil { require.NoError(t, err, "Error closing new database") @@ -375,7 +390,6 @@ func TestAppImportExport(t *testing.T) { ChainID: SimAppChainID, }) - t.Log("initializing genesis for the new app using exported genesis state") // Use genesis state from the first app to initialize the second app newSimApp.ModuleManager().InitGenesis(ctxNewSimApp, newSimApp.AppCodec(), genesisState) newSimApp.StoreConsensusParams(ctxNewSimApp, exported.ConsensusParams) @@ -390,7 +404,7 @@ func TestAppImportExport(t *testing.T) { // We will need to explore this further to find a definitive answer // TODO:https://github.com/zeta-chain/node/issues/3263 - // {simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, + //{simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, { simApp.GetKey(stakingtypes.StoreKey), newSimApp.GetKey(stakingtypes.StoreKey), [][]byte{ @@ -439,6 +453,12 @@ func TestAppImportExport(t *testing.T) { } } +// TestAppSimulationAfterImport tests the application simulation after importing the state exported from a previous simulation run. +// It does the following steps +// 1. It runs a full simulation and exports the state +// 2. It creates a new app, and db +// 3. It imports the exported state into the new app +// 4. It runs a simulation on the new app and verifies that there is no error in the second simulation func TestAppSimulationAfterImport(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -516,6 +536,7 @@ func TestAppSimulationAfterImport(t *testing.T) { exported, err := simApp.ExportAppStateAndValidators(true, []string{}, []string{}) require.NoError(t, err) + // Setup a new app with new database and directory newDB, newDir, _, _, err := cosmossimutils.SetupSimulation( config, SimDBBackend+"_new", @@ -523,9 +544,7 @@ func TestAppSimulationAfterImport(t *testing.T) { zetasimulation.FlagVerboseValue, zetasimulation.FlagEnabledValue, ) - require.NoError(t, err, "simulation setup failed") - t.Cleanup(func() { if err := newDB.Close(); err != nil { require.NoError(t, err, "Error closing new database") @@ -534,7 +553,6 @@ func TestAppSimulationAfterImport(t *testing.T) { require.NoError(t, err, "Error removing directory") } }) - newSimApp, err := zetasimulation.NewSimApp( logger, newDB, @@ -544,12 +562,14 @@ func TestAppSimulationAfterImport(t *testing.T) { ) require.NoError(t, err) + // Initialize the new app with the exported genesis state of the first run t.Log("Importing genesis into the new app") newSimApp.InitChain(abci.RequestInitChain{ ChainId: SimAppChainID, AppStateBytes: exported.AppState, }) + // Run simulation on the new app stopEarly, simParams, simErr = simulation.SimulateFromSeed( t, os.Stdout, @@ -567,5 +587,5 @@ func TestAppSimulationAfterImport(t *testing.T) { config, simApp.AppCodec(), ) - require.NoError(t, err) + require.NoError(t, simErr) } diff --git a/simulation/state.go b/simulation/state.go index 9bf2c24949..4e76f7d623 100644 --- a/simulation/state.go +++ b/simulation/state.go @@ -3,17 +3,12 @@ package simulation import ( "encoding/json" "fmt" - "io" "math/rand" - "os" "testing" "time" "cosmossdk.io/math" - cmtjson "github.com/cometbft/cometbft/libs/json" - tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -23,9 +18,12 @@ import ( "github.com/stretchr/testify/require" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" - zetaapp "github.com/zeta-chain/node/app" + zetachains "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/crypto" "github.com/zeta-chain/node/testutil/sample" authoritytypes "github.com/zeta-chain/node/x/authority/types" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -36,7 +34,10 @@ const ( InitiallyBondedValidators = "initially_bonded_validators" ) -func updateBankState( +// extractBankGenesisState extracts and updates the bank genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool +func extractBankGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -67,7 +68,9 @@ func updateBankState( return bankState } -func updateEVMState( +// extractEVMGenesisState extracts and updates the evm genesis state. +// It replaces the EvmDenom with BondDenom +func extractEVMGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -85,7 +88,11 @@ func updateEVMState( return evmState } -func updateStakingState( +// extractStakingGenesisState extracts and updates the staking genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool +// It additionally returns the non-bonded coins as well +func extractStakingGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -112,7 +119,16 @@ func updateStakingState( return stakingState, notBondedCoins } -func updateObserverState( +// extractObserverGenesisState extracts and updates the observer genesis state. +// It adds the following +// - A random observer set which is a subset of the current validator set +// - A randomised node account for each observer +// - A random TSS +// - A TSS history for the TSS created +// - Chain nonces for each chain +// - Pending nonces for each chain +// - Crosschain flags, inbound and outbound enabled +func extractObserverGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -125,6 +141,7 @@ func updateObserverState( observerState := new(observertypes.GenesisState) cdc.MustUnmarshalJSON(observerStateBz, observerState) + // Create an observer set as a subset of the current validator set observers := make([]string, 0) for _, validator := range validators { accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) @@ -138,24 +155,67 @@ func updateObserverState( observers[i], observers[j] = observers[j], observers[i] }) - numObservers := r.Intn(11) + 5 + numObservers := r.Intn(21) + 5 if numObservers > len(observers) { numObservers = len(observers) } observers = observers[:numObservers] + // Create node account list for the observers set + nodeAccounts := make([]*observertypes.NodeAccount, len(observers)) + for i, observer := range observers { + nodeAccounts[i] = &observertypes.NodeAccount{ + Operator: observer, + GranteeAddress: observer, + GranteePubkey: &crypto.PubKeySet{}, + NodeStatus: observertypes.NodeStatus_Active, + } + } + // Create a random tss + tss, err := sample.TSSFromRand(r) + require.NoError(t, err) + tss.OperatorAddressList = observers + + // Create a tss history + tssHistory := make([]observertypes.TSS, 0) + tssHistory = append(tssHistory, tss) + + // Create chainnonces and pendingnonces + chains := zetachains.DefaultChainsList() + chainsNonces := make([]observertypes.ChainNonces, 0) + pendingNonces := make([]observertypes.PendingNonces, 0) + for _, chain := range chains { + chainNonce := observertypes.ChainNonces{ + ChainId: chain.ChainId, + Nonce: 0, + } + chainsNonces = append(chainsNonces, chainNonce) + pendingNonce := observertypes.PendingNonces{ + NonceLow: 0, + NonceHigh: 0, + ChainId: chain.ChainId, + Tss: tss.TssPubkey, + } + pendingNonces = append(pendingNonces, pendingNonce) + } + + observerState.Tss = &tss observerState.Observers.ObserverList = observers + observerState.NodeAccountList = nodeAccounts observerState.CrosschainFlags.IsInboundEnabled = true observerState.CrosschainFlags.IsOutboundEnabled = true - - tss := sample.TSSFromRand(t, r) - tss.OperatorAddressList = observers - observerState.Tss = &tss + observerState.ChainNonces = chainsNonces + observerState.PendingNonces = pendingNonces + observerState.TssHistory = tssHistory return observerState } -func updateAuthorityState( +// extractAuthorityGenesisState extracts and updates the authority genesis state. +// It adds the following +// - A policy for each policy type; +// the address is a random account address selected from the simulation accounts list +func extractAuthorityGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -190,7 +250,40 @@ func updateAuthorityState( return authorityState } -func updateFungibleState( +// extractCrosschainGenesisState extracts and updates the crosschain genesis state. +// It adds the following +// - A gas price list for each chain +func extractCrosschainGenesisState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + r *rand.Rand, +) *crosschaintypes.GenesisState { + crossChainStateBz, ok := rawState[crosschaintypes.ModuleName] + require.True(t, ok, "crosschain genesis state is missing") + + crossChainState := new(crosschaintypes.GenesisState) + cdc.MustUnmarshalJSON(crossChainStateBz, crossChainState) + + // Add a gasprice for each chain + chains := zetachains.DefaultChainsList() + gasPriceList := make([]*crosschaintypes.GasPrice, len(chains)) + for i, chain := range chains { + gasPriceList[i] = sample.GasPriceFromRand(r, chain.ChainId) + } + + crossChainState.GasPriceList = gasPriceList + + return crossChainState +} + +// extractFungibleGenesisState extracts and updates the fungible genesis state. +// It adds the following +// - A random system contract address +// - A random connector zevm address +// - A random gateway address +// - A foreign coin for each chain under the default chain list. +func extractFungibleGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -207,9 +300,28 @@ func updateFungibleState( Gateway: sample.EthAddressFromRand(r).String(), } + foreignCoins := make([]fungibletypes.ForeignCoins, 0) + chains := zetachains.DefaultChainsList() + + for _, chain := range chains { + foreignCoin := fungibletypes.ForeignCoins{ + ForeignChainId: chain.ChainId, + Asset: sample.EthAddressFromRand(r).String(), + Zrc20ContractAddress: sample.EthAddressFromRand(r).String(), + Decimals: 18, + Paused: false, + CoinType: coin.CoinType_Gas, + LiquidityCap: math.ZeroUint(), + } + foreignCoins = append(foreignCoins, foreignCoin) + } + fungibleState.ForeignCoinsList = foreignCoins + return fungibleState } +// updateRawState updates the raw genesis state for the application. +// This is used to inject values needed to run the simulation tests. func updateRawState( t *testing.T, rawState map[string]json.RawMessage, @@ -217,12 +329,12 @@ func updateRawState( r *rand.Rand, accs []simtypes.Account, ) { - stakingState, notBondedCoins := updateStakingState(t, rawState, cdc) - bankState := updateBankState(t, rawState, cdc, notBondedCoins) - evmState := updateEVMState(t, rawState, cdc, stakingState.Params.BondDenom) - observerState := updateObserverState(t, rawState, cdc, r, stakingState.Validators) - authorityState := updateAuthorityState(t, rawState, cdc, r, accs) - fungibleState := updateFungibleState(t, rawState, cdc, r) + stakingState, notBondedCoins := extractStakingGenesisState(t, rawState, cdc) + bankState := extractBankGenesisState(t, rawState, cdc, notBondedCoins) + evmState := extractEVMGenesisState(t, rawState, cdc, stakingState.Params.BondDenom) + observerState := extractObserverGenesisState(t, rawState, cdc, r, stakingState.Validators) + authorityState := extractAuthorityGenesisState(t, rawState, cdc, r, accs) + fungibleState := extractFungibleGenesisState(t, rawState, cdc, r) rawState[stakingtypes.ModuleName] = cdc.MustMarshalJSON(stakingState) rawState[banktypes.ModuleName] = cdc.MustMarshalJSON(bankState) @@ -230,6 +342,7 @@ func updateRawState( rawState[observertypes.ModuleName] = cdc.MustMarshalJSON(observerState) rawState[authoritytypes.ModuleName] = cdc.MustMarshalJSON(authorityState) rawState[fungibletypes.ModuleName] = cdc.MustMarshalJSON(fungibleState) + rawState[crosschaintypes.ModuleName] = cdc.MustMarshalJSON(extractCrosschainGenesisState(t, rawState, cdc, r)) } // AppStateFn returns the initial application state using a genesis or the simulation parameters. @@ -253,7 +366,7 @@ func AppStateFn( chainID = config.ChainID - // if exported state is provided then use it + // if exported state is provided, then use it if exportedState != nil { return exportedState, accs, chainID, genesisTimestamp } @@ -342,61 +455,5 @@ func AppStateRandomizedFn( if err != nil { panic(err) } - return appState, accs } - -// AppStateFromGenesisFileFn util function to generate the genesis AppState -// from a genesis.json file. -func AppStateFromGenesisFileFn( - r io.Reader, - cdc codec.JSONCodec, - genesisFile string, -) (tmtypes.GenesisDoc, []simtypes.Account, error) { - bytes, err := os.ReadFile(genesisFile) // #nosec G304 -- genesisFile value is controlled - if err != nil { - panic(err) - } - - var genesis tmtypes.GenesisDoc - // NOTE: Comet uses a custom JSON decoder for GenesisDoc - err = cmtjson.Unmarshal(bytes, &genesis) - if err != nil { - panic(err) - } - - var appState zetaapp.GenesisState - err = json.Unmarshal(genesis.AppState, &appState) - if err != nil { - panic(err) - } - - var authGenesis authtypes.GenesisState - if appState[authtypes.ModuleName] != nil { - cdc.MustUnmarshalJSON(appState[authtypes.ModuleName], &authGenesis) - } - - newAccs := make([]simtypes.Account, len(authGenesis.Accounts)) - for i, acc := range authGenesis.Accounts { - // Pick a random private key, since we don't know the actual key - // This should be fine as it's only used for mock Tendermint validators - // and these keys are never actually used to sign by mock Tendermint. - privkeySeed := make([]byte, 15) - if _, err := r.Read(privkeySeed); err != nil { - panic(err) - } - - privKey := secp256k1.GenPrivKeyFromSecret(privkeySeed) - - a, ok := acc.GetCachedValue().(authtypes.AccountI) - if !ok { - return genesis, nil, fmt.Errorf("expected account") - } - - // create simulator accounts - simAcc := simtypes.Account{PrivKey: privKey, PubKey: privKey.PubKey(), Address: a.GetAddress()} - newAccs[i] = simAcc - } - - return genesis, newAccs, nil -} diff --git a/testutil/keeper/mocks/crosschain/authority.go b/testutil/keeper/mocks/crosschain/authority.go index 7dafb1331b..9f2a9a8837 100644 --- a/testutil/keeper/mocks/crosschain/authority.go +++ b/testutil/keeper/mocks/crosschain/authority.go @@ -3,8 +3,10 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" chains "github.com/zeta-chain/node/pkg/chains" + authoritytypes "github.com/zeta-chain/node/x/authority/types" + + mock "github.com/stretchr/testify/mock" types "github.com/cosmos/cosmos-sdk/types" ) @@ -52,6 +54,34 @@ func (_m *CrosschainAuthorityKeeper) GetAdditionalChainList(ctx types.Context) [ return r0 } +// GetPolicies provides a mock function with given fields: ctx +func (_m *CrosschainAuthorityKeeper) GetPolicies(ctx types.Context) (authoritytypes.Policies, bool) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetPolicies") + } + + var r0 authoritytypes.Policies + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context) (authoritytypes.Policies, bool)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) authoritytypes.Policies); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(authoritytypes.Policies) + } + + if rf, ok := ret.Get(1).(func(types.Context) bool); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // NewCrosschainAuthorityKeeper creates a new instance of CrosschainAuthorityKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCrosschainAuthorityKeeper(t interface { diff --git a/testutil/keeper/mocks/observer/staking.go b/testutil/keeper/mocks/observer/staking.go index 90007b6c35..af2537bac0 100644 --- a/testutil/keeper/mocks/observer/staking.go +++ b/testutil/keeper/mocks/observer/staking.go @@ -15,6 +15,26 @@ type ObserverStakingKeeper struct { mock.Mock } +// GetAllValidators provides a mock function with given fields: ctx +func (_m *ObserverStakingKeeper) GetAllValidators(ctx types.Context) []stakingtypes.Validator { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetAllValidators") + } + + var r0 []stakingtypes.Validator + if rf, ok := ret.Get(0).(func(types.Context) []stakingtypes.Validator); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]stakingtypes.Validator) + } + } + + return r0 +} + // GetDelegation provides a mock function with given fields: ctx, delAddr, valAddr func (_m *ObserverStakingKeeper) GetDelegation(ctx types.Context, delAddr types.AccAddress, valAddr types.ValAddress) (stakingtypes.Delegation, bool) { ret := _m.Called(ctx, delAddr, valAddr) diff --git a/testutil/sample/crosschain.go b/testutil/sample/crosschain.go index 82e683604a..b3ed7d5ee8 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -2,6 +2,7 @@ package sample import ( "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "math/big" @@ -11,7 +12,7 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" @@ -54,6 +55,36 @@ func RateLimiterFlags() types.RateLimiterFlags { } } +func RateLimiterFlagsFromRand(r *rand.Rand) types.RateLimiterFlags { + return types.RateLimiterFlags{ + Enabled: true, + Window: r.Int63(), + Rate: sdk.NewUint(r.Uint64()), + Conversions: []types.Conversion{ + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + }, + } +} + // CustomAssetRate creates a custom asset rate with the given parameters func CustomAssetRate( chainID int64, @@ -118,6 +149,25 @@ func GasPriceWithChainID(t *testing.T, chainID int64) types.GasPrice { } } +func GasPriceFromRand(r *rand.Rand, chainID int64) *types.GasPrice { + var price uint64 + for price == 0 { + maxGasPrice := uint64(1000 * 1e9) // 1000 Gwei + price = uint64(1e9) + r.Uint64()%maxGasPrice + } + // Select priority fee between 0 and price + priorityFee := r.Uint64() % price + return &types.GasPrice{ + Creator: "", + ChainId: chainID, + Signers: []string{AccAddressFromRand(r)}, + BlockNums: []uint64{r.Uint64()}, + Prices: []uint64{price}, + MedianIndex: 0, + PriorityFees: []uint64{priorityFee}, + } +} + func InboundParams(r *rand.Rand) *types.InboundParams { return &types.InboundParams{ Sender: EthAddress().String(), @@ -314,8 +364,10 @@ func InboundVote(coinType coin.CoinType, from, to int64) types.MsgVoteInbound { } // InboundVoteFromRand creates a simulated inbound vote message. This function uses the provided source of randomness to generate the vote -func InboundVoteFromRand(coinType coin.CoinType, from, to int64, r *rand.Rand) types.MsgVoteInbound { - EthAddress() +func InboundVoteFromRand(from, to int64, r *rand.Rand, asset string) types.MsgVoteInbound { + coinType := CoinTypeFromRand(r) + _, _, memo := MemoFromRand(r) + return types.MsgVoteInbound{ Creator: "", Sender: EthAddressFromRand(r).String(), @@ -323,19 +375,118 @@ func InboundVoteFromRand(coinType coin.CoinType, from, to int64, r *rand.Rand) t Receiver: EthAddressFromRand(r).String(), ReceiverChain: to, Amount: math.NewUint(r.Uint64()), - Message: base64.StdEncoding.EncodeToString(RandomBytes(r)), + Message: memo, InboundBlockHeight: r.Uint64(), CallOptions: &types.CallOptions{ GasLimit: 1000000000, }, - InboundHash: ethcommon.BytesToHash(RandomBytes(r)).String(), - CoinType: coinType, - TxOrigin: EthAddressFromRand(r).String(), - Asset: StringRandom(r, 32), - EventIndex: r.Uint64(), + InboundHash: common.BytesToHash(RandomBytes(r)).String(), + CoinType: coinType, + TxOrigin: EthAddressFromRand(r).String(), + Asset: asset, + EventIndex: r.Uint64(), + ProtocolContractVersion: ProtocolVersionFromRand(r), } } +func ProtocolVersionFromRand(r *rand.Rand) types.ProtocolContractVersion { + versions := []types.ProtocolContractVersion{types.ProtocolContractVersion_V1, types.ProtocolContractVersion_V2} + return versions[r.Intn(len(versions))] +} + +func CoinTypeFromRand(r *rand.Rand) coin.CoinType { + coinTypes := []coin.CoinType{coin.CoinType_Gas, coin.CoinType_ERC20, coin.CoinType_Zeta} + coinType := coinTypes[r.Intn(len(coinTypes))] + return coinType +} + +func MemoFromRand(r *rand.Rand) (common.Address, []byte, string) { + randomMemo := common.BytesToAddress([]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}). + Hex() + randomData := []byte(StringRandom(r, 10)) + memoHex := hex.EncodeToString(append(common.FromHex(randomMemo), randomData...)) + return common.HexToAddress(randomMemo), randomData, memoHex +} + +func CCTXfromRand(r *rand.Rand, + creator string, + index string, + to int64, + from int64, + tssPubkey string, + asset string, +) types.CrossChainTx { + coinType := CoinTypeFromRand(r) + + amount := math.NewUint(uint64(r.Int63())) + inbound := &types.InboundParams{ + Sender: EthAddressFromRand(r).String(), + SenderChainId: from, + TxOrigin: EthAddressFromRand(r).String(), + CoinType: coinType, + Asset: asset, + Amount: amount, + ObservedHash: StringRandom(r, 32), + ObservedExternalHeight: r.Uint64(), + BallotIndex: StringRandom(r, 32), + FinalizedZetaHeight: r.Uint64(), + } + + outbound := &types.OutboundParams{ + Receiver: EthAddressFromRand(r).String(), + ReceiverChainId: to, + CoinType: coinType, + Amount: math.NewUint(uint64(r.Int63())), + TssNonce: 0, + TssPubkey: tssPubkey, + CallOptions: &types.CallOptions{ + GasLimit: r.Uint64(), + }, + GasPrice: math.NewUint(uint64(r.Int63())).String(), + Hash: StringRandom(r, 32), + BallotIndex: StringRandom(r, 32), + ObservedExternalHeight: r.Uint64(), + GasUsed: 100, + EffectiveGasPrice: math.NewInt(r.Int63()), + EffectiveGasLimit: 100, + } + + cctx := types.CrossChainTx{ + Creator: creator, + Index: index, + ZetaFees: sdk.NewUint(1), + RelayedMessage: base64.StdEncoding.EncodeToString(RandomBytes(r)), + CctxStatus: &types.Status{ + IsAbortRefunded: false, + Status: types.CctxStatus_PendingOutbound, + }, + InboundParams: inbound, + OutboundParams: []*types.OutboundParams{outbound}, + ProtocolContractVersion: ProtocolVersionFromRand(r), + } + return cctx +} + +func OutboundVoteSim(r *rand.Rand, + cctx types.CrossChainTx, +) (types.CrossChainTx, types.MsgVoteOutbound) { + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: chains.ReceiveStatus_success, + Creator: cctx.Creator, + ObservedOutboundHash: common.BytesToHash(EthAddressFromRand(r).Bytes()).String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: cctx.GetCurrentOutboundParam().ObservedExternalHeight, + ObservedOutboundEffectiveGasPrice: cctx.GetCurrentOutboundParam().EffectiveGasPrice, + ObservedOutboundGasUsed: cctx.GetCurrentOutboundParam().GasUsed, + CoinType: cctx.InboundParams.CoinType, + } + + return cctx, msg +} + func ZRC20Withdrawal(to []byte, value *big.Int) *zrc20.ZRC20Withdrawal { return &zrc20.ZRC20Withdrawal{ From: EthAddress(), diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index 93b7db2d1e..6ebf295010 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -2,6 +2,7 @@ package sample import ( "crypto/ecdsa" + cryptoed25519 "crypto/ed25519" "math/big" "math/rand" "strconv" @@ -34,6 +35,15 @@ func PubKeySet() *crypto.PubKeySet { return &pubKeySet } +func Ed25519PrivateKeyFromRand(r *rand.Rand) (*ed25519.PrivKey, error) { + randomBytes := make([]byte, 32) + _, err := r.Read(randomBytes) + if err != nil { + return nil, err + } + return ed25519.GenPrivKeyFromSecret(randomBytes), nil +} + // PubKeyString returns a sample public key string func PubKeyString() string { priKey := ed25519.GenPrivKey() @@ -48,6 +58,22 @@ func PubKeyString() string { return pubkey.String() } +func PubkeyStringFromRand(r *rand.Rand) (string, error) { + priKey, err := Ed25519PrivateKeyFromRand(r) + if err != nil { + return "", err + } + s, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, priKey.PubKey()) + if err != nil { + return "", err + } + pubkey, err := crypto.NewPubKey(s) + if err != nil { + return "", err + } + return pubkey.String(), nil +} + // PrivKeyAddressPair returns a private key, address pair func PrivKeyAddressPair() (*ed25519.PrivKey, sdk.AccAddress) { privKey := ed25519.GenPrivKey() @@ -92,6 +118,16 @@ func SolanaPrivateKey(t *testing.T) solana.PrivateKey { return privKey } +func SolanaPrivateKeyFromRand(r *rand.Rand) (solana.PrivateKey, error) { + pub, priv, err := cryptoed25519.GenerateKey(r) + if err != nil { + return nil, err + } + var publicKey cryptoed25519.PublicKey + copy(publicKey[:], pub) + return solana.PrivateKey(priv), nil +} + // SolanaAddress returns a sample solana address func SolanaAddress(t *testing.T) string { privKey, err := solana.NewRandomPrivateKey() @@ -99,6 +135,14 @@ func SolanaAddress(t *testing.T) string { return privKey.PublicKey().String() } +func SolanaAddressFromRand(r *rand.Rand) (string, error) { + privKey, err := SolanaPrivateKeyFromRand(r) + if err != nil { + return "", err + } + return privKey.PublicKey().String(), nil +} + // SolanaSignature returns a sample solana signature func SolanaSignature(t *testing.T) solana.Signature { // Generate a random keypair @@ -122,6 +166,11 @@ func Hash() ethcommon.Hash { return ethcommon.BytesToHash(EthAddress().Bytes()) } +// Hash returns a sample hash +func HashFromRand(r *rand.Rand) ethcommon.Hash { + return ethcommon.BytesToHash(EthAddressFromRand(r).Bytes()) +} + // BtcHash returns a sample btc hash func BtcHash() chainhash.Hash { return chainhash.Hash(Hash()) @@ -147,6 +196,13 @@ func AccAddress() string { return sdk.AccAddress(addr).String() } +// AccAddressFromRand returns a sample account address in string +func AccAddressFromRand(r *rand.Rand) string { + pk := PubKey(r) + addr := pk.Address() + return sdk.AccAddress(addr).String() +} + // ValAddress returns a sample validator operator address func ValAddress(r *rand.Rand) sdk.ValAddress { return sdk.ValAddress(PubKey(r).Address()) diff --git a/testutil/sample/observer.go b/testutil/sample/observer.go index 1a0ed8f2ba..5ee8c6f648 100644 --- a/testutil/sample/observer.go +++ b/testutil/sample/observer.go @@ -10,7 +10,6 @@ import ( "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/cosmos" @@ -72,6 +71,15 @@ func Keygen(t *testing.T) *types.Keygen { } } +func KeygenFromRand(r *rand.Rand) types.Keygen { + pubkey := PubKey(r) + return types.Keygen{ + Status: types.KeygenStatus_KeyGenSuccess, + GranteePubkeys: []string{pubkey.String()}, + BlockNumber: r.Int63(), + } +} + func LastObserverCount(lastChangeHeight int64) *types.LastObserverCount { r := newRandFromSeed(lastChangeHeight) @@ -108,6 +116,27 @@ func ChainParams(chainID int64) *types.ChainParams { } } +func ChainParamsFromRand(r *rand.Rand, chainID int64) *types.ChainParams { + fiftyPercent := sdk.MustNewDecFromStr("0.5") + return &types.ChainParams{ + ChainId: chainID, + ConfirmationCount: r.Uint64(), + + GasPriceTicker: Uint64InRangeFromRand(r, 1, 300), + InboundTicker: Uint64InRangeFromRand(r, 1, 300), + OutboundTicker: Uint64InRangeFromRand(r, 1, 300), + WatchUtxoTicker: Uint64InRangeFromRand(r, 1, 300), + ZetaTokenContractAddress: EthAddressFromRand(r).String(), + ConnectorContractAddress: EthAddressFromRand(r).String(), + Erc20CustodyContractAddress: EthAddressFromRand(r).String(), + OutboundScheduleInterval: Int64InRangeFromRand(r, 1, 100), + OutboundScheduleLookahead: Int64InRangeFromRand(r, 1, 500), + BallotThreshold: fiftyPercent, + MinObserverDelegation: sdk.NewDec(r.Int63()), + IsSupported: true, + } +} + func ChainParamsSupported(chainID int64) *types.ChainParams { cp := ChainParams(chainID) cp.IsSupported = true @@ -124,12 +153,16 @@ func ChainParamsList() (cpl types.ChainParamsList) { } // TSSFromRand returns a random TSS,it uses the randomness provided as a parameter -func TSSFromRand(t *testing.T, r *rand.Rand) types.TSS { +func TSSFromRand(r *rand.Rand) (types.TSS, error) { pubKey := PubKey(r) spk, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pubKey) - require.NoError(t, err) + if err != nil { + return types.TSS{}, err + } pk, err := zetacrypto.NewPubKey(spk) - require.NoError(t, err) + if err != nil { + return types.TSS{}, err + } pubkeyString := pk.String() return types.TSS{ TssPubkey: pubkeyString, @@ -137,7 +170,7 @@ func TSSFromRand(t *testing.T, r *rand.Rand) types.TSS { OperatorAddressList: []string{}, FinalizedZetaHeight: r.Int63(), KeyGenZetaHeight: r.Int63(), - } + }, nil } // TODO: rename to TSS @@ -287,6 +320,17 @@ func GasPriceIncreaseFlags() types.GasPriceIncreaseFlags { } } +func GasPriceIncreaseFlagsFromRand(r *rand.Rand) types.GasPriceIncreaseFlags { + minValue := 1 + maxValue := 100 + return types.GasPriceIncreaseFlags{ + EpochLength: int64(r.Intn(maxValue-minValue) + minValue), + RetryInterval: time.Duration(r.Intn(maxValue-minValue) + minValue), + GasPriceIncreasePercent: 1, + MaxPendingCctxs: 100, + } +} + func OperationalFlags() types.OperationalFlags { return types.OperationalFlags{ RestartHeight: 1, diff --git a/testutil/sample/sample.go b/testutil/sample/sample.go index 3f0390ddad..c9f416a088 100644 --- a/testutil/sample/sample.go +++ b/testutil/sample/sample.go @@ -93,12 +93,22 @@ func Uint64InRange(low, high uint64) uint64 { return r.Uint64()%(high-low) + low } +// Uint64InRange returns a sample uint64 in the given ranges +func Uint64InRangeFromRand(r *rand.Rand, low, high uint64) uint64 { + return r.Uint64()%(high-low) + low +} + // Int64InRange returns a sample int64 in the given ranges func Int64InRange(low, high int64) int64 { r := newRandFromSeed(low) return r.Int63()%(high-low) + low } +// Int64InRangeFromRand returns a sample int64 in the given ranges +func Int64InRangeFromRand(r *rand.Rand, low, high int64) int64 { + return r.Int63()%(high-low) + low +} + func UintInRange(low, high uint64) sdkmath.Uint { u := Uint64InRange(low, high) return sdkmath.NewUint(u) diff --git a/x/crosschain/genesis.go b/x/crosschain/genesis.go index 8ea365e059..0f6856d559 100644 --- a/x/crosschain/genesis.go +++ b/x/crosschain/genesis.go @@ -1,7 +1,6 @@ package crosschain import ( - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/node/x/crosschain/keeper" @@ -11,9 +10,8 @@ import ( // InitGenesis initializes the crosschain module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - // Always set the zeta accounting to zero at genesis. - // ZetaAccounting value is build by iterating through all the cctxs and adding the amount to the zeta accounting. - k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: sdkmath.ZeroUint()}) + k.SetZetaAccounting(ctx, genState.ZetaAccounting) + // Set all the outbound tracker for _, elem := range genState.OutboundTrackerList { k.SetOutboundTracker(ctx, elem) @@ -36,8 +34,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the chain nonces - // Set all the last block heights for _, elem := range genState.LastBlockHeightList { if elem != nil { @@ -45,15 +41,15 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the cross-chain txs - tss, found := k.GetObserverKeeper().GetTSS(ctx) - if found { - for _, elem := range genState.CrossChainTxs { - if elem != nil { - k.SaveCCTXUpdate(ctx, *elem, tss.TssPubkey) - } + // Set the cross-chain transactions only, + // We don't need to call SaveCCTXUpdate as the other fields are being set already + for _, elem := range genState.CrossChainTxs { + if elem != nil { + cctx := *elem + k.SetCrossChainTx(ctx, cctx) } } + for _, elem := range genState.FinalizedInbounds { k.SetFinalizedInbound(ctx, elem) } diff --git a/x/crosschain/keeper/cctx_utils.go b/x/crosschain/keeper/cctx_utils.go index 3f62202cb7..3fedf03cef 100644 --- a/x/crosschain/keeper/cctx_utils.go +++ b/x/crosschain/keeper/cctx_utils.go @@ -30,7 +30,6 @@ func (k Keeper) SetObserverOutboundInfo(ctx sdk.Context, receiveChainID int64, c "identifiers: %s (chain %q)", cctx.LogIdentifierForCCTX(), chain.Name, ) } - // SET nonce cctx.GetCurrentOutboundParam().TssNonce = nonce.Nonce tss, found := k.GetObserverKeeper().GetTSS(ctx) diff --git a/x/crosschain/keeper/msg_server_add_outbound_tracker.go b/x/crosschain/keeper/msg_server_add_outbound_tracker.go index 9beffffd1e..360a8f8301 100644 --- a/x/crosschain/keeper/msg_server_add_outbound_tracker.go +++ b/x/crosschain/keeper/msg_server_add_outbound_tracker.go @@ -13,7 +13,6 @@ import ( ) // MaxOutboundTrackerHashes is the maximum number of hashes that can be stored in the outbound transaction tracker -const MaxOutboundTrackerHashes = 5 // AddOutboundTracker adds a new record to the outbound transaction tracker. // only the admin policy account and the observer validators are authorized to broadcast this message without proof. @@ -45,7 +44,6 @@ func (k msgServer) AddOutboundTracker( msg.Nonce, ) } - // tracker submission is only allowed when the cctx is pending if !IsPending(cctx.CrossChainTx) { // garbage tracker (for any reason) is harmful to outTx observation and should be removed if it exists @@ -87,7 +85,7 @@ func (k msgServer) AddOutboundTracker( } // check if max hashes are reached - if len(tracker.HashList) >= MaxOutboundTrackerHashes { + if tracker.MaxReached() { return nil, types.ErrMaxTxOutTrackerHashesReached.Wrapf( "max hashes reached for chain %d, nonce %d, hash number: %d", msg.ChainId, diff --git a/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go b/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go index fb477c9920..3de3bacf0b 100644 --- a/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go +++ b/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go @@ -226,8 +226,8 @@ func TestMsgServer_AddToOutboundTracker(t *testing.T) { observerMock.On("IsNonTombstonedObserver", mock.Anything, mock.Anything).Return(false) keepertest.MockCctxByNonce(t, ctx, *k, observerMock, types.CctxStatus_PendingOutbound, false) - hashes := make([]*types.TxHash, keeper.MaxOutboundTrackerHashes) - for i := 0; i < keeper.MaxOutboundTrackerHashes; i++ { + hashes := make([]*types.TxHash, types.MaxOutboundTrackerHashes) + for i := 0; i < types.MaxOutboundTrackerHashes; i++ { hashes[i] = &types.TxHash{ TxHash: sample.Hash().Hex(), } diff --git a/x/crosschain/keeper/msg_server_refund_aborted_tx.go b/x/crosschain/keeper/msg_server_refund_aborted_tx.go index 22e2b5da81..a4f899b9e2 100644 --- a/x/crosschain/keeper/msg_server_refund_aborted_tx.go +++ b/x/crosschain/keeper/msg_server_refund_aborted_tx.go @@ -75,7 +75,6 @@ func (k msgServer) RefundAbortedCCTX( cctx.CctxStatus.AbortRefunded() k.SetCrossChainTx(ctx, cctx) - return &types.MsgRefundAbortedCCTXResponse{}, nil } diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx.go b/x/crosschain/keeper/msg_server_vote_inbound_tx.go index 263b7b23bc..0c1b875479 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx.go @@ -64,6 +64,7 @@ func (k msgServer) VoteInbound( // vote on inbound ballot // use a temporary context to not commit any ballot state change in case of error tmpCtx, commit := ctx.CacheContext() + finalized, isNew, err := k.zetaObserverKeeper.VoteOnInboundBallot( tmpCtx, msg.SenderChainId, diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go index 7fc5c9be99..57e73a72ae 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go @@ -52,7 +52,7 @@ func TestKeeper_VoteInbound(t *testing.T) { msgServer := keeper.NewMsgServerImpl(*k) validatorList := setObservers(t, k, ctx, zk) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { @@ -185,7 +185,7 @@ func TestKeeper_VoteInbound(t *testing.T) { observerMock.On("VoteOnInboundBallot", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(true, false, errors.New("err")) msgServer := keeper.NewMsgServerImpl(*k) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId msg := sample.InboundVote(0, from, to) res, err := msgServer.VoteInbound( @@ -208,7 +208,7 @@ func TestKeeper_VoteInbound(t *testing.T) { zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ ObserverList: observerSet, }) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { @@ -255,7 +255,7 @@ func TestKeeper_VoteInbound(t *testing.T) { Return(true, false, nil) observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, false) msgServer := keeper.NewMsgServerImpl(*k) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId msg := sample.InboundVote(0, from, to) res, err := msgServer.VoteInbound( diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index fff9feb92a..155bf06770 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -107,6 +107,7 @@ func (k msgServer) WhitelistERC20( msg.ChainId, ) } + if zrc20Addr == (ethcommon.Address{}) { return nil, errorsmod.Wrapf( types.ErrDeployContract, diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go index c82261bd05..1b2c03710c 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go @@ -18,6 +18,11 @@ import ( ) func TestKeeper_WhitelistERC20(t *testing.T) { + r := sample.Rand() + firstTokenAddress, err := sample.SolanaAddressFromRand(r) + require.NoError(t, err) + secondTokenAddress, err := sample.SolanaAddressFromRand(r) + require.NoError(t, err) tests := []struct { name string tokenAddress string @@ -36,6 +41,12 @@ func TestKeeper_WhitelistERC20(t *testing.T) { secondTokenAddress: sample.SolanaAddress(t), chainID: getValidSolanaChainID(), }, + { + name: "can deploy and whitelist a spl", + tokenAddress: firstTokenAddress, + secondTokenAddress: secondTokenAddress, + chainID: getValidSolanaChainID(), + }, } for _, tt := range tests { diff --git a/x/crosschain/keeper/refund.go b/x/crosschain/keeper/refund.go index 6c9deef32f..8d1e2da230 100644 --- a/x/crosschain/keeper/refund.go +++ b/x/crosschain/keeper/refund.go @@ -27,7 +27,7 @@ func (k Keeper) RefundAbortedAmountOnZetaChain( case coin.CoinType_ERC20: return k.RefundAmountOnZetaChainERC20(ctx, cctx, refundAddress) default: - return errors.New("unsupported coin type for refund on ZetaChain") + return fmt.Errorf("unsupported coin type for refund on ZetaChain : %s", coinType) } } diff --git a/x/crosschain/simulation/decoders.go b/x/crosschain/simulation/decoders.go index dd2c6c4e07..9c7f743960 100644 --- a/x/crosschain/simulation/decoders.go +++ b/x/crosschain/simulation/decoders.go @@ -19,44 +19,80 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { var cctxA, cctxB types.CrossChainTx cdc.MustUnmarshal(kvA.Value, &cctxA) cdc.MustUnmarshal(kvB.Value, &cctxB) - return fmt.Sprintf("%v\n%v", cctxA, cctxB) + return fmt.Sprintf("key %s value A %v value B %v", types.CCTXKey, cctxA, cctxB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockHeightKey)): var lastBlockHeightA, lastBlockHeightB types.LastBlockHeight cdc.MustUnmarshal(kvA.Value, &lastBlockHeightA) cdc.MustUnmarshal(kvB.Value, &lastBlockHeightB) - return fmt.Sprintf("%v\n%v", lastBlockHeightA, lastBlockHeightB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockHeightKey, + lastBlockHeightA, + lastBlockHeightB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.FinalizedInboundsKey)): var finalizedInboundsA, finalizedInboundsB []byte finalizedInboundsA = kvA.Value finalizedInboundsB = kvB.Value - return fmt.Sprintf("%v\n%v", finalizedInboundsA, finalizedInboundsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.FinalizedInboundsKey, + finalizedInboundsA, + finalizedInboundsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.GasPriceKey)): var gasPriceA, gasPriceB types.GasPrice cdc.MustUnmarshal(kvA.Value, &gasPriceA) cdc.MustUnmarshal(kvB.Value, &gasPriceB) - return fmt.Sprintf("%v\n%v", gasPriceA, gasPriceB) + return fmt.Sprintf("key %s value A %v value B %v", types.GasPriceKey, gasPriceA, gasPriceB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.OutboundTrackerKeyPrefix)): var outboundTrackerA, outboundTrackerB types.OutboundTracker cdc.MustUnmarshal(kvA.Value, &outboundTrackerA) cdc.MustUnmarshal(kvB.Value, &outboundTrackerB) - return fmt.Sprintf("%v\n%v", outboundTrackerA, outboundTrackerB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.OutboundTrackerKeyPrefix, + outboundTrackerA, + outboundTrackerB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.InboundTrackerKeyPrefix)): var inboundTrackerA, inboundTrackerB types.InboundTracker cdc.MustUnmarshal(kvA.Value, &inboundTrackerA) cdc.MustUnmarshal(kvB.Value, &inboundTrackerB) - return fmt.Sprintf("%v\n%v", inboundTrackerA, inboundTrackerB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.InboundTrackerKeyPrefix, + inboundTrackerA, + inboundTrackerB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ZetaAccountingKey)): var zetaAccountingA, zetaAccountingB types.ZetaAccounting cdc.MustUnmarshal(kvA.Value, &zetaAccountingA) cdc.MustUnmarshal(kvB.Value, &zetaAccountingB) - return fmt.Sprintf("%v\n%v", zetaAccountingA, zetaAccountingB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.ZetaAccountingKey, + zetaAccountingA, + zetaAccountingB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.RateLimiterFlagsKey)): var rateLimiterFlagsA, rateLimiterFlagsB types.RateLimiterFlags cdc.MustUnmarshal(kvA.Value, &rateLimiterFlagsA) cdc.MustUnmarshal(kvB.Value, &rateLimiterFlagsB) - return fmt.Sprintf("%v\n%v", rateLimiterFlagsA, rateLimiterFlagsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.RateLimiterFlagsKey, + rateLimiterFlagsA, + rateLimiterFlagsB, + ) default: - panic(fmt.Sprintf("invalid crosschain key prefix %X", kvA.Key[:1])) + panic( + fmt.Sprintf( + "invalid crosschain key prefix %X (first 8 bytes: %X)", + kvA.Key[:1], + kvA.Key[:min(8, len(kvA.Key))], + ), + ) } } } diff --git a/x/crosschain/simulation/decoders_test.go b/x/crosschain/simulation/decoders_test.go index 9765aca43d..8c22cf9707 100644 --- a/x/crosschain/simulation/decoders_test.go +++ b/x/crosschain/simulation/decoders_test.go @@ -41,14 +41,37 @@ func TestDecodeStore(t *testing.T) { name string expectedLog string }{ - {"CrossChainTx", fmt.Sprintf("%v\n%v", *cctx, *cctx)}, - {"LastBlockHeight", fmt.Sprintf("%v\n%v", *lastBlockHeight, *lastBlockHeight)}, - {"GasPrice", fmt.Sprintf("%v\n%v", *gasPrice, *gasPrice)}, - {"OutboundTracker", fmt.Sprintf("%v\n%v", outboundTracker, outboundTracker)}, - {"InboundTracker", fmt.Sprintf("%v\n%v", inboundTracker, inboundTracker)}, - {"ZetaAccounting", fmt.Sprintf("%v\n%v", zetaAccounting, zetaAccounting)}, - {"RateLimiterFlags", fmt.Sprintf("%v\n%v", rateLimiterFlags, rateLimiterFlags)}, - {"FinalizedInbounds", fmt.Sprintf("%v\n%v", []byte{1}, []byte{1})}, + {"CrossChainTx", fmt.Sprintf("key %s value A %v value B %v", types.CCTXKey, *cctx, *cctx)}, + { + "LastBlockHeight", + fmt.Sprintf("key %s value A %v value B %v", types.LastBlockHeightKey, *lastBlockHeight, *lastBlockHeight), + }, + {"GasPrice", fmt.Sprintf("key %s value A %v value B %v", types.GasPriceKey, *gasPrice, *gasPrice)}, + { + "OutboundTracker", + fmt.Sprintf( + "key %s value A %v value B %v", + types.OutboundTrackerKeyPrefix, + outboundTracker, + outboundTracker, + ), + }, + { + "InboundTracker", + fmt.Sprintf("key %s value A %v value B %v", types.InboundTrackerKeyPrefix, inboundTracker, inboundTracker), + }, + { + "ZetaAccounting", + fmt.Sprintf("key %s value A %v value B %v", types.ZetaAccountingKey, zetaAccounting, zetaAccounting), + }, + { + "RateLimiterFlags", + fmt.Sprintf("key %s value A %v value B %v", types.RateLimiterFlagsKey, rateLimiterFlags, rateLimiterFlags), + }, + { + "FinalizedInbounds", + fmt.Sprintf("key %s value A %v value B %v", types.FinalizedInboundsKey, []byte{1}, []byte{1}), + }, } for i, tt := range tests { diff --git a/x/crosschain/simulation/operation_abort_stuck_cctx.go b/x/crosschain/simulation/operation_abort_stuck_cctx.go new file mode 100644 index 0000000000..55d0a1cd60 --- /dev/null +++ b/x/crosschain/simulation/operation_abort_stuck_cctx.go @@ -0,0 +1,128 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAbortStuckCCTX generates a MsgAbortStuckCCTX with random values +func SimulateMsgAbortStuckCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Pick a ethereum chain to abort a stuck cctx + chainID := chains.GoerliLocalnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no supported chains found", + ), nil, nil + } + + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + chainID = chain.ChainId + } + } + + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAbortStuckCCTX, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no TSS found", + ), nil, nil + } + + pendingNonces, found := k.GetObserverKeeper().GetPendingNonces(ctx, tss.TssPubkey, chainID) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no pending nonces found", + ), nil, nil + } + + // If nonce low is the same as nonce high, it means that there are no pending nonces to add trackers for + if pendingNonces.NonceLow == pendingNonces.NonceHigh { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no pending nonces found", + ), nil, nil + } + // Pick a random pending nonce + nonce := 0 + switch { + case pendingNonces.NonceHigh <= 1: + nonce = int(pendingNonces.NonceLow) + case pendingNonces.NonceLow == 0: + nonce = r.Intn(int(pendingNonces.NonceHigh)) + default: + nonce = r.Intn(int(pendingNonces.NonceHigh)-int(pendingNonces.NonceLow)) + int(pendingNonces.NonceLow) + } + + nonceToCctx, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tss.TssPubkey, chainID, int64(nonce)) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no cctx found", + ), nil, nil + } + + cctx, found := k.GetCrossChainTx(ctx, nonceToCctx.CctxIndex) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no cctx found", + ), nil, nil + } + + msg := types.MsgAbortStuckCCTX{ + Creator: policyAccount.Address.String(), + CctxIndex: cctx.Index, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate MsgAbortStuckCCTX msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_add_inbound_tracker.go b/x/crosschain/simulation/operation_add_inbound_tracker.go new file mode 100644 index 0000000000..909c5cd9dc --- /dev/null +++ b/x/crosschain/simulation/operation_add_inbound_tracker.go @@ -0,0 +1,72 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAddInboundTracker generates a MsgAddInboundTracker with random values +func SimulateMsgAddInboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddInboundTracker, + "no supported chains found", + ), nil, nil + } + randomChainID := GetRandomChainID(r, supportedChains) + txHash := sample.HashFromRand(r) + coinType := sample.CoinTypeFromRand(r) + + // Add a new inbound Tracker + msg := types.MsgAddInboundTracker{ + Creator: randomObserver, + ChainId: randomChainID, + TxHash: txHash.String(), + CoinType: coinType, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate MsgAddInboundTracker"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_add_outbound_tracker.go b/x/crosschain/simulation/operation_add_outbound_tracker.go new file mode 100644 index 0000000000..1640bfcf07 --- /dev/null +++ b/x/crosschain/simulation/operation_add_outbound_tracker.go @@ -0,0 +1,152 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAddOutboundTracker generates a MsgAddOutboundTracker with random values +func SimulateMsgAddOutboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + chainID := chains.GoerliLocalnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no supported chains found", + ), nil, nil + } + + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + chainID = chain.ChainId + } + } + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + txHash := sample.HashFromRand(r) + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no TSS found", + ), nil, nil + } + + pendingNonces, found := k.GetObserverKeeper().GetPendingNonces(ctx, tss.TssPubkey, chainID) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "pending nonces object not found", + ), nil, nil + } + + // pick a random nonce from the pending nonces between 0 and nonceLow + // If nonce low is the same as nonce high, it means that there are no pending nonces to add trackers for + if pendingNonces.NonceLow == pendingNonces.NonceHigh { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no pending nonces found", + ), nil, nil + } + // Pick a random pending nonce + nonce := 0 + switch { + case pendingNonces.NonceHigh <= 1: + nonce = int(pendingNonces.NonceLow) + case pendingNonces.NonceLow == 0: + nonce = r.Intn(int(pendingNonces.NonceHigh)) + default: + nonce = r.Intn(int(pendingNonces.NonceHigh)-int(pendingNonces.NonceLow)) + int(pendingNonces.NonceLow) + } + + // Verify if the tracker is maxed + tracker, found := k.GetOutboundTracker( + ctx, + chainID, + uint64(nonce), + ) // #nosec G115 - overflow is not an issue here + if found && tracker.MaxReached() { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "tracker is maxed", + ), nil, nil + } + + // Verify the nonceToCCTX exists + nonceToCCTX, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tss.TssPubkey, chainID, int64(nonce)) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no nonce to cctx found", + ), nil, nil + } + + // Verify the cctx exists + _, found = k.GetCrossChainTx(ctx, nonceToCCTX.CctxIndex) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no cctx found for nonce", + ), nil, nil + } + // Add a new inbound Tracker + msg := types.MsgAddOutboundTracker{ + Creator: randomObserver, + ChainId: chainID, + Nonce: uint64(nonce), // #nosec G115 - overflow is not an issue here + TxHash: txHash.String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgAddOutboundTracker msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_gas_price_voter.go b/x/crosschain/simulation/operation_gas_price_voter.go new file mode 100644 index 0000000000..65970dcc91 --- /dev/null +++ b/x/crosschain/simulation/operation_gas_price_voter.go @@ -0,0 +1,78 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgVoteGasPrice generates a MsgVoteGasPrice and delivers it +func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + authz.GasPriceVoter.String(), + "no supported chains found", + ), nil, nil + } + randomChainID := GetRandomChainID(r, supportedChains) + // Vote for random gas price. Gas prices do not use a ballot system, so we can vote directly without having to schedule future operations. + gasPrice := sample.GasPriceFromRand(r, randomChainID) + msg := types.MsgVoteGasPrice{ + Creator: randomObserver, + ChainId: randomChainID, + Price: gasPrice.Prices[0], + PriorityFee: gasPrice.PriorityFees[0], + BlockNumber: uint64(ctx.BlockHeight()) + r.Uint64()%1000, // #nosec G115 - overflow is not a issue here + Supply: sdk.NewInt(r.Int63n(1e18)).String(), + } + + // System contracts are deployed on the first block, so we cannot vote on gas prices before that + if ctx.BlockHeight() <= 1 { + return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_refund_aborted_cctx.go b/x/crosschain/simulation/operation_refund_aborted_cctx.go new file mode 100644 index 0000000000..9f46da5933 --- /dev/null +++ b/x/crosschain/simulation/operation_refund_aborted_cctx.go @@ -0,0 +1,84 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgRefundAbortedCCTX generates a MsgRefundAbortedCCTX with random values +func SimulateMsgRefundAbortedCCTX(k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins} + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.RefundAborted, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + cctxList := k.GetAllCrossChainTx(ctx) + abortedCctx := types.CrossChainTx{} + abortedCctxFound := false + + for _, cctx := range cctxList { + if cctx.CctxStatus.Status == types.CctxStatus_Aborted { + if !cctx.InboundParams.CoinType.SupportsRefund() { + continue + } + if cctx.CctxStatus.IsAbortRefunded { + continue + } + abortedCctx = cctx + abortedCctxFound = true + break + } + } + if !abortedCctxFound { + return simtypes.NoOpMsg(types.ModuleName, types.RefundAborted, "no aborted cctx found"), nil, nil + } + + msg := types.MsgRefundAbortedCCTX{ + Creator: policyAccount.Address.String(), + CctxIndex: abortedCctx.Index, + RefundAddress: sample.EthAddressFromRand(r).String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgRefundAbortedCCTX msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_remove_outbound_tracker.go b/x/crosschain/simulation/operation_remove_outbound_tracker.go new file mode 100644 index 0000000000..6eaa52be81 --- /dev/null +++ b/x/crosschain/simulation/operation_remove_outbound_tracker.go @@ -0,0 +1,72 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgRemoveOutboundTracker generates a MsgRemoveOutboundTracker with random values +func SimulateMsgRemoveOutboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgRemoveOutboundTracker, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + trackers := k.GetAllOutboundTracker(ctx) + + if len(trackers) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveOutboundTracker, + "no outbound trackers found", + ), nil, nil + } + + randomTracker := trackers[r.Intn(len(trackers))] + + msg := types.MsgRemoveOutboundTracker{ + ChainId: randomTracker.ChainId, + Nonce: randomTracker.Nonce, + Creator: policyAccount.Address.String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgRemoveOutboundTracker", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_erc20_pause_status.go b/x/crosschain/simulation/operation_update_erc20_pause_status.go new file mode 100644 index 0000000000..a4fa5b30c0 --- /dev/null +++ b/x/crosschain/simulation/operation_update_erc20_pause_status.go @@ -0,0 +1,120 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateUpdateERC20CustodyPauseStatus generates a MsgUpdateERC20CustodyPauseStatus with random values and delivers it +func SimulateUpdateERC20CustodyPauseStatus(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeUpdateERC20CustodyPauseStatus, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no supported chains found", + ), nil, nil + } + + filteredChains := chains.FilterChains(supportedChains, chains.FilterExternalChains) + + //pick a random chain + randomChain := filteredChains[r.Intn(len(filteredChains))] + + _, found := k.GetObserverKeeper().GetChainNonces(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no chain nonces found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no TSS found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetChainParamsByChainID(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no chain params found", + ), nil, nil + } + medianGasPrice, priorityFee, found := k.GetMedianGasValues(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no median gas values found", + ), nil, nil + } + medianGasPrice = medianGasPrice.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + priorityFee = priorityFee.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + + if priorityFee.GT(medianGasPrice) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "priorityFee is greater than median gasPrice", + ), nil, nil + } + + msg := types.MsgUpdateERC20CustodyPauseStatus{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + Pause: r.Intn(2) == 0, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateERC20CustodyPauseStatus msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_rate_limiter_flags.go b/x/crosschain/simulation/operation_update_rate_limiter_flags.go new file mode 100644 index 0000000000..f835e57e24 --- /dev/null +++ b/x/crosschain/simulation/operation_update_rate_limiter_flags.go @@ -0,0 +1,60 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgUpdateRateLimiterFlags generates a MsgUpdateRateLimiterFlags with random values +func SimulateMsgUpdateRateLimiterFlags(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins} + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateRateLimiterFlags, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgUpdateRateLimiterFlags{ + Creator: policyAccount.Address.String(), + RateLimiterFlags: sample.RateLimiterFlagsFromRand(r), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateRateLimiterFlags msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_tss_address.go b/x/crosschain/simulation/operation_update_tss_address.go new file mode 100644 index 0000000000..aac70b512b --- /dev/null +++ b/x/crosschain/simulation/operation_update_tss_address.go @@ -0,0 +1,129 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" + observertypes "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateMsgUpdateTssAddress generates a MsgUpdateTssAddress with random values and delivers it +func SimulateMsgUpdateTssAddress(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateTssAddress, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetChainsSupportingTSSMigration(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no chains found which support tss migration", + ), nil, nil + } + + cctxList := k.GetAllCrossChainTx(ctx) + if len(cctxList) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no cross chain txs found", + ), nil, nil + } + + // Pick any cctx with status OutboundMined, and use its index for the migration + // We set the fund migrator directly as we are not simulating MsgMigrateTssFunds + minedCCTX := types.CrossChainTx{} + foundMined := false + for _, cctx := range cctxList { + if cctx.CctxStatus.Status == types.CctxStatus_OutboundMined { + minedCCTX = cctx + foundMined = true + break + } + } + if !foundMined { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no mined cross chain txs found in mined state", + ), nil, nil + } + + // Thee tss migrator is set for all chains supporting tss migration + for _, chain := range supportedChains { + tssMigrator := observertypes.TssFundMigratorInfo{ + ChainId: chain.ChainId, + MigrationCctxIndex: minedCCTX.Index, + } + k.GetObserverKeeper().SetFundMigrator(ctx, tssMigrator) + } + + oldTss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no TSS found", + ), nil, nil + } + + // Set the new TSS to state + newTss, err := sample.TSSFromRand(r) + newTss.FinalizedZetaHeight = oldTss.FinalizedZetaHeight + 10 + newTss.KeyGenZetaHeight = oldTss.KeyGenZetaHeight + 10 + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + err.Error()), + nil, nil + } + k.GetObserverKeeper().SetTSSHistory(ctx, newTss) + + msg := types.MsgUpdateTssAddress{ + Creator: policyAccount.Address.String(), + TssPubkey: newTss.TssPubkey, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateTssAddress msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_vote_inbound.go b/x/crosschain/simulation/operation_vote_inbound.go new file mode 100644 index 0000000000..714d649aaa --- /dev/null +++ b/x/crosschain/simulation/operation_vote_inbound.go @@ -0,0 +1,190 @@ +package simulation + +import ( + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// operationSimulateVoteInbound generates a MsgVoteInbound with a random vote and delivers it. +func operationSimulateVoteInbound( + k keeper.Keeper, + msg types.MsgVoteInbound, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateVoteInbound generates a MsgVoteInbound with random values and delivers it. It also schedules future operations for subsequent votes. +func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { + observerVotesTransitionMatrix, statePercentageArray, curNumVotesState := ObserverVotesSimulationMatrix() + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + from = chain.ChainId + } + if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { + to = chain.ChainId + } + } + + asset, err := GetAsset(ctx, k.GetFungibleKeeper(), from) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "unable to get asset"), nil, err + } + + // Generate a random inbound vote , coin type is randomly selected + msg := sample.InboundVoteFromRand(from, to, r, asset) + + cf, found := k.GetObserverKeeper().GetCrosschainFlags(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "crosschain flags not found"), nil, nil + } + + // Return early if inbound is not enabled. + if !cf.IsInboundEnabled { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "inbound is not enabled"), nil, nil + } + + // Return early if the inbound has already been finalized. + if k.IsFinalizedInbound(ctx, msg.InboundHash, msg.SenderChainId, msg.EventIndex) { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "inbound already finalized"), nil, nil + } + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetObserverKeeper().GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first inbound vote"), nil, err + } + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + // Add subsequent votes + observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "observer set not found"), nil, nil + } + + // 1) Schedule operations for votes + // 1.1) first pick a number of people to vote. + curNumVotesState = observerVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) + + // 1.2) select who votes + whoVotes := r.Perm(len(observerSet.ObserverList)) + whoVotes = whoVotes[:numVotes] + + var fops []simtypes.FutureOperation + + for _, observerIdx := range whoVotes { + observerAddress := observerSet.ObserverList[observerIdx] + // firstVoter has already voted. + if observerAddress == firstVoter { + continue + } + observerAccount, err := GetObserverAccount(observerAddress, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = observerAddress + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteInbound(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/crosschain/simulation/operation_vote_outbound.go b/x/crosschain/simulation/operation_vote_outbound.go new file mode 100644 index 0000000000..bf4dbaf5dd --- /dev/null +++ b/x/crosschain/simulation/operation_vote_outbound.go @@ -0,0 +1,226 @@ +package simulation + +import ( + "fmt" + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + ethcommon "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +func operationSimulateVoteOutbound( + k keeper.Keeper, + msg types.MsgVoteOutbound, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateVoteOutbound generates a MsgVoteOutbound with random values and delivers it. +//It also schedules future operations for subsequent votes. + +func SimulateVoteOutbound(k keeper.Keeper) simtypes.Operation { + defaultVote := chains.ReceiveStatus_success + alternativeVote := chains.ReceiveStatus_failed + observerVotesTransitionMatrix, statePercentageArray, curNumVotesState := ObserverVotesSimulationMatrix() + ballotVotesTransitionMatrix, yesVotePercentageArray, ballotVotesState := BallotVoteSimulationMatrix() + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + to = chain.ChainId + } + if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { + from = chain.ChainId + } + } + + _, creator, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + index := ethcrypto.Keccak256Hash([]byte(fmt.Sprintf("%d", r.Int63()))).Hex() + + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "tss not found"), nil, nil + } + + asset, err := GetAsset(ctx, k.GetFungibleKeeper(), from) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "unable to get asset"), nil, err + } + + // Generate a new cctx and save it , which can be used to finalize the outbound + cctx := sample.CCTXfromRand(r, creator, index, to, from, tss.TssPubkey, asset) + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: defaultVote, + Creator: cctx.Creator, + ObservedOutboundHash: ethcommon.BytesToHash(sample.EthAddressFromRand(r).Bytes()).String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: cctx.GetCurrentOutboundParam().ObservedExternalHeight, + ObservedOutboundEffectiveGasPrice: cctx.GetCurrentOutboundParam().EffectiveGasPrice, + ObservedOutboundGasUsed: cctx.GetCurrentOutboundParam().GasUsed, + CoinType: cctx.InboundParams.CoinType, + } + + err = k.SetObserverOutboundInfo(ctx, to, &cctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to set observer outbound info"), nil, err + } + + msg.OutboundTssNonce = cctx.GetCurrentOutboundParam().TssNonce + k.SaveCCTXUpdate(ctx, cctx, tss.TssPubkey) + + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetObserverKeeper().GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first outbound vote"), nil, err + } + + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + // Add subsequent votes + observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "observer set not found"), nil, nil + } + + // 1) Schedule operations for votes + // 1.1) first pick a number of people to vote. + curNumVotesState = observerVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) + + // 1.2) select who votes + whoVotes := r.Perm(len(observerSet.ObserverList)) + whoVotes = whoVotes[:numVotes] + + var fops []simtypes.FutureOperation + + ballotVotesState = ballotVotesTransitionMatrix.NextState(r, ballotVotesState) + yesVotePercentage := yesVotePercentageArray[ballotVotesState] + numberOfYesVotes := int(math.Ceil(float64(numVotes) * yesVotePercentage)) + vote := defaultVote + + for voteCount, observerIdx := range whoVotes { + if voteCount == numberOfYesVotes { + vote = alternativeVote + } + observerAddress := observerSet.ObserverList[observerIdx] + // firstVoter has already voted. + if observerAddress == firstVoter { + continue + } + observerAccount, err := GetObserverAccount(observerAddress, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = observerAddress + votingMsg.Status = vote + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteOutbound(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/crosschain/simulation/operation_whitelist_erc20.go b/x/crosschain/simulation/operation_whitelist_erc20.go new file mode 100644 index 0000000000..b67cad6a44 --- /dev/null +++ b/x/crosschain/simulation/operation_whitelist_erc20.go @@ -0,0 +1,147 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgWhitelistERC20 generates a MsgWhitelistERC20 with random values and delivers it +func SimulateMsgWhitelistERC20(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgWhitelistERC20, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no supported chains found", + ), nil, nil + } + + filteredChains := chains.FilterChains(supportedChains, chains.FilterByVM(chains.Vm_evm)) + if len(filteredChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no EVM-compatible chains found", + ), nil, nil + } + + //pick a random chain + // Keep the switch case to add solana support in future + // TODO : https://github.com/zeta-chain/node/issues/3287 + randomChain := filteredChains[r.Intn(len(filteredChains))] + var tokenAddress string + switch { + case randomChain.IsEVMChain(): + tokenAddress = sample.EthAddressFromRand(r).String() + default: + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgWhitelistERC20, "unsupported chain"), nil, nil + } + + _, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no TSS found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetChainParamsByChainID(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no chain params found", + ), nil, nil + } + + medianGasPrice, priorityFee, isFound := k.GetMedianGasValues(ctx, randomChain.ChainId) + if !isFound { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "median gas price not found", + ), nil, nil + } + + medianGasPrice = medianGasPrice.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + priorityFee = priorityFee.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + + if priorityFee.GT(medianGasPrice) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "priorityFee is greater than median gasPrice", + ), nil, nil + } + + foreignCoins := k.GetFungibleKeeper().GetAllForeignCoins(ctx) + for _, fCoin := range foreignCoins { + if fCoin.Asset == tokenAddress && fCoin.ForeignChainId == randomChain.ChainId { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "ERC20 already whitelisted", + ), nil, nil + } + } + + gasLimit := r.Int63n(1000000000) + 1 + nameLength := r.Intn(97) + 3 + msg := types.MsgWhitelistERC20{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + Erc20Address: tokenAddress, + GasLimit: gasLimit, + Decimals: 18, + Name: sample.StringRandom(r, nameLength), + Symbol: sample.StringRandom(r, 3), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgWhitelistERC20", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operations.go b/x/crosschain/simulation/operations.go index 96b04970b9..5d2403a64f 100644 --- a/x/crosschain/simulation/operations.go +++ b/x/crosschain/simulation/operations.go @@ -2,26 +2,24 @@ package simulation import ( "fmt" - "math" "math/rand" - "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "github.com/zeta-chain/node/pkg/authz" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/x/crosschain/keeper" "github.com/zeta-chain/node/x/crosschain/types" observerTypes "github.com/zeta-chain/node/x/observer/types" ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -30,55 +28,57 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( - DefaultWeightMsgAddOutboundTracker = 50 - DefaultWeightAddInboundTracker = 50 - DefaultWeightRemoveOutboundTracker = 5 - DefaultWeightVoteGasPrice = 100 - DefaultWeightVoteOutbound = 50 - DefaultWeightVoteInbound = 100 - DefaultWeightWhitelistERC20 = 1 - DefaultWeightMigrateTssFunds = 1 - DefaultWeightUpdateTssAddress = 1 - DefaultWeightAbortStuckCCTX = 10 - DefaultWeightUpdateRateLimiterFlags = 1 - - OpWeightMsgAddOutboundTracker = "op_weight_msg_add_outbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightAddInboundTracker = "op_weight_msg_add_inbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightRemoveOutboundTracker = "op_weight_msg_remove_outbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightVoteGasPrice = "op_weight_msg_vote_gas_price" // #nosec G101 not a hardcoded credential - OpWeightVoteOutbound = "op_weight_msg_vote_outbound" // #nosec G101 not a hardcoded credential - OpWeightVoteInbound = "op_weight_msg_vote_inbound" // #nosec G101 not a hardcoded credential - OpWeightWhitelistERC20 = "op_weight_msg_whitelist_erc20" // #nosec G101 not a hardcoded credential - OpWeightMigrateTssFunds = "op_weight_msg_migrate_tss_funds" // #nosec G101 not a hardcoded credential - OpWeightUpdateTssAddress = "op_weight_msg_update_tss_address" // #nosec G101 not a hardcoded credential - OpWeightAbortStuckCCTX = "op_weight_msg_abort_stuck_cctx" // #nosec G101 not a hardcoded credential - OpWeightUpdateRateLimiterFlags = "op_weight_msg_update_rate_limiter_flags" // #nosec G101 not a hardcoded credential - + DefaultWeightAddOutboundTracker = 10 + DefaultWeightAddInboundTracker = 10 + DefaultWeightRemoveOutboundTracker = 10 + DefaultWeightVoteGasPrice = 50 + DefaultWeightVoteOutbound = 10 + DefaultWeightVoteInbound = 10 + DefaultWeightWhitelistERC20 = 10 + DefaultWeightMigrateTssFunds = 1 + DefaultWeightUpdateTssAddress = 10 + DefaultWeightAbortStuckCCTX = 5 + DefaultWeightUpdateRateLimiterFlags = 10 + DefaultWeightRefundAbortedCCTX = 10 + DefaultWeightUpdateERC20CustodyPauseStatus = 10 + + OpWeightMsgAddOutboundTracker = "op_weight_msg_add_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightAddInboundTracker = "op_weight_msg_add_inbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightRemoveOutboundTracker = "op_weight_msg_remove_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightVoteGasPrice = "op_weight_msg_vote_gas_price" // #nosec G101 not a hardcoded credential + OpWeightVoteOutbound = "op_weight_msg_vote_outbound" // #nosec G101 not a hardcoded credential + OpWeightVoteInbound = "op_weight_msg_vote_inbound" // #nosec G101 not a hardcoded credential + OpWeightWhitelistERC20 = "op_weight_msg_whitelist_erc20" // #nosec G101 not a hardcoded credential + OpWeightMigrateTssFunds = "op_weight_msg_migrate_tss_funds" // #nosec G101 not a hardcoded credential + OpWeightUpdateTssAddress = "op_weight_msg_update_tss_address" // #nosec G101 not a hardcoded credential + OpWeightAbortStuckCCTX = "op_weight_msg_abort_stuck_cctx" // #nosec G101 not a hardcoded credential + OpWeightUpdateRateLimiterFlags = "op_weight_msg_update_rate_limiter_flags" // #nosec G101 not a hardcoded credential + OpWeightRefundAbortedCCTX = "op_weight_msg_refund_aborted_cctx" // #nosec G101 not a hardcoded credential + OpWeightUpdateERC20CustodyPauseStatus = "op_weight_msg_update_erc20_custody_pause_status" // #nosec G101 not a hardcoded credential ) func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper) simulation.WeightedOperations { var ( - weightMsgAddOutboundTracker int - weightAddInboundTracker int - weightRemoveOutboundTracker int - weightVoteGasPrice int - weightVoteOutbound int - weightVoteInbound int - weightWhitelistERC20 int - weightMigrateTssFunds int - weightUpdateTssAddress int - weightAbortStuckCCTX int - weightUpdateRateLimiterFlags int + weightAddOutboundTracker int + weightAddInboundTracker int + weightRemoveOutboundTracker int + weightVoteGasPrice int + weightVoteOutbound int + weightVoteInbound int + weightWhitelistERC20 int + weightMigrateTssFunds int + weightUpdateTssAddress int + weightAbortStuckCCTX int + weightUpdateRateLimiterFlags int + weightRefundAbortedCCTX int + weightUpdateERC20CustodyPauseStatus int ) - appParams.GetOrGenerate(cdc, OpWeightMsgAddOutboundTracker, &weightMsgAddOutboundTracker, nil, + appParams.GetOrGenerate(cdc, OpWeightMsgAddOutboundTracker, &weightAddOutboundTracker, nil, func(_ *rand.Rand) { - weightMsgAddOutboundTracker = DefaultWeightMsgAddOutboundTracker + weightAddOutboundTracker = DefaultWeightAddOutboundTracker }, ) @@ -142,6 +142,18 @@ func WeightedOperations( }, ) + appParams.GetOrGenerate(cdc, OpWeightRefundAbortedCCTX, &weightRefundAbortedCCTX, nil, + func(_ *rand.Rand) { + weightRefundAbortedCCTX = DefaultWeightRefundAbortedCCTX + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightUpdateERC20CustodyPauseStatus, &weightUpdateERC20CustodyPauseStatus, nil, + func(_ *rand.Rand) { + weightUpdateERC20CustodyPauseStatus = DefaultWeightUpdateERC20CustodyPauseStatus + }, + ) + return simulation.WeightedOperations{ simulation.NewWeightedOperation( weightVoteGasPrice, @@ -151,239 +163,46 @@ func WeightedOperations( weightVoteInbound, SimulateVoteInbound(k), ), - } -} - -// operationSimulateVoteInbound generates a MsgVoteInbound with a random vote and delivers it. -func operationSimulateVoteInbound( - k keeper.Keeper, - msg types.MsgVoteInbound, - simAccount simtypes.Account, -) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - // Fetch the account from the auth keeper which can then be used to fetch spendable coins - authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) - - // Generate a transaction with a random fee and deliver it - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, - CoinsSpentInMsg: spendable, - } - - // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk - // The main difference between the two functions is that the one defined by us does not error out if the vote fails. - // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. - // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. - return GenAndDeliverTxWithRandFees(txCtx) - } -} - -func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { - // The states are: - // column 1: All observers vote - // column 2: 90% vote - // column 3: 75% vote - // column 4: 40% vote - // column 5: 15% vote - // column 6: noone votes - // All columns sum to 100 for simplicity, but this is arbitrary and can be changed - numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ - {20, 10, 0, 0, 0, 0}, - {55, 50, 20, 10, 0, 0}, - {25, 25, 30, 25, 30, 15}, - {0, 15, 30, 25, 30, 30}, - {0, 0, 20, 30, 30, 30}, - {0, 0, 0, 10, 10, 25}, - }) - - statePercentageArray := []float64{1, .9, .75, .4, .15, 0} - curNumVotesState := 1 - - return func( - r *rand.Rand, - app *baseapp.BaseApp, - ctx sdk.Context, - accs []simtypes.Account, - chainID string, - ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO : randomize these values - // Right now we use a constant value for cctx creation , this is the same as the one used in unit tests for the successful condition. - // TestKeeper_VoteInbound/successfully vote on evm deposit - // But this can improved by adding more randomization - - to, from := int64(1337), int64(101) - supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) - for _, chain := range supportedChains { - if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { - from = chain.ChainId - } - if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { - to = chain.ChainId - } - } - - msg := sample.InboundVoteFromRand(0, from, to, r) - - // Pick a random observer to create the ballot - // If this returns an error, it is likely that the entire observer set has been removed - simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) - if err != nil { - return simtypes.OperationMsg{}, nil, nil - } - - txGen := moduletestutil.MakeTestEncodingConfig().TxConfig - account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - firstMsg := msg - firstMsg.Creator = firstVoter - - err = firstMsg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first inbound vote"), nil, err - } - - tx, err := simtestutil.GenSignedMockTx( - r, - txGen, - []sdk.Msg{&firstMsg}, - sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, - simtestutil.DefaultGenTxGas, - chainID, - []uint64{account.GetAccountNumber()}, - []uint64{account.GetSequence()}, - simAccount.PrivKey, - ) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err - } - - // We can return error here as we can guarantee that the first vote will be successful. - // Since we query the observer set before adding votes - _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err - } - - opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) - - // Add subsequent votes - observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) - if !found { - return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "observer set not found"), nil, nil - } - - // 1) Schedule operations for votes - // 1.1) first pick a number of people to vote. - curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState) - numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) - - // 1.2) select who votes - whoVotes := r.Perm(len(observerSet.ObserverList)) - whoVotes = whoVotes[:numVotes] - - var fops []simtypes.FutureOperation - - for _, observerIdx := range whoVotes { - observerAddress := observerSet.ObserverList[observerIdx] - // firstVoter has already voted. - if observerAddress == firstVoter { - continue - } - observerAccount, err := GetObserverAccount(observerAddress, accs) - if err != nil { - continue - } - // 1.3) schedule the vote - votingMsg := msg - votingMsg.Creator = observerAddress - - e := votingMsg.ValidateBasic() - if e != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e - } - - fops = append(fops, simtypes.FutureOperation{ - // Submit all subsequent votes in the next block. - // We can consider adding a random block height between 1 and ballot maturity blocks in the future. - BlockHeight: int(ctx.BlockHeight() + 1), - Op: operationSimulateVoteInbound(k, votingMsg, observerAccount), - }) - } - return opMsg, fops, nil - } -} - -// SimulateMsgVoteGasPrice generates a MsgVoteGasPrice and delivers it -func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - // Get a random account and observer - // If this returns an error, it is likely that the entire observer set has been removed - simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) - if err != nil { - return simtypes.OperationMsg{}, nil, nil - } - authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) - - supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) - if len(supportedChains) == 0 { - return simtypes.NoOpMsg( - types.ModuleName, - authz.GasPriceVoter.String(), - "no supported chains found", - ), nil, nil - } - randomChainID := GetRandomChainID(r, supportedChains) - - // Vote for random gas price. Gas prices do not use a ballot system, so we can vote directly without having to schedule future operations. - // The random nature of the price might create weird gas prices for the chain, but it is fine for now. We can remove the randomness if needed - msg := types.MsgVoteGasPrice{ - Creator: randomObserver, - ChainId: randomChainID, - Price: r.Uint64(), - PriorityFee: r.Uint64(), - BlockNumber: r.Uint64(), - Supply: fmt.Sprintf("%d", r.Int63()), - } - - // System contracts are deployed on the first block, so we cannot vote on gas prices before that - if ctx.BlockHeight() <= 1 { - return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil - } - - err = msg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err - } - - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, - CoinsSpentInMsg: spendable, - } - - return simulation.GenAndDeliverTxWithRandFees(txCtx) + simulation.NewWeightedOperation( + weightVoteOutbound, + SimulateVoteOutbound(k), + ), + simulation.NewWeightedOperation( + weightAddInboundTracker, + SimulateMsgAddInboundTracker(k), + ), + simulation.NewWeightedOperation( + weightAddOutboundTracker, + SimulateMsgAddOutboundTracker(k), + ), + simulation.NewWeightedOperation( + weightRemoveOutboundTracker, + SimulateMsgRemoveOutboundTracker(k), + ), + simulation.NewWeightedOperation( + weightWhitelistERC20, + SimulateMsgWhitelistERC20(k), + ), + simulation.NewWeightedOperation( + weightAbortStuckCCTX, + SimulateMsgAbortStuckCCTX(k), + ), + simulation.NewWeightedOperation( + weightRefundAbortedCCTX, + SimulateMsgRefundAbortedCCTX(k), + ), + simulation.NewWeightedOperation( + weightUpdateRateLimiterFlags, + SimulateMsgUpdateRateLimiterFlags(k), + ), + simulation.NewWeightedOperation( + weightUpdateERC20CustodyPauseStatus, + SimulateUpdateERC20CustodyPauseStatus(k), + ), + simulation.NewWeightedOperation( + weightUpdateTssAddress, + SimulateMsgUpdateTssAddress(k), + ), } } @@ -413,7 +232,21 @@ func GetRandomAccountAndObserver( return simtypes.Account{}, "", fmt.Errorf("no observers present in observer set found") } - randomObserver := GetRandomObserver(r, observers.ObserverList) + randomObserver := "" + foundObserver := false + for i := 0; i < 10; i++ { + randomObserver = GetRandomObserver(r, observers.ObserverList) + ok := k.GetObserverKeeper().IsNonTombstonedObserver(ctx, randomObserver) + if ok { + foundObserver = true + break + } + } + + if !foundObserver { + return simtypes.Account{}, "", fmt.Errorf("no observer found") + } + simAccount, err := GetObserverAccount(randomObserver, accounts) if err != nil { return simtypes.Account{}, "", err @@ -490,3 +323,76 @@ func GenAndDeliverTx( return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil } + +func ObserverVotesSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + observerVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + // The states are: + // column 1: All observers vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, but this is arbitrary and can be changed + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + return observerVotesTransitionMatrix, statePercentageArray, curNumVotesState +} + +func BallotVoteSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + ballotTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {70, 10, 20}, + {20, 30, 30}, + {10, 60, 50}, + }) + // The states are: + // column 1: 100% vote yes + // column 2: 50% vote yes + // column 3: 0% vote yes + // For all conditions we assume if the the vote is not a yes. + // then it is a no .Not voting condtion is handled by the ObserverVotesSimulationMatrix matrix + yesVoteArray := []float64{1, .5, 0} + ballotVotesState := 1 + return ballotTransitionMatrix, yesVoteArray, ballotVotesState +} + +func GetPolicyAccount(ctx sdk.Context, k types.AuthorityKeeper, accounts []simtypes.Account) (simtypes.Account, error) { + policies, found := k.GetPolicies(ctx) + if !found { + return simtypes.Account{}, fmt.Errorf("policies object not found") + } + if len(policies.Items) == 0 { + return simtypes.Account{}, fmt.Errorf("no policies found") + } + + admin := policies.Items[0].Address + address, err := observerTypes.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.Account{}, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.Account{}, fmt.Errorf("admin account not found in list of simulation accounts") + } + return simAccount, nil +} + +func GetAsset(ctx sdk.Context, k types.FungibleKeeper, chainID int64) (string, error) { + foreignCoins := k.GetAllForeignCoins(ctx) + asset := "" + + for _, coin := range foreignCoins { + if coin.ForeignChainId == chainID { + return coin.Asset, nil + } + } + + return asset, fmt.Errorf("asset not found for chain %d", chainID) +} diff --git a/x/crosschain/types/expected_keepers.go b/x/crosschain/types/expected_keepers.go index 2fee9cb31a..a738242a34 100644 --- a/x/crosschain/types/expected_keepers.go +++ b/x/crosschain/types/expected_keepers.go @@ -13,6 +13,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/proofs" + authoritytypes "github.com/zeta-chain/node/x/authority/types" fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -224,6 +225,7 @@ type FungibleKeeper interface { type AuthorityKeeper interface { CheckAuthorization(ctx sdk.Context, msg sdk.Msg) error GetAdditionalChainList(ctx sdk.Context) (list []chains.Chain) + GetPolicies(ctx sdk.Context) (val authoritytypes.Policies, found bool) } type LightclientKeeper interface { diff --git a/x/crosschain/types/keys.go b/x/crosschain/types/keys.go index 76d343dd3e..26422c6101 100644 --- a/x/crosschain/types/keys.go +++ b/x/crosschain/types/keys.go @@ -26,7 +26,8 @@ const ( ProtocolFee = 2000000000000000000 // CCTXIndexLength is the length of a crosschain transaction index - CCTXIndexLength = 66 + CCTXIndexLength = 66 + MaxOutboundTrackerHashes = 5 ) func GetProtocolFee() math.Uint { diff --git a/x/crosschain/types/outbound_tracker.go b/x/crosschain/types/outbound_tracker.go new file mode 100644 index 0000000000..af986b388c --- /dev/null +++ b/x/crosschain/types/outbound_tracker.go @@ -0,0 +1,6 @@ +package types + +// MaxReached returns true if the OutboundTracker has reached the maximum number of hashes it can store. +func (o *OutboundTracker) MaxReached() bool { + return len(o.HashList) >= MaxOutboundTrackerHashes +} diff --git a/x/crosschain/types/outbound_tracker_test.go b/x/crosschain/types/outbound_tracker_test.go new file mode 100644 index 0000000000..1357a76ee2 --- /dev/null +++ b/x/crosschain/types/outbound_tracker_test.go @@ -0,0 +1,48 @@ +package types_test + +import ( + "testing" + + "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestOutboundTracker_IsMaxed(t *testing.T) { + tests := []struct { + name string + tracker types.OutboundTracker + want bool + }{ + {"Not maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + }}, + false}, + + {"Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + }}, + true}, + {"More than Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + {TxHash: "hash6", TxSigner: "signer6"}, + {TxHash: "hash7", TxSigner: "signer7"}, + }}, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.tracker.MaxReached(); got != tt.want { + t.Errorf("OutboundTracker.MaxReached() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/x/crosschain/types/status.go b/x/crosschain/types/status.go index 5ebdf5391c..fc91b8c28b 100644 --- a/x/crosschain/types/status.go +++ b/x/crosschain/types/status.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "slices" ) func (m *Status) AbortRefunded() { @@ -56,12 +57,7 @@ func (m *Status) ValidateTransition(newStatus CctxStatus) bool { if !isOldStatusValid { return false } - for _, status := range nextStatusList { - if status == newStatus { - return true - } - } - return false + return slices.Contains(nextStatusList, newStatus) } func stateTransitionMap() map[CctxStatus][]CctxStatus { diff --git a/x/fungible/keeper/deposits.go b/x/fungible/keeper/deposits.go index b6a0c72c4e..3a5c24d1bb 100644 --- a/x/fungible/keeper/deposits.go +++ b/x/fungible/keeper/deposits.go @@ -99,6 +99,7 @@ func (k Keeper) getAndCheckZRC20( // this simplify the current workflow and allow to pause calls by pausing the gas token // TODO: refactor this logic and create specific workflow for no asset call // https://github.com/zeta-chain/node/issues/2627 + if coinType == coin.CoinType_Gas || coinType == coin.CoinType_NoAssetCall { foreignCoin, found = k.GetGasCoinForForeignCoin(ctx, chainID) if !found { diff --git a/x/fungible/simulation/operations.go b/x/fungible/simulation/operations.go index 2a14713979..c66d1bb3c9 100644 --- a/x/fungible/simulation/operations.go +++ b/x/fungible/simulation/operations.go @@ -16,6 +16,9 @@ import ( ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -24,9 +27,6 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( // #nosec G101 not a hardcoded credential OpWeightMsgDeploySystemContracts = "op_weight_msg_deploy_system_contracts" diff --git a/x/observer/genesis.go b/x/observer/genesis.go index e852e407bf..1910e8074d 100644 --- a/x/observer/genesis.go +++ b/x/observer/genesis.go @@ -11,13 +11,19 @@ import ( // InitGenesis initializes the observer module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - observerCount := uint64(0) if genState.Observers.Len() > 0 { k.SetObserverSet(ctx, genState.Observers) - observerCount = uint64(len(genState.Observers.ObserverList)) + } else { + k.SetObserverSet(ctx, types.ObserverSet{}) + } + + if genState.LastObserverCount != nil { + k.SetLastObserverCount(ctx, genState.LastObserverCount) + } else { + k.SetLastObserverCount(ctx, &types.LastObserverCount{LastChangeHeight: 0, Count: genState.Observers.LenUint()}) } - // if chain params are defined set them + // if chain params are defined, set them if len(genState.ChainParamsList.ChainParams) > 0 { k.SetChainParamsList(ctx, genState.ChainParamsList) } else { @@ -82,12 +88,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) }) } - if genState.LastObserverCount != nil { - k.SetLastObserverCount(ctx, genState.LastObserverCount) - } else { - k.SetLastObserverCount(ctx, &types.LastObserverCount{LastChangeHeight: 0, Count: observerCount}) - } - tss := types.TSS{} if genState.Tss != nil { tss = *genState.Tss diff --git a/x/observer/keeper/msg_server_update_observer.go b/x/observer/keeper/msg_server_update_observer.go index 2e60b291fb..7bf02e1b1c 100644 --- a/x/observer/keeper/msg_server_update_observer.go +++ b/x/observer/keeper/msg_server_update_observer.go @@ -36,6 +36,7 @@ func (k msgServer) UpdateObserver( "Observer address is not authorized : %s", msg.OldObserverAddress) } + // The New address should be a validator, not jailed and bonded err = k.IsValidator(ctx, msg.NewObserverAddress) if err != nil { return nil, errorsmod.Wrap(types.ErrUpdateObserver, err.Error()) @@ -72,7 +73,12 @@ func (k msgServer) UpdateObserver( return nil, errorsmod.Wrap(types.ErrLastObserverCountNotFound, "Observer count not found") } if lastBlockCount.Count != totalObserverCountCurrentBlock { - return nil, errorsmod.Wrap(types.ErrUpdateObserver, "Observer count mismatch") + return nil, errorsmod.Wrapf( + types.ErrUpdateObserver, + "Observer count mismatch current block: %d , last block: %d", + totalObserverCountCurrentBlock, + lastBlockCount.Count, + ) } return &types.MsgUpdateObserverResponse{}, nil } diff --git a/x/observer/keeper/msg_server_vote_tss.go b/x/observer/keeper/msg_server_vote_tss.go index 9b39174fd4..fc0156ffdd 100644 --- a/x/observer/keeper/msg_server_vote_tss.go +++ b/x/observer/keeper/msg_server_vote_tss.go @@ -27,8 +27,7 @@ const voteTSSid = "Vote TSS" func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types.MsgVoteTSSResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // Checks whether a signer is authorized to sign, by checking their address against the observer mapper - // which contains the observer list for the chain and type. + // Checks whether a signer is authorized to sign, by checking if the signer has a node account. _, found := k.GetNodeAccount(ctx, msg.Creator) if !found { return nil, errorsmod.Wrapf( @@ -104,7 +103,7 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types }, nil } - // For cases when an observer tries to vote for an older pending ballot, associated with a keygen that was discarded , we would return at this check while still adding the vote to the ballot + // For cases when an observer tries to vote for an older pending ballot, associated with a keygen that was discarded, we would return at this check while still adding the vote to the ballot if msg.KeygenZetaHeight != keygen.BlockNumber { return &types.MsgVoteTSSResponse{ VoteFinalized: isFinalized, diff --git a/x/observer/simulation/decoders.go b/x/observer/simulation/decoders.go index fd8eae7535..8d5243745b 100644 --- a/x/observer/simulation/decoders.go +++ b/x/observer/simulation/decoders.go @@ -19,79 +19,110 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { var crosschainFlagsA, crosschainFlagsB types.CrosschainFlags cdc.MustUnmarshal(kvA.Value, &crosschainFlagsA) cdc.MustUnmarshal(kvB.Value, &crosschainFlagsB) - return fmt.Sprintf("%v\n%v", crosschainFlagsA, crosschainFlagsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.CrosschainFlagsKey, + crosschainFlagsA, + crosschainFlagsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockObserverCountKey)): var lastBlockObserverCountA, lastBlockObserverCountB types.LastObserverCount cdc.MustUnmarshal(kvA.Value, &lastBlockObserverCountA) cdc.MustUnmarshal(kvB.Value, &lastBlockObserverCountB) - return fmt.Sprintf("%v\n%v", lastBlockObserverCountA, lastBlockObserverCountB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockObserverCountKey, + lastBlockObserverCountA, + lastBlockObserverCountB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.NodeAccountKey)): var nodeAccountA, nodeAccountB types.NodeAccount cdc.MustUnmarshal(kvA.Value, &nodeAccountA) cdc.MustUnmarshal(kvB.Value, &nodeAccountB) - return fmt.Sprintf("%v\n%v", nodeAccountA, nodeAccountB) + return fmt.Sprintf("key %s value A %v value B %v", types.NodeAccountKey, nodeAccountA, nodeAccountB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.KeygenKey)): var keygenA, keygenB types.Keygen cdc.MustUnmarshal(kvA.Value, &keygenA) cdc.MustUnmarshal(kvB.Value, &keygenB) - return fmt.Sprintf("%v\n%v", keygenA, keygenB) + return fmt.Sprintf("key %s value A %v value B %v", types.KeygenKey, keygenA, keygenB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.BallotListKey)): var ballotListA, ballotListB types.BallotListForHeight cdc.MustUnmarshal(kvA.Value, &ballotListA) cdc.MustUnmarshal(kvB.Value, &ballotListB) - return fmt.Sprintf("%v\n%v", ballotListA, ballotListB) + return fmt.Sprintf("key %s value A %v value B %v", types.BallotListKey, ballotListA, ballotListB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.VoterKey)): var voterA, voterB types.Ballot cdc.MustUnmarshal(kvA.Value, &voterA) cdc.MustUnmarshal(kvB.Value, &voterB) - return fmt.Sprintf("%v\n%v", voterA, voterB) + return fmt.Sprintf("key %s value A %v value B %v", types.VoterKey, voterA, voterB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSKey)): var tssA, tssB types.TSS cdc.MustUnmarshal(kvA.Value, &tssA) cdc.MustUnmarshal(kvB.Value, &tssB) - return fmt.Sprintf("%v\n%v", tssA, tssB) + return fmt.Sprintf("key %s value A %v value B %v", types.TSSKey, tssA, tssB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ObserverSetKey)): var observerSetA, observerSetB types.ObserverSet cdc.MustUnmarshal(kvA.Value, &observerSetA) cdc.MustUnmarshal(kvB.Value, &observerSetB) - return fmt.Sprintf("%v\n%v", observerSetA, observerSetB) + return fmt.Sprintf("key %s value A %v value B %v", types.ObserverSetKey, observerSetA, observerSetB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.AllChainParamsKey)): var allChainParamsA, allChainParamsB types.ChainParamsList cdc.MustUnmarshal(kvA.Value, &allChainParamsA) cdc.MustUnmarshal(kvB.Value, &allChainParamsB) - return fmt.Sprintf("%v\n%v", allChainParamsA, allChainParamsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.AllChainParamsKey, + allChainParamsA, + allChainParamsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSHistoryKey)): var tssHistoryA, tssHistoryB types.TSS cdc.MustUnmarshal(kvA.Value, &tssHistoryA) cdc.MustUnmarshal(kvB.Value, &tssHistoryB) - return fmt.Sprintf("%v\n%v", tssHistoryA, tssHistoryB) + return fmt.Sprintf("key %s value A %v value B %v", types.TSSHistoryKey, tssHistoryA, tssHistoryB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TssFundMigratorKey)): var tssFundMigratorA, tssFundMigratorB types.TssFundMigratorInfo cdc.MustUnmarshal(kvA.Value, &tssFundMigratorA) cdc.MustUnmarshal(kvB.Value, &tssFundMigratorB) - return fmt.Sprintf("%v\n%v", tssFundMigratorA, tssFundMigratorB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.TssFundMigratorKey, + tssFundMigratorA, + tssFundMigratorB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.PendingNoncesKeyPrefix)): var pendingNoncesA, pendingNoncesB types.PendingNonces cdc.MustUnmarshal(kvA.Value, &pendingNoncesA) cdc.MustUnmarshal(kvB.Value, &pendingNoncesB) - return fmt.Sprintf("%v\n%v", pendingNoncesA, pendingNoncesB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.PendingNoncesKeyPrefix, + pendingNoncesA, + pendingNoncesB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ChainNoncesKey)): var chainNoncesA, chainNoncesB types.ChainNonces cdc.MustUnmarshal(kvA.Value, &chainNoncesA) cdc.MustUnmarshal(kvB.Value, &chainNoncesB) - return fmt.Sprintf("%v\n%v", chainNoncesA, chainNoncesB) + return fmt.Sprintf("key %s value A %v value B %v", types.ChainNoncesKey, chainNoncesA, chainNoncesB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.NonceToCctxKeyPrefix)): var nonceToCctxA, nonceToCctxB types.NonceToCctx cdc.MustUnmarshal(kvA.Value, &nonceToCctxA) cdc.MustUnmarshal(kvB.Value, &nonceToCctxB) - return fmt.Sprintf("%v\n%v", nonceToCctxA, nonceToCctxB) + return fmt.Sprintf("key %s value A %v value B %v", types.NonceToCctxKeyPrefix, nonceToCctxA, nonceToCctxB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ParamsKey)): var paramsA, paramsB types.Params cdc.MustUnmarshal(kvA.Value, ¶msA) cdc.MustUnmarshal(kvB.Value, ¶msB) - return fmt.Sprintf("%v\n%v", paramsA, paramsB) + return fmt.Sprintf("key %s value A %v value B %v", types.ParamsKey, paramsA, paramsB) default: - panic(fmt.Sprintf("invalid observer key prefix %X", kvA.Key)) + panic( + fmt.Sprintf( + "invalid observer key prefix %X (first 8 bytes: %X)", + kvA.Key[:1], + kvA.Key[:min(8, len(kvA.Key))], + ), + ) } } } diff --git a/x/observer/simulation/decoders_test.go b/x/observer/simulation/decoders_test.go index 616994a664..d5a397d3ad 100644 --- a/x/observer/simulation/decoders_test.go +++ b/x/observer/simulation/decoders_test.go @@ -62,21 +62,44 @@ func TestNewDecodeStore(t *testing.T) { name string expectedLog string }{ - {"CrosschainFlags", fmt.Sprintf("%v\n%v", *crosschainFlags, *crosschainFlags)}, - {"LastBlockObserverCount", fmt.Sprintf("%v\n%v", *lastBlockObserverCount, *lastBlockObserverCount)}, - {"NodeAccount", fmt.Sprintf("%v\n%v", *nodeAccount, *nodeAccount)}, - {"Keygen", fmt.Sprintf("%v\n%v", *keygen, *keygen)}, - {"BallotList", fmt.Sprintf("%v\n%v", ballotList, ballotList)}, - {"Ballot", fmt.Sprintf("%v\n%v", *ballot, *ballot)}, - {"TSS", fmt.Sprintf("%v\n%v", tss, tss)}, - {"TSSHistory", fmt.Sprintf("%v\n%v", tss, tss)}, - {"ObserverSet", fmt.Sprintf("%v\n%v", observerSet, observerSet)}, - {"ChainParamsList", fmt.Sprintf("%v\n%v", chainParamsList, chainParamsList)}, - {"TssFundMigrator", fmt.Sprintf("%v\n%v", tssFundMigrator, tssFundMigrator)}, - {"PendingNonces", fmt.Sprintf("%v\n%v", pendingNonce, pendingNonce)}, - {"ChainNonces", fmt.Sprintf("%v\n%v", chainNonces, chainNonces)}, - {"NonceToCctx", fmt.Sprintf("%v\n%v", nonceToCctx, nonceToCctx)}, - {"Params", fmt.Sprintf("%v\n%v", params, params)}, + { + "CrosschainFlags", + fmt.Sprintf("key %s value A %v value B %v", types.CrosschainFlagsKey, *crosschainFlags, *crosschainFlags), + }, + { + "LastBlockObserverCount", + fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockObserverCountKey, + *lastBlockObserverCount, + *lastBlockObserverCount, + ), + }, + {"NodeAccount", fmt.Sprintf("key %s value A %v value B %v", types.NodeAccountKey, *nodeAccount, *nodeAccount)}, + {"Keygen", fmt.Sprintf("key %s value A %v value B %v", types.KeygenKey, *keygen, *keygen)}, + {"BallotList", fmt.Sprintf("key %s value A %v value B %v", types.BallotListKey, ballotList, ballotList)}, + {"Ballot", fmt.Sprintf("key %s value A %v value B %v", types.VoterKey, *ballot, *ballot)}, + {"TSS", fmt.Sprintf("key %s value A %v value B %v", types.TSSKey, tss, tss)}, + {"TSSHistory", fmt.Sprintf("key %s value A %v value B %v", types.TSSHistoryKey, tss, tss)}, + {"ObserverSet", fmt.Sprintf("key %s value A %v value B %v", types.ObserverSetKey, observerSet, observerSet)}, + { + "ChainParamsList", + fmt.Sprintf("key %s value A %v value B %v", types.AllChainParamsKey, chainParamsList, chainParamsList), + }, + { + "TssFundMigrator", + fmt.Sprintf("key %s value A %v value B %v", types.TssFundMigratorKey, tssFundMigrator, tssFundMigrator), + }, + { + "PendingNonces", + fmt.Sprintf("key %s value A %v value B %v", types.PendingNoncesKeyPrefix, pendingNonce, pendingNonce), + }, + {"ChainNonces", fmt.Sprintf("key %s value A %v value B %v", types.ChainNoncesKey, chainNonces, chainNonces)}, + { + "NonceToCctx", + fmt.Sprintf("key %s value A %v value B %v", types.NonceToCctxKeyPrefix, nonceToCctx, nonceToCctx), + }, + {"Params", fmt.Sprintf("key %s value A %v value B %v", types.ParamsKey, params, params)}, } for i, tt := range tests { diff --git a/x/observer/simulation/operation_add_observer.go b/x/observer/simulation/operation_add_observer.go new file mode 100644 index 0000000000..95eff5ddbb --- /dev/null +++ b/x/observer/simulation/operation_add_observer.go @@ -0,0 +1,95 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateAddObserver generates a TypeMsgAddObserver and delivers it. This message sets AddNodeAccountOnly to false; +// Therefore, it adds the observer to the observer set +func SimulateAddObserver(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + observerSet, found := k.GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no observer set found", + ), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerSet.ObserverList { + observerMap[observer] = true + } + + nodeAccounts := k.GetAllNodeAccount(ctx) + + // Pick a random observer which part of the node account but not in the observer set + // New accounts are added to the node account list via SimulateAddObserverNodeAccount + var newObserver string + foundNA := RepeatCheck(func() bool { + newObserver = nodeAccounts[r.Intn(len(nodeAccounts))].Operator + if _, found := observerMap[newObserver]; !found { + return true + } + return false + }) + if !foundNA { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no node accounts available which can be added as observer", + ), nil, nil + } + pubkey, err := sample.PubkeyStringFromRand(r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + msg := types.MsgAddObserver{ + Creator: policyAccount.Address.String(), + ObserverAddress: newObserver, + ZetaclientGranteePubkey: pubkey, + AddNodeAccountOnly: false, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_add_observer_node_account.go b/x/observer/simulation/operation_add_observer_node_account.go new file mode 100644 index 0000000000..e182123fe1 --- /dev/null +++ b/x/observer/simulation/operation_add_observer_node_account.go @@ -0,0 +1,110 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateAddObserverNodeAccount generates a TypeMsgAddObserver and delivers it. +// This message sets AddNodeAccountOnly to true to it does not add the observer to the observer set +func SimulateAddObserverNodeAccount(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + observerSet, found := k.GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no observer set found", + ), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerSet.ObserverList { + observerMap[observer] = true + } + + validators := k.GetStakingKeeper().GetAllValidators(ctx) + if len(validators) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no validators found", + ), nil, nil + } + newObserver := "" + foundNewObserver := RepeatCheck(func() bool { + randomValidator := validators[r.Intn(len(validators))] + randomValidatorAddress, err := types.GetAccAddressFromOperatorAddress(randomValidator.OperatorAddress) + if err != nil { + return false + } + newObserver = randomValidatorAddress.String() + err = k.IsValidator(ctx, newObserver) + if err != nil { + return false + } + if _, ok := observerMap[newObserver]; !ok { + return true + } + return false + }) + + if !foundNewObserver { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no new observer found", + ), nil, nil + } + + pubkey, err := sample.PubkeyStringFromRand(r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + msg := types.MsgAddObserver{ + Creator: policyAccount.Address.String(), + ObserverAddress: newObserver, + ZetaclientGranteePubkey: pubkey, + AddNodeAccountOnly: true, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_disable_cctx.go b/x/observer/simulation/operation_disable_cctx.go new file mode 100644 index 0000000000..9364aa2d87 --- /dev/null +++ b/x/observer/simulation/operation_disable_cctx.go @@ -0,0 +1,55 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateDisableCCTX generates a MsgDisableCCTX and delivers it. +func SimulateDisableCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDisableCCTX, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgDisableCCTX{ + Creator: policyAccount.Address.String(), + DisableInbound: false, + DisableOutbound: true, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_enable_cctx.go b/x/observer/simulation/operation_enable_cctx.go new file mode 100644 index 0000000000..7ee1d06776 --- /dev/null +++ b/x/observer/simulation/operation_enable_cctx.go @@ -0,0 +1,55 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateEnableCCTX generates a MsgEnableCCTX and delivers it. +func SimulateEnableCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgEnableCCTX{ + Creator: policyAccount.Address.String(), + EnableInbound: true, + EnableOutbound: true, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_remove_chain_params.go b/x/observer/simulation/operation_remove_chain_params.go new file mode 100644 index 0000000000..a0e9f71681 --- /dev/null +++ b/x/observer/simulation/operation_remove_chain_params.go @@ -0,0 +1,83 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateMsgRemoveChainParams generates a MsgRemoveChainParams and delivers it. This message removes a chain from the list +// This is not being run right now as the removal causes a lot of errors for the other operations. +func SimulateMsgRemoveChainParams(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgRemoveChainParams, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveChainParams, + "no supported chains found", + ), nil, nil + } + + randomExternalChain := int64(0) + foundExternalChain := RepeatCheck(func() bool { + c := supportedChains[r.Intn(len(supportedChains))] + if !c.IsZetaChain() { + randomExternalChain = c.ChainId + return true + } + return false + }) + + if !foundExternalChain { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveChainParams, + "no external chain found", + ), nil, nil + } + + msg := types.MsgRemoveChainParams{ + Creator: policyAccount.Address.String(), + ChainId: randomExternalChain, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_reset_chain_nonces.go b/x/observer/simulation/operation_reset_chain_nonces.go new file mode 100644 index 0000000000..38ccacac37 --- /dev/null +++ b/x/observer/simulation/operation_reset_chain_nonces.go @@ -0,0 +1,87 @@ +package simulation + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateResetChainNonces generates a MsgResetChainNonces and delivers it. +func SimulateResetChainNonces(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgResetChainNonces, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + randomChain, err := GetExternalChain(ctx, k, r) + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgResetChainNonces, + err.Error(), + ), nil, fmt.Errorf( + "error getting external chain", + ) + } + + tss, found := k.GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgResetChainNonces, + "TSS not found", + ), nil, fmt.Errorf( + "TSS not found", + ) + } + pendingNonces, found := k.GetPendingNonces(ctx, tss.TssPubkey, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgResetChainNonces, "Pending nonces not found"), nil, + fmt.Errorf("pending nonces not found for chain %d %s", randomChain.ChainId, randomChain.ChainName) + } + + nonceIncrement := int64(r.Intn(99)) + 1 + + msg := types.MsgResetChainNonces{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + ChainNonceHigh: pendingNonces.NonceHigh + nonceIncrement, + ChainNonceLow: pendingNonces.NonceLow + nonceIncrement, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_chain_params.go b/x/observer/simulation/operation_update_chain_params.go new file mode 100644 index 0000000000..21f45d9ee3 --- /dev/null +++ b/x/observer/simulation/operation_update_chain_params.go @@ -0,0 +1,67 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateChainParams generates a MsgUpdateChainParams and delivers it. +func SimulateUpdateChainParams(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + randomChain, err := GetExternalChain(ctx, k, r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + cp := sample.ChainParamsFromRand(r, randomChain.ChainId) + err = types.ValidateChainParams(cp) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + msg := types.MsgUpdateChainParams{ + Creator: policyAccount.Address.String(), + ChainParams: cp, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_gas_price_increase_flags.go b/x/observer/simulation/operation_update_gas_price_increase_flags.go new file mode 100644 index 0000000000..fdb59d789d --- /dev/null +++ b/x/observer/simulation/operation_update_gas_price_increase_flags.go @@ -0,0 +1,58 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateGasPriceIncreaseFlags generates a MsgUpdateGasPriceIncreaseFlags with random values +func SimulateUpdateGasPriceIncreaseFlags(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateGasPriceIncreaseFlags, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + gp := sample.GasPriceIncreaseFlagsFromRand(r) + + msg := types.MsgUpdateGasPriceIncreaseFlags{ + Creator: policyAccount.Address.String(), + GasPriceIncreaseFlags: gp, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_keygen.go b/x/observer/simulation/operation_update_keygen.go new file mode 100644 index 0000000000..c6718b0dcc --- /dev/null +++ b/x/observer/simulation/operation_update_keygen.go @@ -0,0 +1,66 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateKeygen generates a MsgUpdateKeygen and delivers it. +func SimulateUpdateKeygen(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateKeygen, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + _, found := k.GetKeygen(ctx) + if !found { + kg := sample.KeygenFromRand(r) + k.SetKeygen(ctx, kg) + } + + blockHeightMin := ctx.BlockHeight() + 11 + blockHeightMax := ctx.BlockHeight() + 1000 + keygenBlockHeight := int64(r.Intn(int(blockHeightMax-blockHeightMin))) + blockHeightMin + + msg := types.MsgUpdateKeygen{ + Creator: policyAccount.Address.String(), + Block: keygenBlockHeight, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_observer.go b/x/observer/simulation/operation_update_observer.go new file mode 100644 index 0000000000..bc2f729942 --- /dev/null +++ b/x/observer/simulation/operation_update_observer.go @@ -0,0 +1,118 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateObserver generates a TypeMsgUpdateObserver and delivers it. +func SimulateUpdateObserver(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + _, randomObserver, observerList, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateObserver, err.Error()), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerList { + observerMap[observer] = true + } + + validators := k.GetStakingKeeper().GetAllValidators(ctx) + if len(validators) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no validators found", + ), nil, nil + } + + newObserver := "" + foundNewObserver := RepeatCheck(func() bool { + randomValidator := validators[r.Intn(len(validators))] + randomValidatorAddress, err := types.GetAccAddressFromOperatorAddress(randomValidator.OperatorAddress) + if err != nil { + return false + } + newObserver = randomValidatorAddress.String() + err = k.IsValidator(ctx, newObserver) + if err != nil { + return false + } + if _, ok := observerMap[newObserver]; !ok { + return true + } + return false + }) + + if !foundNewObserver { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no new observer found", + ), nil, nil + } + + lastBlockCount, found := k.GetLastObserverCount(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no last block count found", + ), nil, nil + } + // #nosec G115 - overflow is not a concern here + if int(lastBlockCount.Count) != len(observerList) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "observer count mismatch", + ), nil, nil + } + + msg := types.MsgUpdateObserver{ + Creator: policyAccount.Address.String(), + OldObserverAddress: randomObserver, + NewObserverAddress: newObserver, + UpdateReason: types.ObserverUpdateReason_AdminUpdate, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_vote_tss.go b/x/observer/simulation/operation_vote_tss.go new file mode 100644 index 0000000000..8dc3c5668d --- /dev/null +++ b/x/observer/simulation/operation_vote_tss.go @@ -0,0 +1,177 @@ +package simulation + +import ( + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// operationSimulateVoteTss generates a MsgVoteTSS with random values +func operationSimulateVoteTss( + k keeper.Keeper, + msg types.MsgVoteTSS, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateMsgVoteTSS generates a MsgVoteTSS with random values and delivers it, it also schedules future votes for the same ballot +func SimulateMsgVoteTSS(k keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + yesVote := chains.ReceiveStatus_success + noVote := chains.ReceiveStatus_failed + ballotVotesTransitionMatrix, yesVotePercentageArray, ballotVotesState := BallotVoteSimulationMatrix() + nodeAccounts := k.GetAllNodeAccount(ctx) + numVotes := len(nodeAccounts) + ballotVotesState = ballotVotesTransitionMatrix.NextState(r, ballotVotesState) + yesVotePercentage := yesVotePercentageArray[ballotVotesState] + numberOfYesVotes := int(math.Ceil(float64(numVotes) * yesVotePercentage)) + + vote := yesVote + if numberOfYesVotes == 0 { + vote = noVote + } + + newTss, err := sample.TSSFromRand(r) + if err != nil { + return simtypes.OperationMsg{}, nil, err + } + + keygen, found := k.GetKeygen(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVoteTSS, "keygen not found"), nil, nil + } + + msg := types.MsgVoteTSS{ + Creator: "", + TssPubkey: newTss.TssPubkey, + KeygenZetaHeight: keygen.BlockNumber, + Status: vote, + } + + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomNodeAccount(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first tss vote"), nil, err + } + + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + var fops []simtypes.FutureOperation + + for voteCount, nodeAccount := range nodeAccounts { + if vote == yesVote && voteCount == numberOfYesVotes { + vote = noVote + } + // firstVoter has already voted. + if nodeAccount.Operator == firstVoter { + continue + } + observerAccount, err := GetSimAccount(nodeAccount.Operator, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = nodeAccount.Operator + votingMsg.Status = vote + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteTss(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/observer/simulation/operations.go b/x/observer/simulation/operations.go index 7c12ced3c0..1132e8a87e 100644 --- a/x/observer/simulation/operations.go +++ b/x/observer/simulation/operations.go @@ -1,20 +1,24 @@ package simulation import ( + "fmt" "math/rand" - "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/x/observer/keeper" "github.com/zeta-chain/node/x/observer/types" ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -23,85 +27,389 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( - // #nosec G101 not a hardcoded credential - OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" - // DefaultWeightMsgTypeMsgEnableCCTX We ues a high weight for this operation - // to ensure that it is present in the block more number of times than any operation that changes the validator set + OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgDisableCCTX = "op_weight_msg_disable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgVoteTSS = "op_weight_msg_vote_tss" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateKeygen = "op_weight_msg_update_keygen" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateObserver = "op_weight_msg_update_observer" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateChainParams = "op_weight_msg_update_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgRemoveChainParams = "op_weight_msg_remove_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgResetChainNonces = "op_weight_msg_reset_chain_nonces" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = "op_weight_msg_update_gas_price_increase_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgAddObserver = "op_weight_msg_add_observer" // #nosec G101 not a hardcoded credential + // DefaultWeightMsgTypeMsgEnableCCTX We use a high weight for this operation + // to ensure that it is present in the block more number of times than any operation that changes the validator set // Arrived at this number based on the weights used in the cosmos sdk staking module and through some trial and error - DefaultWeightMsgTypeMsgEnableCCTX = 3650 + DefaultWeightMsgTypeMsgEnableCCTX = 100 + DefaultWeightMsgTypeMsgDisableCCTX = 10 + DefaultWeightMsgTypeMsgVoteTSS = 10 + DefaultWeightMsgTypeMsgUpdateKeygen = 10 + DefaultWeightMsgTypeMsgUpdateObserver = 10 + DefaultWeightMsgTypeMsgUpdateChainParams = 10 + DefaultWeightMsgTypeMsgRemoveChainParams = 10 + DefaultWeightMsgTypeMsgResetChainNonces = 5 + DefaultWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = 10 + DefaultWeightMsgTypeMsgAddObserver = 5 + + DefaultRetryCount = 10 ) // WeightedOperations for observer module func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper, ) simulation.WeightedOperations { - var weightMsgTypeMsgEnableCCTX int + var ( + weightMsgTypeMsgEnableCCTX int + weightMsgTypeMsgDisableCCTX int + weightMsgTypeMsgVoteTSS int + weightMsgTypeMsgUpdateKeygen int + weightMsgTypeMsgUpdateObserver int + weightMsgTypeMsgUpdateChainParams int + weightMsgTypeMsgRemoveChainParams int + weightMsgTypeMsgResetChainNonces int + weightMsgTypeMsgUpdateGasPriceIncreaseFlags int + weightMsgTypeMsgAddObserver int + ) appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgEnableCCTX, &weightMsgTypeMsgEnableCCTX, nil, func(_ *rand.Rand) { weightMsgTypeMsgEnableCCTX = DefaultWeightMsgTypeMsgEnableCCTX }) + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgDisableCCTX, &weightMsgTypeMsgDisableCCTX, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgDisableCCTX = DefaultWeightMsgTypeMsgDisableCCTX + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgVoteTSS, &weightMsgTypeMsgVoteTSS, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgVoteTSS = DefaultWeightMsgTypeMsgVoteTSS + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateKeygen, &weightMsgTypeMsgUpdateKeygen, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateKeygen = DefaultWeightMsgTypeMsgUpdateKeygen + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateObserver, &weightMsgTypeMsgUpdateObserver, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateObserver = DefaultWeightMsgTypeMsgUpdateObserver + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateChainParams, &weightMsgTypeMsgUpdateChainParams, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateChainParams = DefaultWeightMsgTypeMsgUpdateChainParams + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgRemoveChainParams, &weightMsgTypeMsgRemoveChainParams, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgRemoveChainParams = DefaultWeightMsgTypeMsgRemoveChainParams + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgResetChainNonces, &weightMsgTypeMsgResetChainNonces, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgResetChainNonces = DefaultWeightMsgTypeMsgResetChainNonces + }) + + appParams.GetOrGenerate( + cdc, + OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags, + &weightMsgTypeMsgUpdateGasPriceIncreaseFlags, + nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateGasPriceIncreaseFlags = DefaultWeightMsgTypeMsgUpdateGasPriceIncreaseFlags + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgAddObserver, &weightMsgTypeMsgAddObserver, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgAddObserver = DefaultWeightMsgTypeMsgAddObserver + }) + return simulation.WeightedOperations{ simulation.NewWeightedOperation( weightMsgTypeMsgEnableCCTX, - SimulateMsgTypeMsgEnableCCTX(k), + SimulateEnableCCTX(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgDisableCCTX, + SimulateDisableCCTX(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateKeygen, + SimulateUpdateKeygen(k), + ), + // + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateChainParams, + SimulateUpdateChainParams(k), + ), + // + //simulation.NewWeightedOperation( + // weightMsgTypeMsgRemoveChainParams, + // SimulateMsgRemoveChainParams(k), + //), + + simulation.NewWeightedOperation( + weightMsgTypeMsgResetChainNonces, + SimulateResetChainNonces(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateGasPriceIncreaseFlags, + SimulateUpdateGasPriceIncreaseFlags(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateUpdateObserver(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateAddObserverNodeAccount(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateAddObserver(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgVoteTSS, + SimulateMsgVoteTSS(k), ), } } -// SimulateMsgTypeMsgEnableCCTX generates a MsgEnableCCTX and delivers it. -func SimulateMsgTypeMsgEnableCCTX(k keeper.Keeper) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - policies, found := k.GetAuthorityKeeper().GetPolicies(ctx) - if !found { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "policies object not found"), nil, nil - } - if len(policies.Items) == 0 { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "no policies found"), nil, nil +func GetPolicyAccount(ctx sdk.Context, k types.AuthorityKeeper, accounts []simtypes.Account) (simtypes.Account, error) { + policies, found := k.GetPolicies(ctx) + if !found { + return simtypes.Account{}, fmt.Errorf("policies object not found") + } + if len(policies.Items) == 0 { + return simtypes.Account{}, fmt.Errorf("no policies found") + } + + admin := policies.Items[0].Address + address, err := types.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.Account{}, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.Account{}, fmt.Errorf("admin account not found in list of simulation accounts") + } + return simAccount, nil +} + +func GetExternalChain(ctx sdk.Context, k keeper.Keeper, r *rand.Rand) (chains.Chain, error) { + supportedChains := k.GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return chains.Chain{}, fmt.Errorf("no supported chains found") + } + externalChain := chains.Chain{} + foundExternalChain := RepeatCheck(func() bool { + c := supportedChains[r.Intn(len(supportedChains))] + if !c.IsZetaChain() { + externalChain = c + return true } + return false + }) - admin := policies.Items[0].Address - address, err := types.GetOperatorAddressFromAccAddress(admin) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, err.Error()), nil, err + if !foundExternalChain { + return chains.Chain{}, fmt.Errorf("no external chain found") + } + return externalChain, nil +} + +// GetRandomAccountAndObserver returns a random account and the associated observer address +func GetRandomAccountAndObserver( + r *rand.Rand, + ctx sdk.Context, + k keeper.Keeper, + accounts []simtypes.Account, +) (simtypes.Account, string, []string, error) { + observerList := []string{} + observers, found := k.GetObserverSet(ctx) + if !found { + return simtypes.Account{}, "", observerList, fmt.Errorf("observer set not found") + } + + observerList = observers.ObserverList + + if len(observers.ObserverList) == 0 { + return simtypes.Account{}, "", observerList, fmt.Errorf("no observers present in observer set found") + } + + randomObserver := "" + foundObserver := RepeatCheck(func() bool { + randomObserver = GetRandomObserver(r, observerList) + _, foundNodeAccount := k.GetNodeAccount(ctx, randomObserver) + if !foundNodeAccount { + return false } - simAccount, found := simtypes.FindAccount(accounts, address) - if !found { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "admin account not found"), nil, nil + ok := k.IsNonTombstonedObserver(ctx, randomObserver) + if ok { + return true } + return false + }) - msg := types.MsgEnableCCTX{ - Creator: simAccount.Address.String(), - EnableInbound: true, - EnableOutbound: false, - } + if !foundObserver { + return simtypes.Account{}, "", nil, fmt.Errorf("no observer found") + } - err = msg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err - } + simAccount, err := GetSimAccount(randomObserver, accounts) + if err != nil { + return simtypes.Account{}, "", observerList, err + } + return simAccount, randomObserver, observerList, nil +} + +func GetRandomNodeAccount( + r *rand.Rand, + ctx sdk.Context, + k keeper.Keeper, + accounts []simtypes.Account, +) (simtypes.Account, string, error) { + nodeAccounts := k.GetAllNodeAccount(ctx) + + if len(nodeAccounts) == 0 { + return simtypes.Account{}, "", fmt.Errorf("no node accounts present") + } + + randomNodeAccount := nodeAccounts[r.Intn(len(nodeAccounts))].Operator - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, + simAccount, err := GetSimAccount(randomNodeAccount, accounts) + if err != nil { + return simtypes.Account{}, "", err + } + return simAccount, randomNodeAccount, nil +} + +func GetRandomObserver(r *rand.Rand, observerList []string) string { + idx := r.Intn(len(observerList)) + return observerList[idx] +} + +// GetSimAccount returns the account associated with the observer address from the list of accounts provided +// GetSimAccount can fail if all the observers are removed from the observer set ,this can happen +//if the other modules create transactions which affect the validator +//and triggers any of the staking hooks defined in the observer modules + +func GetSimAccount(observerAddress string, accounts []simtypes.Account) (simtypes.Account, error) { + operatorAddress, err := types.GetOperatorAddressFromAccAddress(observerAddress) + if err != nil { + return simtypes.Account{}, fmt.Errorf("validator not found for observer ") + } + + simAccount, found := simtypes.FindAccount(accounts, operatorAddress) + if !found { + return simtypes.Account{}, fmt.Errorf("operator account not found") + } + return simAccount, nil +} + +func RepeatCheck(fn func() bool) bool { + for i := 0; i < DefaultRetryCount; i++ { + if fn() { + return true } + } + return false +} - return simulation.GenAndDeliverTxWithRandFees(txCtx) +func ObserverVotesSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + observerVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + // The states are: + // column 1: All observers vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, but this is arbitrary and can be changed + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + return observerVotesTransitionMatrix, statePercentageArray, curNumVotesState +} + +func BallotVoteSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + ballotTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {70, 10}, + {30, 10}, + }) + // The states are: + // column 1: 100% vote yes + // column 2: 0% vote yes + // For all conditions we assume if the vote is not a yes + // then it is a no . + yesVoteArray := []float64{1, 0} + ballotVotesState := 1 + return ballotTransitionMatrix, yesVoteArray, ballotVotesState +} + +// GenAndDeliverTxWithRandFees generates a transaction with a random fee and delivers it. +func GenAndDeliverTxWithRandFees( + txCtx simulation.OperationInput, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + spendable := txCtx.Bankkeeper.SpendableCoins(txCtx.Context, account.GetAddress()) + + var fees sdk.Coins + var err error + + coins, hasNeg := spendable.SafeSub(txCtx.CoinsSpentInMsg...) + if hasNeg { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "message doesn't leave room for fees"), nil, err + } + + fees, err = simtypes.RandomFees(txCtx.R, txCtx.Context, coins) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate fees"), nil, err } + return GenAndDeliverTx(txCtx, fees) +} + +// GenAndDeliverTx generates a transactions and delivers it with the provided fees. +// This function does not return an error if the transaction fails to deliver. +func GenAndDeliverTx( + txCtx simulation.OperationInput, + fees sdk.Coins, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + tx, err := simtestutil.GenSignedMockTx( + txCtx.R, + txCtx.TxGen, + []sdk.Msg{txCtx.Msg}, + fees, + simtestutil.DefaultGenTxGas, + txCtx.Context.ChainID(), + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + txCtx.SimAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate mock tx"), nil, err + } + + _, _, err = txCtx.App.SimDeliver(txCtx.TxGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to deliver tx"), nil, nil + } + + return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil } diff --git a/x/observer/types/ballot.go b/x/observer/types/ballot.go index 6c04963cf9..2d217c0952 100644 --- a/x/observer/types/ballot.go +++ b/x/observer/types/ballot.go @@ -79,8 +79,8 @@ func (m Ballot) IsFinalizingVote() (Ballot, bool) { return m, false } -func CreateVotes(len int) []VoteType { - voterList := make([]VoteType, len) +func CreateVotes(listSize int) []VoteType { + voterList := make([]VoteType, listSize) for i := range voterList { voterList[i] = VoteType_NotYetVoted } diff --git a/x/observer/types/expected_keepers.go b/x/observer/types/expected_keepers.go index 2788187c94..15eb476af6 100644 --- a/x/observer/types/expected_keepers.go +++ b/x/observer/types/expected_keepers.go @@ -19,6 +19,7 @@ type StakingKeeper interface { valAddr sdk.ValAddress, ) (delegation stakingtypes.Delegation, found bool) SetValidator(ctx sdk.Context, validator stakingtypes.Validator) + GetAllValidators(ctx sdk.Context) (validators []stakingtypes.Validator) } type SlashingKeeper interface { diff --git a/x/observer/types/keys.go b/x/observer/types/keys.go index ed57b13d79..e2bed33dec 100644 --- a/x/observer/types/keys.go +++ b/x/observer/types/keys.go @@ -71,6 +71,10 @@ const ( NodeAccountKey = "NodeAccount-value-" KeygenKey = "Keygen-value-" + // TODO remove unused keys + BlockHeaderKey = "BlockHeader-value-" + BlockHeaderStateKey = "BlockHeaderState-value-" + // TODO rename to BallotListForHeightKey BallotListKey = "BallotList-value-" TSSKey = "TSS-value-" diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 5afb1cae11..699629684f 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -166,7 +166,13 @@ func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.mu.Lock() defer ob.mu.Unlock() + if observertypes.ChainParamsEqual(ob.chainParams, params) { + return + } + ob.chainParams = params + + ob.logger.Chain.Info().Any("observer.chain_params", params).Msg("updated chain params") } // ZetacoreClient returns the zetacore client for the observer. diff --git a/zetaclient/chains/bitcoin/bitcoin.go b/zetaclient/chains/bitcoin/bitcoin.go new file mode 100644 index 0000000000..3a901c4bee --- /dev/null +++ b/zetaclient/chains/bitcoin/bitcoin.go @@ -0,0 +1,233 @@ +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" + "github.com/zeta-chain/node/zetaclient/common" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/outboundprocessor" +) + +type Bitcoin struct { + scheduler *scheduler.Scheduler + observer *observer.Observer + signer *signer.Signer + proc *outboundprocessor.Processor +} + +func New( + scheduler *scheduler.Scheduler, + observer *observer.Observer, + signer *signer.Signer, +) *Bitcoin { + // TODO move this to base signer + // https://github.com/zeta-chain/node/issues/3330 + proc := outboundprocessor.NewProcessor(observer.Logger().Outbound) + + return &Bitcoin{ + scheduler: scheduler, + observer: observer, + signer: signer, + proc: proc, + } +} + +func (b *Bitcoin) Chain() chains.Chain { + return b.observer.Chain() +} + +func (b *Bitcoin) Start(ctx context.Context) error { + if ok := b.observer.Observer.Start(); !ok { + return errors.New("observer is already started") + } + + app, err := zctx.FromContext(ctx) + if err != nil { + return errors.Wrap(err, "unable to get app from context") + } + + newBlockChan, err := b.observer.ZetacoreClient().NewBlockSubscriber(ctx) + if err != nil { + return errors.Wrap(err, "unable to create new block subscriber") + } + + optInboundInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().InboundTicker) + }) + + optGasInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().GasPriceTicker) + }) + + optUTXOInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().WatchUtxoTicker) + }) + + optMempoolInterval := scheduler.Interval(common.MempoolStuckTxCheckInterval) + + optOutboundInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(b.observer.ChainParams().OutboundTicker) + }) + + optInboundSkipper := scheduler.Skipper(func() bool { + return !app.IsInboundObservationEnabled() + }) + + optOutboundSkipper := scheduler.Skipper(func() bool { + return !app.IsOutboundObservationEnabled() + }) + + optGenericSkipper := scheduler.Skipper(func() bool { + return !b.observer.ChainParams().IsSupported + }) + + register := func(exec scheduler.Executable, name string, opts ...scheduler.Opt) { + opts = append([]scheduler.Opt{ + scheduler.GroupName(b.group()), + scheduler.Name(name), + }, opts...) + + b.scheduler.Register(ctx, exec, opts...) + } + + // Observers + register(b.observer.ObserveInbound, "observe_inbound", optInboundInterval, optInboundSkipper) + register(b.observer.ObserveInboundTrackers, "observe_inbound_trackers", optInboundInterval, optInboundSkipper) + register(b.observer.FetchUTXOs, "fetch_utxos", optUTXOInterval, optGenericSkipper) + register(b.observer.WatchMempoolTxs, "watch_mempool_txs", optMempoolInterval) + register(b.observer.PostGasPrice, "post_gas_price", optGasInterval, optGenericSkipper) + register(b.observer.CheckRPCStatus, "check_rpc_status") + register(b.observer.ObserveOutbound, "observe_outbound", optOutboundInterval, optOutboundSkipper) + + // CCTX Scheduler + register(b.scheduleCCTX, "schedule_cctx", scheduler.BlockTicker(newBlockChan), optOutboundSkipper) + + return nil +} + +func (b *Bitcoin) Stop() { + b.observer.Logger().Chain.Info().Msg("stopping observer-signer") + b.scheduler.StopGroup(b.group()) +} + +func (b *Bitcoin) group() scheduler.Group { + return scheduler.Group( + fmt.Sprintf("btc:%d", b.observer.Chain().ChainId), + ) +} + +// scheduleCCTX schedules pending cross-chain transactions on NEW zeta blocks +// 1. schedule at most one keysign per ticker +// 2. schedule keysign only when nonce-mark UTXO is available +// 3. stop keysign when lookahead is reached +func (b *Bitcoin) scheduleCCTX(ctx context.Context) error { + var ( + lookahead = b.observer.ChainParams().OutboundScheduleLookahead + chainID = b.observer.Chain().ChainId + ) + + if err := b.updateChainParams(ctx); err != nil { + return errors.Wrap(err, "unable to update chain params") + } + + zetaBlock, ok := scheduler.BlockFromContext(ctx) + if !ok { + return errors.New("unable to get zeta block from context") + } + + // #nosec G115 always in range + zetaHeight := uint64(zetaBlock.Block.Height) + + cctxList, _, err := b.observer.ZetacoreClient().ListPendingCCTX(ctx, chainID) + if err != nil { + return errors.Wrap(err, "unable to list pending cctx") + } + + // schedule at most one keysign per ticker + for idx, cctx := range cctxList { + var ( + params = cctx.GetCurrentOutboundParam() + nonce = params.TssNonce + outboundID = outboundprocessor.ToOutboundID(cctx.Index, params.ReceiverChainId, nonce) + ) + + if params.ReceiverChainId != chainID { + b.outboundLogger(outboundID).Error().Msg("Schedule CCTX: chain id mismatch") + + continue + } + + // try confirming the outbound + continueKeysign, err := b.observer.VoteOutboundIfConfirmed(ctx, cctx) + + switch { + case err != nil: + b.outboundLogger(outboundID).Error().Err(err).Msg("Schedule CCTX: VoteOutboundIfConfirmed failed") + continue + case !continueKeysign: + b.outboundLogger(outboundID).Info().Msg("Schedule CCTX: outbound already processed") + continue + case nonce > b.observer.GetPendingNonce(): + // stop if the nonce being processed is higher than the pending nonce + return nil + case int64(idx) >= lookahead: + // stop if lookahead is reached 2 bitcoin confirmations span is 20 minutes on average. + // We look ahead up to 100 pending cctx to target TPM of 5. + b.outboundLogger(outboundID).Warn(). + Uint64("outbound.earliest_pending_nonce", cctxList[0].GetCurrentOutboundParam().TssNonce). + Msg("Schedule CCTX: lookahead reached") + return nil + case b.proc.IsOutboundActive(outboundID): + // outbound is already being processed + continue + } + + b.proc.StartTryProcess(outboundID) + + go b.signer.TryProcessOutbound( + ctx, + cctx, + b.proc, + outboundID, + b.observer, + b.observer.ZetacoreClient(), + zetaHeight, + ) + } + + return nil +} + +func (b *Bitcoin) updateChainParams(ctx context.Context) error { + // no changes for signer + + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + chain, err := app.GetChain(b.observer.Chain().ChainId) + if err != nil { + return err + } + + b.observer.SetChainParams(*chain.Params()) + + return nil +} + +func (b *Bitcoin) outboundLogger(id string) *zerolog.Logger { + l := b.observer.Logger().Outbound.With().Str("outbound.id", id).Logger() + + return &l +} diff --git a/zetaclient/chains/bitcoin/fee.go b/zetaclient/chains/bitcoin/common/fee.go similarity index 99% rename from zetaclient/chains/bitcoin/fee.go rename to zetaclient/chains/bitcoin/common/fee.go index 39605f91d6..b84886f346 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 2efe3b45c2..e73e4150b9 100644 --- a/zetaclient/chains/bitcoin/fee_test.go +++ b/zetaclient/chains/bitcoin/common/fee_test.go @@ -1,4 +1,4 @@ -package bitcoin +package common import ( "math/rand" @@ -6,14 +6,13 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" + btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/require" - - btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/zeta-chain/node/pkg/chains" ) diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/common/tx_script.go similarity index 99% rename from zetaclient/chains/bitcoin/tx_script.go rename to zetaclient/chains/bitcoin/common/tx_script.go index e15268a868..5b3fe9d11b 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/common/tx_script.go @@ -1,4 +1,4 @@ -package bitcoin +package common // #nosec G507 ripemd160 required for bitcoin address encoding import ( diff --git a/zetaclient/chains/bitcoin/tx_script_test.go b/zetaclient/chains/bitcoin/common/tx_script_test.go similarity index 88% rename from zetaclient/chains/bitcoin/tx_script_test.go rename to zetaclient/chains/bitcoin/common/tx_script_test.go index 0d4b96bd63..b47e2249e0 100644 --- a/zetaclient/chains/bitcoin/tx_script_test.go +++ b/zetaclient/chains/bitcoin/common/tx_script_test.go @@ -1,4 +1,4 @@ -package bitcoin_test +package common_test import ( "bytes" @@ -10,15 +10,14 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/testutil" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/testutils" ) // the relative path to the testdata directory -var TestDataDir = "../../" +var TestDataDir = "../../../" func TestDecodeVoutP2TR(t *testing.T) { // load archived tx raw result @@ -31,7 +30,7 @@ func TestDecodeVoutP2TR(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2TR - receiver, err := bitcoin.DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", receiver) } @@ -47,14 +46,14 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) @@ -62,7 +61,7 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_1 '51' to OP_2 '52' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "51", "52", 1) - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) @@ -70,7 +69,7 @@ func TestDecodeVoutP2TRErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '20' to '19' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "5120", "5119", 1) - _, err := bitcoin.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2TR script") }) } @@ -86,7 +85,7 @@ func TestDecodeVoutP2WSH(t *testing.T) { require.Len(t, rawResult.Vout, 1) // decode vout 0, P2WSH - receiver, err := bitcoin.DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", receiver) } @@ -102,14 +101,14 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0020" // 2 bytes, should be 34 - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) @@ -117,7 +116,7 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_0 '00' to OP_1 '51' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "00", "51", 1) - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) @@ -125,7 +124,7 @@ func TestDecodeVoutP2WSHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '20' to '19' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0020", "0019", 1) - _, err := bitcoin.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WSH script") }) } @@ -143,17 +142,17 @@ func TestDecodeP2WPKHVout(t *testing.T) { require.Len(t, rawResult.Vout, 3) // decode vout 0, nonce mark 148 - receiver, err := bitcoin.DecodeScriptP2WPKH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2WPKH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) // decode vout 1, payment 0.00012000 BTC - receiver, err = bitcoin.DecodeScriptP2WPKH(rawResult.Vout[1].ScriptPubKey.Hex, net) + receiver, err = common.DecodeScriptP2WPKH(rawResult.Vout[1].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "bc1qpsdlklfcmlcfgm77c43x65ddtrt7n0z57hsyjp", receiver) // decode vout 2, change 0.39041489 BTC - receiver, err = bitcoin.DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) + receiver, err = common.DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) } @@ -172,14 +171,14 @@ func TestDecodeP2WPKHVoutErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 22 - _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WPKH script") }) @@ -187,7 +186,7 @@ func TestDecodeP2WPKHVoutErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "0014", "0013", 1) - _, err := bitcoin.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2WPKH script") }) } @@ -203,7 +202,7 @@ func TestDecodeVoutP2SH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2SH - receiver, err := bitcoin.DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", receiver) } @@ -219,21 +218,21 @@ func TestDecodeVoutP2SHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "0014" // 2 bytes, should be 23 - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_HASH160 'a9' to OP_HASH256 'aa' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a9", "aa", 1) - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) @@ -241,14 +240,14 @@ func TestDecodeVoutP2SHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "a914", "a913", 1) - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) t.Run("should return error on invalid OP_EQUAL", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "87", "88", 1) - _, err := bitcoin.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2SH script") }) } @@ -264,7 +263,7 @@ func TestDecodeVoutP2PKH(t *testing.T) { require.Len(t, rawResult.Vout, 2) // decode vout 0, P2PKH - receiver, err := bitcoin.DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) + receiver, err := common.DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) require.NoError(t, err) require.Equal(t, "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", receiver) } @@ -280,14 +279,14 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { t.Run("should return error on invalid script", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "error decoding script") }) t.Run("should return error on wrong script length", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "76a914" // 3 bytes, should be 25 - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -295,14 +294,14 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_DUP '76' to OP_NIP '77' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76", "77", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) t.Run("should return error on invalid OP_HASH160", func(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_HASH160 'a9' to OP_HASH256 'aa' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a9", "76aa", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -310,7 +309,7 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the length '14' to '13' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "76a914", "76a913", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -318,7 +317,7 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_EQUALVERIFY '88' to OP_RESERVED1 '89' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "89ac", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) @@ -326,7 +325,7 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // modify the OP_CHECKSIG 'ac' to OP_CHECKSIGVERIFY 'ad' invalidVout.ScriptPubKey.Hex = strings.Replace(invalidVout.ScriptPubKey.Hex, "88ac", "88ad", 1) - _, err := bitcoin.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + _, err := common.DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) require.ErrorContains(t, err, "invalid P2PKH script") }) } @@ -370,7 +369,7 @@ func TestDecodeOpReturnMemo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - memo, found, err := bitcoin.DecodeOpReturnMemo(tt.scriptHex) + memo, found, err := common.DecodeOpReturnMemo(tt.scriptHex) require.NoError(t, err) require.Equal(t, tt.found, found) require.True(t, bytes.Equal(tt.expected, memo)) @@ -415,7 +414,7 @@ func TestDecodeOpReturnMemoErrors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - memo, found, err := bitcoin.DecodeOpReturnMemo(tt.scriptHex) + memo, found, err := common.DecodeOpReturnMemo(tt.scriptHex) require.ErrorContains(t, err, tt.errMsg) require.False(t, found) require.Nil(t, memo) @@ -493,7 +492,7 @@ func TestDecodeSenderFromScript(t *testing.T) { } // Decode the sender address from the script - sender, err := bitcoin.DecodeSenderFromScript(pkScript, net) + sender, err := common.DecodeSenderFromScript(pkScript, net) // Validate the results require.NoError(t, err) @@ -511,7 +510,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2TR", txHash) receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(45000), amount) @@ -523,7 +522,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2WSH", txHash) receiverExpected := "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(36557203), amount) @@ -535,7 +534,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2WPKH", txHash) receiverExpected := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(79938), amount) @@ -547,7 +546,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2SH", txHash) receiverExpected := "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(1003881), amount) @@ -559,7 +558,7 @@ func TestDecodeTSSVout(t *testing.T) { rawResult := testutils.LoadBTCTxRawResult(t, TestDataDir, chain.ChainId, "P2PKH", txHash) receiverExpected := "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte" - receiver, amount, err := bitcoin.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) require.NoError(t, err) require.Equal(t, receiverExpected, receiver) require.Equal(t, int64(1140000), amount) @@ -578,7 +577,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { t.Run("should return error on invalid amount", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.Value = -0.05 // use negative amount - receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(invalidVout, receiverExpected, chain) require.ErrorContains(t, err, "error getting satoshis") require.Empty(t, receiver) require.Zero(t, amount) @@ -588,7 +587,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // use invalid chain invalidChain := chains.Chain{ChainId: 123} - receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, invalidChain) + receiver, amount, err := common.DecodeTSSVout(invalidVout, receiverExpected, invalidChain) require.ErrorContains(t, err, "error GetBTCChainParams") require.Empty(t, receiver) require.Zero(t, amount) @@ -598,7 +597,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { invalidVout := rawResult.Vout[0] // use testnet params to decode mainnet receiver wrongChain := chains.BitcoinTestnet - receiver, amount, err := bitcoin.DecodeTSSVout( + receiver, amount, err := common.DecodeTSSVout( invalidVout, "bc1qulmx8ej27cj0xe20953cztr2excnmsqvuh0s5c", wrongChain, @@ -611,7 +610,7 @@ func TestDecodeTSSVoutErrors(t *testing.T) { t.Run("should return error on decoding failure", func(t *testing.T) { invalidVout := rawResult.Vout[0] invalidVout.ScriptPubKey.Hex = "invalid script" - receiver, amount, err := bitcoin.DecodeTSSVout(invalidVout, receiverExpected, chain) + receiver, amount, err := common.DecodeTSSVout(invalidVout, receiverExpected, chain) require.ErrorContains(t, err, "error decoding TSS vout") require.Empty(t, receiver) require.Zero(t, amount) @@ -624,7 +623,7 @@ func TestDecodeScript(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ac00634d0802c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f4c50646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe68" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.Nil(t, err) require.True(t, isFound) @@ -638,7 +637,7 @@ func TestDecodeScript(t *testing.T) { data := "20d6f59371037bf30115d9fd6016f0e3ef552cdfc0367ee20aa9df3158f74aaeb4ac00634c51bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c397160881668" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.Nil(t, err) require.True(t, isFound) @@ -652,7 +651,7 @@ func TestDecodeScript(t *testing.T) { data := "20cabd6ecc0245c40f27ca6299dcd3732287c317f3946734f04e27568fc5334218ac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "should contain more data, but script ended") require.False(t, isFound) require.Nil(t, memo) @@ -663,7 +662,7 @@ func TestDecodeScript(t *testing.T) { data := "1f01a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "public key not found") require.False(t, isFound) require.Nil(t, memo) @@ -674,7 +673,7 @@ func TestDecodeScript(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ab" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "OP_CHECKSIG not found") require.False(t, isFound) require.Nil(t, memo) @@ -683,7 +682,7 @@ func TestDecodeScript(t *testing.T) { t.Run("parsing opcode OP_DATA_32 failed", func(t *testing.T) { data := "01" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "public key not found") require.False(t, isFound) @@ -693,7 +692,7 @@ func TestDecodeScript(t *testing.T) { t.Run("parsing opcode OP_CHECKSIG failed", func(t *testing.T) { data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c701" script := testutil.HexToBytes(t, data) - memo, isFound, err := bitcoin.DecodeScript(script) + memo, isFound, err := common.DecodeScript(script) require.ErrorContains(t, err, "OP_CHECKSIG not found") require.False(t, isFound) diff --git a/zetaclient/chains/bitcoin/utils.go b/zetaclient/chains/bitcoin/common/utils.go similarity index 77% rename from zetaclient/chains/bitcoin/utils.go rename to zetaclient/chains/bitcoin/common/utils.go index d14fb6315a..e6aaccce3a 100644 --- a/zetaclient/chains/bitcoin/utils.go +++ b/zetaclient/chains/bitcoin/common/utils.go @@ -1,7 +1,6 @@ -package bitcoin +package common import ( - "encoding/json" "math" "github.com/btcsuite/btcd/btcutil" @@ -10,19 +9,6 @@ import ( // TODO(revamp): Remove utils.go and move the functions to the appropriate files -// PrettyPrintStruct returns a pretty-printed string representation of a struct -func PrettyPrintStruct(val interface{}) (string, error) { - prettyStruct, err := json.MarshalIndent( - val, - "", - " ", - ) - if err != nil { - return "", err - } - return string(prettyStruct), nil -} - // GetSatoshis converts a bitcoin amount to satoshis func GetSatoshis(btc float64) (int64, error) { // The amount is only considered invalid if it cannot be represented diff --git a/zetaclient/chains/bitcoin/errors.go b/zetaclient/chains/bitcoin/errors.go deleted file mode 100644 index d04d67687d..0000000000 --- a/zetaclient/chains/bitcoin/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package bitcoin - -import "errors" - -// ErrBitcoinNotEnabled is the error returned when bitcoin is not enabled -var ErrBitcoinNotEnabled = errors.New("bitcoin is not enabled") diff --git a/zetaclient/chains/bitcoin/observer/gas_price.go b/zetaclient/chains/bitcoin/observer/gas_price.go index 8a246c3754..735d06f412 100644 --- a/zetaclient/chains/bitcoin/observer/gas_price.go +++ b/zetaclient/chains/bitcoin/observer/gas_price.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" "github.com/zeta-chain/node/pkg/chains" - "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" clienttypes "github.com/zeta-chain/node/zetaclient/types" ) @@ -95,7 +95,7 @@ func (ob *Observer) specialHandleFeeRate() (int64, error) { case chains.NetworkType_privnet: return rpc.FeeRateRegnet, 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/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 27f0839856..ecaf9f1e7a 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -14,61 +14,12 @@ import ( "github.com/zeta-chain/node/pkg/coin" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/interfaces" - zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/logs" - "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) -// WatchInbound watches Bitcoin chain for inbounds on a ticker -// It starts a ticker and run ObserveInbound -// TODO(revamp): move all ticker related methods in the same file -func (ob *Observer) WatchInbound(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - - ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.ChainParams().InboundTicker) - if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") - return err - } - defer ticker.Stop() - - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) - sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) - - // ticker loop - for { - select { - case <-ticker.C(): - if !app.IsInboundObservationEnabled() { - sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) - continue - } - err := ob.ObserveInbound(ctx) - if err != nil { - // skip showing log for block number 0 as it means Bitcoin node is not enabled - // TODO: prevent this routine from running if Bitcoin node is not enabled - // https://github.com/zeta-chain/node/issues/2790 - if !errors.Is(err, bitcoin.ErrBitcoinNotEnabled) { - ob.logger.Inbound.Error().Err(err).Msg("WatchInbound error observing in tx") - } else { - ob.logger.Inbound.Debug().Err(err).Msg("WatchInbound: Bitcoin node is not enabled") - } - } - ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.StopChannel(): - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) - return nil - } - } -} - // ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore // TODO(revamp): simplify this function into smaller functions func (ob *Observer) ObserveInbound(ctx context.Context) error { @@ -83,9 +34,13 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // 0 will be returned if the node is not synced if currentBlock == 0 { - return errors.Wrap(bitcoin.ErrBitcoinNotEnabled, "observeInboundBTC: current block number 0 is too low") + ob.nodeEnabled.Store(false) + ob.logger.Inbound.Debug().Err(err).Msg("WatchInbound: Bitcoin node is not enabled") + return nil } + ob.nodeEnabled.Store(true) + // #nosec G115 checked positive lastBlock := uint64(currentBlock) if lastBlock < ob.LastBlock() { @@ -156,44 +111,9 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { return nil } -// WatchInboundTracker watches zetacore for bitcoin inbound trackers -// TODO(revamp): move all ticker related methods in the same file -func (ob *Observer) WatchInboundTracker(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - - ticker, err := types.NewDynamicTicker("Bitcoin_WatchInboundTracker", ob.ChainParams().InboundTicker) - if err != nil { - ob.logger.Inbound.Err(err).Msg("error creating ticker") - return err - } - - defer ticker.Stop() - for { - select { - case <-ticker.C(): - if !app.IsInboundObservationEnabled() { - continue - } - err := ob.ProcessInboundTrackers(ctx) - if err != nil { - ob.logger.Inbound.Error(). - Err(err). - Msgf("error observing inbound tracker for chain %d", ob.Chain().ChainId) - } - ticker.UpdateInterval(ob.ChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.StopChannel(): - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) - return nil - } - } -} - -// ProcessInboundTrackers processes inbound trackers +// ObserveInboundTrackers processes inbound trackers // TODO(revamp): move inbound tracker logic in a specific file -func (ob *Observer) ProcessInboundTrackers(ctx context.Context) error { +func (ob *Observer) ObserveInboundTrackers(ctx context.Context) error { trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ctx, ob.Chain().ChainId) if err != nil { return err @@ -263,7 +183,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, uint64(blockVb.Height), ob.logger.Inbound, ob.netParams, - bitcoin.CalcDepositorFee, + common.CalcDepositorFee, ) if err != nil { return "", err @@ -303,7 +223,7 @@ func FilterAndParseIncomingTx( continue // the first tx is coinbase; we do not process coinbase tx } - event, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, bitcoin.CalcDepositorFee) + event, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, common.CalcDepositorFee) if err != nil { // unable to parse the tx, the caller should retry return nil, errors.Wrapf(err, "error getting btc event for tx %s in block %d", tx.Txid, blockNumber) @@ -343,7 +263,7 @@ func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *crosscha } // convert the amount to integer (satoshis) - amountSats, err := bitcoin.GetSatoshis(event.Value) + amountSats, err := common.GetSatoshis(event.Value) if err != nil { ob.Logger().Inbound.Error().Err(err).Fields(lf).Msgf("can't convert value %f to satoshis", event.Value) return nil @@ -368,7 +288,7 @@ func GetBtcEvent( blockNumber uint64, logger zerolog.Logger, netParams *chaincfg.Params, - feeCalculator bitcoin.DepositorFeeCalculator, + feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { if netParams.Name == chaincfg.MainNetParams.Name { return GetBtcEventWithoutWitness(rpcClient, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) @@ -386,7 +306,7 @@ func GetBtcEventWithoutWitness( blockNumber uint64, logger zerolog.Logger, netParams *chaincfg.Params, - feeCalculator bitcoin.DepositorFeeCalculator, + feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { var ( found bool @@ -401,7 +321,7 @@ func GetBtcEventWithoutWitness( script := vout0.ScriptPubKey.Hex if len(script) == 44 && script[:4] == "0014" { // P2WPKH output: 0x00 + 20 bytes of pubkey hash - receiver, err := bitcoin.DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) + receiver, err := common.DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) if err != nil { // should never happen return nil, err } @@ -427,7 +347,7 @@ func GetBtcEventWithoutWitness( // 2nd vout must be a valid OP_RETURN memo vout1 := tx.Vout[1] - memo, found, err = bitcoin.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex) + memo, found, err = common.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex) if err != nil { logger.Error().Err(err).Msgf("GetBtcEvent: error decoding OP_RETURN memo: %s", vout1.ScriptPubKey.Hex) return nil, nil @@ -487,5 +407,5 @@ func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, n // decode sender address from previous pkScript pkScript := tx.MsgTx().TxOut[vin.Vout].PkScript - return bitcoin.DecodeSenderFromScript(pkScript, net) + return common.DecodeSenderFromScript(pkScript, net) } diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index 9e0d629f98..614a48a403 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/mempool.go b/zetaclient/chains/bitcoin/observer/mempool.go index d483b20dbc..1c865b0565 100644 --- a/zetaclient/chains/bitcoin/observer/mempool.go +++ b/zetaclient/chains/bitcoin/observer/mempool.go @@ -8,10 +8,8 @@ import ( "github.com/pkg/errors" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/ticker" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/node/zetaclient/chains/interfaces" - "github.com/zeta-chain/node/zetaclient/common" "github.com/zeta-chain/node/zetaclient/logs" ) @@ -54,20 +52,11 @@ type StuckTxChecker func(client interfaces.BTCRPCClient, txHash string, maxWaitB // WatchMempoolTxs monitors pending outbound txs in the Bitcoin mempool. func (ob *Observer) WatchMempoolTxs(ctx context.Context) error { txChecker := GetStuckTxChecker(ob.Chain().ChainId) - task := func(ctx context.Context, _ *ticker.Ticker) error { - if err := ob.RefreshLastStuckOutbound(ctx, GetLastPendingOutbound, txChecker); err != nil { - ob.Logger().Chain.Err(err).Msg("RefreshLastStuckOutbound error") - } - return nil - } - return ticker.Run( - ctx, - common.MempoolStuckTxCheckInterval, - task, - ticker.WithStopChan(ob.StopChannel()), - ticker.WithLogger(ob.Logger().Chain, "WatchMempoolTxs"), - ) + if err := ob.RefreshLastStuckOutbound(ctx, GetLastPendingOutbound, txChecker); err != nil { + ob.Logger().Chain.Err(err).Msg("RefreshLastStuckOutbound failed") + } + return nil } // RefreshLastStuckOutbound refreshes the information about the last stuck tx in the Bitcoin mempool. @@ -84,7 +73,7 @@ func (ob *Observer) RefreshLastStuckOutbound( // step 1: get last TSS transaction lastTx, lastNonce, err := txFinder(ctx, ob) if err != nil { - ob.logger.Outbound.Info().Msgf("last pending outbound not found: %s", err.Error()) + ob.logger.Outbound.Info().Fields(lf).Msgf("last pending outbound not found: %s", err.Error()) return nil } diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index e0c374c842..ba7bd26981 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -2,8 +2,8 @@ package observer import ( - "context" "math/big" + "sync/atomic" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" @@ -11,7 +11,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" @@ -35,8 +34,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 @@ -82,6 +79,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 } @@ -133,55 +134,21 @@ func NewObserver( }, } + ob.nodeEnabled.Store(true) + // load last scanned block - if err := ob.LoadLastBlockScanned(); err != nil { + if err = ob.LoadLastBlockScanned(); err != nil { return nil, errors.Wrap(err, "unable to load last scanned block") } // load broadcasted transactions - if err := ob.LoadBroadcastedTxMap(); err != nil { + if err = ob.LoadBroadcastedTxMap(); err != nil { return nil, errors.Wrap(err, "unable to load broadcasted tx map") } return ob, nil } -// BtcClient returns the btc client -func (ob *Observer) BtcClient() interfaces.BTCRPCClient { - return ob.btcClient -} - -// Start starts the Go routine processes to observe the Bitcoin chain -func (ob *Observer) Start(ctx context.Context) { - if ok := ob.Observer.Start(); !ok { - ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) - return - } - - ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) - - // watch bitcoin chain for incoming txs and post votes to zetacore - bg.Work(ctx, ob.WatchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) - - // watch bitcoin chain for outgoing txs status - bg.Work(ctx, ob.WatchOutbound, bg.WithName("WatchOutbound"), bg.WithLogger(ob.Logger().Outbound)) - - // watch bitcoin chain for UTXOs owned by the TSS address - bg.Work(ctx, ob.WatchUTXOs, bg.WithName("WatchUTXOs"), bg.WithLogger(ob.Logger().Outbound)) - - // watch bitcoin chain for pending mempool txs - bg.Work(ctx, ob.WatchMempoolTxs, bg.WithName("WatchMempoolTxs"), 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)) -} - // GetPendingNonce returns the artificial pending nonce // Note: pending nonce is accessed concurrently func (ob *Observer) GetPendingNonce() uint64 { @@ -291,3 +258,7 @@ func (ob *Observer) GetBroadcastedTx(nonce uint64) (string, bool) { txHash, found := ob.broadcastedTx[outboundID] return txHash, found } + +func (ob *Observer) isNodeEnabled() bool { + return ob.nodeEnabled.Load() +} diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index d3b79f103d..401a45eb24 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -9,19 +9,16 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/pkg/errors" - "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/compliance" - zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/logs" - "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) @@ -31,54 +28,11 @@ const ( minTxConfirmations = 0 ) -// 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 { - // get app context - app, err := zctx.FromContext(ctx) - if err != nil { - return errors.Wrap(err, "unable to get app from context") - } - - // create outbound ticker - ticker, err := types.NewDynamicTicker("Bitcoin_WatchOutbound", ob.ChainParams().OutboundTicker) - if err != nil { - return errors.Wrap(err, "unable to create dynamic ticker") - } - defer ticker.Stop() - - ob.logger.Outbound.Info().Msg("WatchOutbound: started") - sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) - - for { - select { - case <-ticker.C(): - if !app.IsOutboundObservationEnabled() { - sampledLogger.Info().Msg("WatchOutbound: outbound observation is disabled") - continue - } - - // process outbound trackers - err := ob.ProcessOutboundTrackers(ctx) - if err != nil { - ob.Logger().Outbound.Error().Err(err).Msg("WatchOutbound: ProcessOutboundTrackers failed") - } - - ticker.UpdateInterval(ob.ChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.StopChannel(): - ob.logger.Outbound.Info().Msg("WatchOutbound: stopped") - return nil - } - } -} - -// ProcessOutboundTrackers processes outbound trackers -func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { +func (ob *Observer) ObserveOutbound(ctx context.Context) error { chainID := ob.Chain().ChainId trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) if err != nil { - return errors.Wrap(err, "GetAllOutboundTrackerByChain failed") + return errors.Wrap(err, "unable to get all outbound trackers") } // logger fields @@ -86,7 +40,6 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { logs.FieldMethod: "ProcessOutboundTrackers", } - // process outbound trackers for _, tracker := range trackers { // set logger fields lf[logs.FieldNonce] = tracker.Nonce @@ -519,7 +472,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 } @@ -573,7 +526,7 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams tssAddress := ob.TSSAddressString() for _, vout := range vouts { // decode receiver and amount from vout - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain()) + receiverVout, amount, err := common.DecodeTSSVout(vout, tssAddress, ob.Chain()) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index 4cd935c88c..f09e3ec32d 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -2,44 +2,30 @@ package observer import ( "context" - "time" + + "github.com/pkg/errors" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" - "github.com/zeta-chain/node/zetaclient/common" ) -// watchRPCStatus watches the RPC status of the Bitcoin chain -func (ob *Observer) watchRPCStatus(_ context.Context) error { - ob.Logger().Chain.Info().Msgf("WatchRPCStatus started for chain %d", ob.Chain().ChainId) - - ticker := time.NewTicker(common.RPCStatusCheckInterval) - for { - select { - case <-ticker.C: - if !ob.ChainParams().IsSupported { - continue - } - - ob.checkRPCStatus() - case <-ob.StopChannel(): - return nil - } - } -} - -// checkRPCStatus checks the RPC status of the Bitcoin chain -func (ob *Observer) checkRPCStatus() { +// CheckRPCStatus checks the RPC status of the Bitcoin chain +func (ob *Observer) CheckRPCStatus(_ context.Context) error { tssAddress, err := ob.TSS().PubKey().AddressBTC(ob.Chain().ChainId) if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("unable to get TSS BTC address") - return + return errors.Wrap(err, "unable to get TSS BTC address") } blockTime, err := rpc.CheckRPCStatus(ob.btcClient, tssAddress) - if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("CheckRPCStatus failed") - return + switch { + case err != nil && !ob.isNodeEnabled(): + // suppress error if node is disabled + ob.logger.Chain.Debug().Err(err).Msg("CheckRPC status failed") + return nil + case err != nil: + return errors.Wrap(err, "unable to check RPC status") } ob.ReportBlockLatency(blockTime) + + return nil } diff --git a/zetaclient/chains/bitcoin/observer/utxos.go b/zetaclient/chains/bitcoin/observer/utxos.go index af0e2a16f8..b89ce14c5d 100644 --- a/zetaclient/chains/bitcoin/observer/utxos.go +++ b/zetaclient/chains/bitcoin/observer/utxos.go @@ -10,7 +10,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" clienttypes "github.com/zeta-chain/node/zetaclient/types" ) @@ -85,7 +85,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 @@ -206,7 +206,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) } diff --git a/zetaclient/chains/bitcoin/observer/witness.go b/zetaclient/chains/bitcoin/observer/witness.go index bb85bdc47b..69d2726459 100644 --- a/zetaclient/chains/bitcoin/observer/witness.go +++ b/zetaclient/chains/bitcoin/observer/witness.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/interfaces" ) @@ -24,7 +24,7 @@ func GetBtcEventWithWitness( blockNumber uint64, logger zerolog.Logger, netParams *chaincfg.Params, - feeCalculator bitcoin.DepositorFeeCalculator, + feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { if len(tx.Vout) < 1 { logger.Debug().Msgf("no output %s", tx.Txid) @@ -137,7 +137,7 @@ func tryExtractOpRet(tx btcjson.TxRawResult, logger zerolog.Logger) []byte { return nil } - memo, found, err := bitcoin.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex) + memo, found, err := common.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex) if err != nil { logger.Error().Err(err).Msgf("tryExtractOpRet: error decoding OP_RETURN memo: %s", tx.Vout[1].ScriptPubKey.Hex) return nil @@ -159,7 +159,7 @@ func tryExtractInscription(tx btcjson.TxRawResult, logger zerolog.Logger) []byte logger.Debug().Msgf("potential witness script, tx %s, input idx %d", tx.Txid, i) - memo, found, err := bitcoin.DecodeScript(script) + memo, found, err := common.DecodeScript(script) if err != nil || !found { logger.Debug().Msgf("invalid witness script, tx %s, input idx %d", tx.Txid, i) continue @@ -187,7 +187,7 @@ func isValidRecipient( tssAddress string, netParams *chaincfg.Params, ) error { - receiver, err := bitcoin.DecodeScriptP2WPKH(script, netParams) + receiver, err := common.DecodeScriptP2WPKH(script, netParams) if err != nil { return fmt.Errorf("invalid p2wpkh script detected, %s", err) } diff --git a/zetaclient/chains/bitcoin/observer/witness_test.go b/zetaclient/chains/bitcoin/observer/witness_test.go index 745f2003a9..34b676c7ac 100644 --- a/zetaclient/chains/bitcoin/observer/witness_test.go +++ b/zetaclient/chains/bitcoin/observer/witness_test.go @@ -10,9 +10,9 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" clientcommon "github.com/zeta-chain/node/zetaclient/common" "github.com/zeta-chain/node/zetaclient/testutils" @@ -60,7 +60,7 @@ func TestGetBtcEventWithWitness(t *testing.T) { net := &chaincfg.MainNetParams // fee rate of above tx is 28 sat/vB - depositorFee := bitcoin.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) + depositorFee := common.DepositorFee(28 * clientcommon.BTCOutboundGasPriceMultiplier) feeCalculator := mockDepositFeeCalculator(depositorFee, nil) t.Run("decode OP_RETURN ok", func(t *testing.T) { diff --git a/zetaclient/chains/bitcoin/rpc/rpc.go b/zetaclient/chains/bitcoin/rpc/rpc.go index 9f54e4dca7..2f248052da 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc.go +++ b/zetaclient/chains/bitcoin/rpc/rpc.go @@ -365,30 +365,29 @@ func CheckRPCStatus(client interfaces.BTCRPCClient, tssAddress btcutil.Address) // query latest block number bn, err := client.GetBlockCount() if err != nil { - return time.Time{}, errors.Wrap(err, "RPC failed on GetBlockCount, RPC down?") + return time.Time{}, errors.Wrap(err, "unable to get block count") } // query latest block header hash, err := client.GetBlockHash(bn) if err != nil { - return time.Time{}, errors.Wrap(err, "RPC failed on GetBlockHash, RPC down?") + return time.Time{}, errors.Wrapf(err, "unable to get hash for block %d", bn) } // query latest block header thru hash header, err := client.GetBlockHeader(hash) if err != nil { - return time.Time{}, errors.Wrap(err, "RPC failed on GetBlockHeader, RPC down?") + return time.Time{}, errors.Wrapf(err, "unable to get block header (%s)", hash.String()) } // should be able to list utxos owned by TSS address res, err := client.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddress}) - if err != nil { - return time.Time{}, errors.Wrap(err, "can't list utxos of TSS address; TSS address is not imported?") - } - // TSS address should have utxos - if len(res) == 0 { - return time.Time{}, errors.New("TSS address has no utxos; TSS address is not imported?") + switch { + case err != nil: + return time.Time{}, errors.Wrap(err, "unable to list TSS UTXOs") + case len(res) == 0: + return time.Time{}, errors.New("no UTXOs found for TSS") } return header.Timestamp, nil diff --git a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go index 0dff97f52d..c26912d508 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" @@ -343,7 +343,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 @@ -395,7 +395,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)) } @@ -576,19 +576,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/rpc/rpc_rbf_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_rbf_live_test.go index 34fccd2569..41f2bdc400 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc_rbf_live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_rbf_live_test.go @@ -20,7 +20,7 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + btccommon "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/node/zetaclient/common" ) @@ -363,15 +363,15 @@ func buildRBFTx( tx.AddTxIn(txIn) total += output.Amount } - totalSats, err := bitcoin.GetSatoshis(total) + totalSats, err := btccommon.GetSatoshis(total) require.NoError(t, err) // amount to send in satoshis - amountSats, err := bitcoin.GetSatoshis(amount) + amountSats, err := btccommon.GetSatoshis(amount) require.NoError(t, err) // calculate tx fee - txSize, err := bitcoin.EstimateOutboundSize(int64(len(utxos)), []btcutil.Address{to}) + txSize, err := btccommon.EstimateOutboundSize(int64(len(utxos)), []btcutil.Address{to}) require.NoError(t, err) fees := int64(txSize) * feeRate diff --git a/zetaclient/chains/bitcoin/signer/fee_bumper.go b/zetaclient/chains/bitcoin/signer/fee_bumper.go index 403a0210a8..d9a182243f 100644 --- a/zetaclient/chains/bitcoin/signer/fee_bumper.go +++ b/zetaclient/chains/bitcoin/signer/fee_bumper.go @@ -14,7 +14,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" mathpkg "github.com/zeta-chain/node/pkg/math" - "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" ) @@ -180,7 +180,7 @@ func (b *CPFPFeeBumper) FetchFeeBumpInfo(memplTxsInfoFetcher MempoolTxsInfoFetch if err != nil { return errors.Wrap(err, "unable to fetch mempool txs info") } - totalFeesSats, err := bitcoin.GetSatoshis(totalFees) + totalFeesSats, err := common.GetSatoshis(totalFees) if err != nil { return errors.Wrapf(err, "cannot convert total fees %f", totalFees) } diff --git a/zetaclient/chains/bitcoin/signer/sign.go b/zetaclient/chains/bitcoin/signer/sign.go index ebf7ba57fa..8327ef1e0d 100644 --- a/zetaclient/chains/bitcoin/signer/sign.go +++ b/zetaclient/chains/bitcoin/signer/sign.go @@ -15,7 +15,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" - "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" ) @@ -39,7 +39,7 @@ func (signer *Signer) SignWithdrawTx( ob *observer.Observer, ) (*wire.MsgTx, error) { nonceMark := chains.NonceMarkAmount(txData.nonce) - estimateFee := float64(txData.feeRate*bitcoin.OutboundBytesMax) / 1e8 + estimateFee := float64(txData.feeRate*common.OutboundBytesMax) / 1e8 totalAmount := txData.amount + estimateFee + reservedRBFFees + float64(nonceMark)*1e-8 // refreshing UTXO list before TSS keysign is important: @@ -76,23 +76,23 @@ func (signer *Signer) SignWithdrawTx( // size checking // #nosec G115 always positive - txSize, err := bitcoin.EstimateOutboundSize(int64(len(prevOuts)), []btcutil.Address{txData.to}) + txSize, err := common.EstimateOutboundSize(int64(len(prevOuts)), []btcutil.Address{txData.to}) if err != nil { return nil, err } - if txData.txSize < bitcoin.BtcOutboundBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user + if txData.txSize < common.BtcOutboundBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user signer.Logger().Std.Info(). Msgf("txSize %d is less than BtcOutboundBytesWithdrawer %d for nonce %d", txData.txSize, txSize, txData.nonce) } - if txSize < bitcoin.OutboundBytesMin { // outbound shouldn't be blocked by low sizeLimit + if txSize < common.OutboundBytesMin { // outbound shouldn't be blocked by 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 @@ -137,7 +137,7 @@ func (signer *Signer) AddTxInputs(tx *wire.MsgTx, utxos []btcjson.ListUnspentRes tx.AddTxIn(txIn) // store the amount for later signing use - amount, err := bitcoin.GetSatoshis(utxos[i].Amount) + amount, err := common.GetSatoshis(utxos[i].Amount) if err != nil { return nil, err } @@ -165,7 +165,7 @@ func (signer *Signer) AddWithdrawTxOutputs( // 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 } diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 88ba3ea9ab..25099f28fa 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -9,7 +9,7 @@ import ( "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" "github.com/zeta-chain/node/x/crosschain/types" @@ -29,8 +29,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 @@ -55,35 +53,24 @@ func NewSigner( } } -// 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{} -} +// Broadcast sends the signed transaction to the network +func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error { + var outBuff bytes.Buffer + if err := signedTx.Serialize(&outBuff); err != nil { + return errors.Wrap(err, "unable to serialize tx") + } -// GetERC20CustodyAddress returns dummy address -func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { - return ethcommon.Address{} -} + signer.Logger().Std.Info(). + Str(logs.FieldTx, signedTx.TxHash().String()). + Str("signer.tx_payload", hex.EncodeToString(outBuff.Bytes())). + Msg("Broadcasting transaction") -// SetGatewayAddress does nothing for BTC -// Note: TSS address will be used as gateway address for Bitcoin -func (signer *Signer) SetGatewayAddress(_ string) { -} + _, err := signer.client.SendRawTransaction(signedTx, true) + if err != nil { + return errors.Wrap(err, "unable to broadcast raw tx") + } -// GetGatewayAddress returns empty address -// Note: same as SetGatewayAddress -func (signer *Signer) GetGatewayAddress() string { - return "" + return nil } // PkScriptTSS returns the TSS pkScript @@ -95,31 +82,13 @@ func (signer *Signer) PkScriptTSS() ([]byte, error) { return txscript.PayToAddrScript(tssAddrP2WPKH) } -// Broadcast sends the signed transaction to the network -func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error { - var outBuff bytes.Buffer - err := signedTx.Serialize(&outBuff) - if err != nil { - return err - } - str := hex.EncodeToString(outBuff.Bytes()) - - _, err = signer.client.SendRawTransaction(signedTx, true) - if err != nil { - return err - } - - signer.Logger().Std.Info().Msgf("Broadcasted transaction data: %s ", str) - return nil -} - // TryProcessOutbound signs and broadcasts a BTC transaction from a new outbound func (signer *Signer) TryProcessOutbound( ctx context.Context, cctx *types.CrossChainTx, outboundProcessor *outboundprocessor.Processor, outboundID string, - chainObserver interfaces.ChainObserver, + observer *observer.Observer, zetacoreClient interfaces.ZetacoreClient, height uint64, ) { @@ -145,13 +114,6 @@ func (signer *Signer) TryProcessOutbound( } logger := signer.Logger().Std.With().Fields(lf).Logger() - // convert chain observer to BTC observer - btcObserver, ok := chainObserver.(*observer.Observer) - if !ok { - logger.Error().Msg("chain observer is not a bitcoin observer") - return - } - // query network info to get minRelayFee (typically 1000 satoshis) networkInfo, err := signer.client.GetNetworkInfo() if err != nil { @@ -166,7 +128,7 @@ func (signer *Signer) TryProcessOutbound( var ( signedTx *wire.MsgTx - stuckTx = btcObserver.GetLastStuckOutbound() + stuckTx = observer.GetLastStuckOutbound() ) // sign outbound @@ -188,7 +150,7 @@ func (signer *Signer) TryProcessOutbound( } // sign withdraw tx - signedTx, err = signer.SignWithdrawTx(ctx, txData, btcObserver) + signedTx, err = signer.SignWithdrawTx(ctx, txData, observer) if err != nil { logger.Error().Err(err).Msg("SignWithdrawTx failed") return @@ -197,7 +159,7 @@ func (signer *Signer) TryProcessOutbound( } // broadcast signed outbound - signer.BroadcastOutbound(ctx, signedTx, params.TssNonce, cctx, btcObserver, zetacoreClient) + signer.BroadcastOutbound(ctx, signedTx, params.TssNonce, cctx, observer, zetacoreClient) } // BroadcastOutbound sends the signed transaction to the Bitcoin network diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 2998f094dc..f8808d495d 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -20,7 +20,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" - crosschainkeeper "github.com/zeta-chain/node/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/evm" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -126,7 +125,7 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { // should not happen. We can't tell which txHash is true. It might happen (e.g. bug, glitchy/hacked endpoint) ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, chainID, nonce) } else { - if len(tracker.HashList) == crosschainkeeper.MaxOutboundTrackerHashes { + if tracker.MaxReached() { ob.Logger().Outbound.Error().Msgf("WatchOutbound: outbound tracker is full of hashes for chain %d nonce %d", chainID, nonce) } } diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go index 4f35953ce0..26cab104c3 100644 --- a/zetaclient/context/chain.go +++ b/zetaclient/context/chain.go @@ -64,7 +64,9 @@ func (cr *ChainRegistry) Get(chainID int64) (Chain, error) { // All returns all chains in the registry sorted by chain ID. func (cr *ChainRegistry) All() []Chain { + cr.mu.Lock() items := maps.Values(cr.chains) + cr.mu.Unlock() slices.SortFunc(items, func(a, b Chain) int { return cmp.Compare(a.ID(), b.ID()) @@ -143,6 +145,10 @@ func (c Chain) Name() string { return c.chainInfo.Name } +func (c Chain) LogFields() map[string]any { + return c.RawChain().LogFields() +} + func (c Chain) Params() *observer.ChainParams { return c.observerParams } diff --git a/zetaclient/context/context.go b/zetaclient/context/context.go index 4d0b06866a..ee45eae58c 100644 --- a/zetaclient/context/context.go +++ b/zetaclient/context/context.go @@ -8,7 +8,7 @@ import ( type appContextKey struct{} -var ErrNotSet = errors.New("AppContext is not set in the context.Context") +var ErrNotSet = errors.New("unable to get AppContext from context.Context") // WithAppContext applied AppContext to standard Go context.Context. func WithAppContext(ctx goctx.Context, app *AppContext) goctx.Context { diff --git a/zetaclient/maintenance/shutdown_listener_test.go b/zetaclient/maintenance/shutdown_listener_test.go index c85c70cde0..f8ae414ef3 100644 --- a/zetaclient/maintenance/shutdown_listener_test.go +++ b/zetaclient/maintenance/shutdown_listener_test.go @@ -15,7 +15,7 @@ import ( func assertChannelNotClosed[T any](t *testing.T, ch <-chan T) { select { case <-ch: - t.FailNow() + t.Errorf("Failed: channel was closed") default: } } diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index b6bb729860..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,21 +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 - } - - rpcClient, err := rpc.NewRPCClient(cfg) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to create rpc client for BTC chain %d", chainID) - continue - } - - signer := btcsigner.NewSigner(*rawChain, rpcClient, tss, logger) + // managed by orchestrator V2 + continue - addSigner(chainID, signer) case chain.IsSolana(): cfg, found := app.Config().GetSolanaConfig() if !found { @@ -347,40 +332,9 @@ func syncObserverMap( addObserver(chainID, observer) case chain.IsBitcoin(): - cfg, found := app.Config().GetBTCConfig(chainID) - if !found { - logger.Std.Warn().Msgf("Unable to find BTC config for chain %d observer", chainID) - continue - } - - btcRPC, err := rpc.NewRPCClient(cfg) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to create rpc client for BTC chain %d", chainID) - continue - } - - database, err := db.NewFromSqlite(dbpath, btcDatabaseFileName(*rawChain), true) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to open database for BTC chain %d", chainID) - continue - } - - btcObserver, err := btcobserver.NewObserver( - *rawChain, - btcRPC, - *params, - client, - tss, - database, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for BTC chain %d", chainID) - continue - } + // managed by orchestrator V2 + continue - addObserver(chainID, btcObserver) case chain.IsSolana(): cfg, found := app.Config().GetSolanaConfig() if !found { diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstrap_test.go similarity index 78% rename from zetaclient/orchestrator/bootstap_test.go rename to zetaclient/orchestrator/bootstrap_test.go index 71b3ec744b..c6f44acf9a 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstrap_test.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/ptr" @@ -36,9 +37,6 @@ func TestCreateSignerMap(t *testing.T) { t.Run("CreateSignerMap", func(t *testing.T) { // ARRANGE - // Given a BTC server - _, btcConfig := testrpc.NewBtcServer(t) - // Given a zetaclient config with ETH, MATIC, and BTC chains cfg := config.New(false) @@ -50,8 +48,6 @@ func TestCreateSignerMap(t *testing.T) { Endpoint: testutils.MockEVMRPCEndpoint, } - cfg.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = btcConfig - // Given AppContext app := zctx.New(cfg, nil, log) ctx := zctx.WithAppContext(context.Background(), app) @@ -69,15 +65,14 @@ func TestCreateSignerMap(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, signers) - // Okay, now we want to check that signers for EVM and BTC were created - assert.Equal(t, 2, len(signers)) + // Okay, now we want to check that signer for EVM was created + assert.Equal(t, 1, len(signers)) hasSigner(t, signers, chains.Ethereum.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) t.Run("Add polygon in the runtime", func(t *testing.T) { // ARRANGE mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, chains.BitcoinMainnet, chains.Polygon, + chains.Ethereum, chains.Polygon, }) // ACT @@ -90,7 +85,6 @@ func TestCreateSignerMap(t *testing.T) { hasSigner(t, signers, chains.Ethereum.ChainId) hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) }) t.Run("Disable ethereum in the runtime", func(t *testing.T) { @@ -109,57 +103,14 @@ func TestCreateSignerMap(t *testing.T) { missesSigner(t, signers, chains.Ethereum.ChainId) hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Re-enable ethereum in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, - chains.Polygon, - chains.BitcoinMainnet, - }) - - // ACT - added, removed, err := syncSignerMap(ctx, tss, baseLogger, &signers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 1, added) - assert.Equal(t, 0, removed) - - hasSigner(t, signers, chains.Ethereum.ChainId) - hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Disable btc in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, - chains.Polygon, - }) - - // ACT - added, removed, err := syncSignerMap(ctx, tss, baseLogger, &signers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 0, added) - assert.Equal(t, 1, removed) - - hasSigner(t, signers, chains.Ethereum.ChainId) - hasSigner(t, signers, chains.Polygon.ChainId) missesSigner(t, signers, chains.BitcoinMainnet.ChainId) }) - t.Run("Re-enable btc in the runtime", func(t *testing.T) { + t.Run("Re-enable ethereum in the runtime", func(t *testing.T) { // ARRANGE - // Given updated data from zetacore containing polygon chain mustUpdateAppContextChainParams(t, app, []chains.Chain{ chains.Ethereum, chains.Polygon, - chains.BitcoinMainnet, }) // ACT @@ -172,7 +123,6 @@ func TestCreateSignerMap(t *testing.T) { hasSigner(t, signers, chains.Ethereum.ChainId) hasSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) }) t.Run("No changes", func(t *testing.T) { @@ -201,6 +151,8 @@ func TestCreateChainObserverMap(t *testing.T) { dbPath = db.SqliteInMemory ) + mockZetacore(client) + t.Run("CreateChainObserverMap", func(t *testing.T) { // ARRANGE // Given a BTC server @@ -238,11 +190,12 @@ func TestCreateChainObserverMap(t *testing.T) { ctx := zctx.WithAppContext(context.Background(), app) // Given chain & chainParams "fetched" from zetacore - // (note that slice LACKS polygon & SOL chains on purpose) + // note that slice LACKS polygon & SOL chains on purpose + // also note that BTC is handled by orchestrator v2 mustUpdateAppContextChainParams(t, app, []chains.Chain{ chains.Ethereum, - chains.BitcoinMainnet, chains.TONMainnet, + chains.BitcoinMainnet, }) // ACT @@ -253,10 +206,10 @@ func TestCreateChainObserverMap(t *testing.T) { assert.NotEmpty(t, observers) // Okay, now we want to check that signers for EVM and BTC were created - assert.Equal(t, 3, len(observers)) + assert.Equal(t, 2, len(observers)) hasObserver(t, observers, chains.Ethereum.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) hasObserver(t, observers, chains.TONMainnet.ChainId) + missesObserver(t, observers, chains.BitcoinMainnet.ChainId) t.Run("Add polygon and remove TON in the runtime", func(t *testing.T) { // ARRANGE @@ -274,7 +227,6 @@ func TestCreateChainObserverMap(t *testing.T) { hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) }) t.Run("Add solana in the runtime", func(t *testing.T) { @@ -296,7 +248,6 @@ func TestCreateChainObserverMap(t *testing.T) { hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) hasObserver(t, observers, chains.SolanaMainnet.ChainId) }) @@ -317,7 +268,6 @@ func TestCreateChainObserverMap(t *testing.T) { missesObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) missesObserver(t, observers, chains.SolanaMainnet.ChainId) }) @@ -337,45 +287,6 @@ func TestCreateChainObserverMap(t *testing.T) { hasObserver(t, observers, chains.Ethereum.ChainId) hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Disable btc in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.Ethereum, chains.Polygon, - }) - - // ACT - added, removed, err := syncObserverMap(ctx, client, tss, dbPath, baseLogger, ts, &observers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 0, added) - assert.Equal(t, 1, removed) - - hasObserver(t, observers, chains.Ethereum.ChainId) - hasObserver(t, observers, chains.Polygon.ChainId) - missesObserver(t, observers, chains.BitcoinMainnet.ChainId) - }) - - t.Run("Re-enable btc in the runtime", func(t *testing.T) { - // ARRANGE - mustUpdateAppContextChainParams(t, app, []chains.Chain{ - chains.BitcoinMainnet, chains.Ethereum, chains.Polygon, - }) - - // ACT - added, removed, err := syncObserverMap(ctx, client, tss, dbPath, baseLogger, ts, &observers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 1, added) - assert.Equal(t, 0, removed) - - hasObserver(t, observers, chains.Ethereum.ChainId) - hasObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) }) t.Run("No changes", func(t *testing.T) { @@ -503,6 +414,8 @@ func missesSigner(t *testing.T, signers map[int64]interfaces.ChainSigner, chainI } func hasObserver(t *testing.T, observer map[int64]interfaces.ChainObserver, chainId int64) { + t.Helper() + signer, ok := observer[chainId] assert.True(t, ok, "missing observer for chain %d", chainId) assert.NotEmpty(t, signer) @@ -512,3 +425,13 @@ func missesObserver(t *testing.T, observer map[int64]interfaces.ChainObserver, c _, ok := observer[chainId] assert.False(t, ok, "unexpected observer for chain %d", chainId) } + +// observer&signers have background tasks that rely on mocked calls. +// Ignorance results in FLAKY tests which fail silently with exit code 1. +func mockZetacore(client *mocks.ZetacoreClient) { + // ctx context.Context, chain chains.Chain, gasPrice uint64, priorityFee uint64, blockNum uint64 + client. + On("PostVoteGasPrice", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return("", nil). + Maybe() +} diff --git a/zetaclient/orchestrator/contextupdater.go b/zetaclient/orchestrator/contextupdater.go index 071ded772c..3c806a8eed 100644 --- a/zetaclient/orchestrator/contextupdater.go +++ b/zetaclient/orchestrator/contextupdater.go @@ -8,7 +8,6 @@ import ( "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/ticker" observertypes "github.com/zeta-chain/node/x/observer/types" zctx "github.com/zeta-chain/node/zetaclient/context" ) @@ -26,39 +25,6 @@ type Zetacore interface { var ErrUpgradeRequired = errors.New("upgrade required") -func (oc *Orchestrator) runAppContextUpdater(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - - interval := ticker.DurationFromUint64Seconds(app.Config().ConfigUpdateTicker) - - oc.logger.Info().Msg("UpdateAppContext worker started") - - task := func(ctx context.Context, t *ticker.Ticker) error { - err := UpdateAppContext(ctx, app, oc.zetacoreClient, oc.logger.Sampled) - switch { - case errors.Is(err, ErrUpgradeRequired): - oc.onUpgradeDetected(err) - t.Stop() - return nil - case err != nil: - oc.logger.Err(err).Msg("UpdateAppContext failed") - } - - return nil - } - - return ticker.Run( - ctx, - interval, - task, - ticker.WithLogger(oc.logger.Logger, "UpdateAppContext"), - ticker.WithStopChan(oc.stop), - ) -} - // UpdateAppContext fetches latest data from Zetacore and updates the AppContext. // Also detects if an upgrade is required. If an upgrade is required, it returns ErrUpgradeRequired. func UpdateAppContext(ctx context.Context, app *zctx.AppContext, zc Zetacore, logger zerolog.Logger) error { @@ -149,12 +115,3 @@ func checkForZetacoreUpgrade(ctx context.Context, zetaHeight int64, zc Zetacore) return nil } - -// onUpgradeDetected is called when an upgrade is detected. -func (oc *Orchestrator) onUpgradeDetected(errDetected error) { - const msg = "Upgrade detected." + - " Kill the process, replace the binary with upgraded version, and restart zetaclientd" - - oc.logger.Warn().Str("upgrade", errDetected.Error()).Msg(msg) - oc.Stop() -} diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index fb78d4d9f3..be71824e19 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "sync" + "sync/atomic" "time" sdkmath "cosmossdk.io/math" @@ -19,9 +20,7 @@ import ( zetamath "github.com/zeta-chain/node/pkg/math" "github.com/zeta-chain/node/pkg/ticker" "github.com/zeta-chain/node/x/crosschain/types" - observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" - btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" solanaobserver "github.com/zeta-chain/node/zetaclient/chains/solana/observer" tonobserver "github.com/zeta-chain/node/zetaclient/chains/ton/observer" @@ -72,10 +71,11 @@ type Orchestrator struct { signerBlockTimeOffset time.Duration // misc - logger multiLogger - ts *metrics.TelemetryServer - stop chan struct{} - mu sync.RWMutex + logger multiLogger + ts *metrics.TelemetryServer + stop chan struct{} + stopped atomic.Bool + mu sync.RWMutex } type multiLogger struct { @@ -137,7 +137,6 @@ func (oc *Orchestrator) Start(ctx context.Context) error { bg.Work(ctx, oc.runScheduler, bg.WithName("runScheduler"), bg.WithLogger(oc.logger.Logger)) bg.Work(ctx, oc.runObserverSignerSync, bg.WithName("runObserverSignerSync"), bg.WithLogger(oc.logger.Logger)) - bg.Work(ctx, oc.runAppContextUpdater, bg.WithName("runAppContextUpdater"), bg.WithLogger(oc.logger.Logger)) bg.Work( ctx, oc.runSyncObserverOperationalFlags, @@ -149,7 +148,15 @@ func (oc *Orchestrator) Start(ctx context.Context) error { } func (oc *Orchestrator) Stop() { + // noop + if oc.stopped.Load() { + oc.logger.Warn().Msg("Already stopped") + return + } + close(oc.stop) + + oc.stopped.Store(true) } // returns signer with updated chain parameters. @@ -211,18 +218,7 @@ func (oc *Orchestrator) resolveObserver(app *zctx.AppContext, chainID int64) (in } // update chain observer chain parameters - var ( - curParams = observer.ChainParams() - freshParams = chain.Params() - ) - - if !observertypes.ChainParamsEqual(curParams, *freshParams) { - observer.SetChainParams(*freshParams) - oc.logger.Info(). - Int64("observer.chain_id", chainID). - Interface("observer.chain_params", *freshParams). - Msg("updated chain params") - } + observer.SetChainParams(*chain.Params()) return observer, nil } @@ -366,6 +362,13 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { continue } + // managed by V2 + if chain.IsBitcoin() { + continue + } + + // todo move metrics to v2 + chainID := chain.ID() // update chain parameters for signer and chain observer @@ -407,7 +410,8 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { case chain.IsEVM(): oc.ScheduleCCTXEVM(ctx, zetaHeight, chainID, cctxList, ob, signer) case chain.IsBitcoin(): - oc.ScheduleCCTXBTC(ctx, zetaHeight, chainID, cctxList, ob, signer) + // Managed by orchestrator V2 + continue case chain.IsSolana(): oc.ScheduleCCTXSolana(ctx, zetaHeight, chainID, cctxList, ob, signer) case chain.IsTON(): @@ -523,81 +527,6 @@ func (oc *Orchestrator) ScheduleCCTXEVM( } } -// ScheduleCCTXBTC schedules bitcoin outbound keysign on each ZetaChain block (the ticker) -// 1. schedule at most one keysign per ticker -// 2. schedule keysign only when nonce-mark UTXO is available -// 3. stop keysign when lookahead is reached -func (oc *Orchestrator) ScheduleCCTXBTC( - ctx context.Context, - zetaHeight uint64, - chainID int64, - cctxList []*types.CrossChainTx, - observer interfaces.ChainObserver, - signer interfaces.ChainSigner, -) { - btcObserver, ok := observer.(*btcobserver.Observer) - if !ok { // should never happen - oc.logger.Error().Msgf("ScheduleCCTXBTC: chain observer is not a bitcoin observer") - return - } - // #nosec G115 positive - interval := uint64(observer.ChainParams().OutboundScheduleInterval) - lookahead := observer.ChainParams().OutboundScheduleLookahead - - // schedule at most one keysign per ticker - for idx, cctx := range cctxList { - params := cctx.GetCurrentOutboundParam() - nonce := params.TssNonce - outboundID := outboundprocessor.ToOutboundID(cctx.Index, params.ReceiverChainId, nonce) - - if params.ReceiverChainId != chainID { - oc.logger.Error(). - Msgf("ScheduleCCTXBTC: outbound %s chainid mismatch: want %d, got %d", outboundID, chainID, params.ReceiverChainId) - continue - } - // try confirming the outbound - continueKeysign, err := btcObserver.VoteOutboundIfConfirmed(ctx, cctx) - if err != nil { - oc.logger.Error(). - Err(err). - Msgf("ScheduleCCTXBTC: VoteOutboundIfConfirmed failed for chain %d nonce %d", chainID, nonce) - continue - } - if !continueKeysign { - oc.logger.Info(). - Msgf("ScheduleCCTXBTC: outbound %s already processed; do not schedule keysign", outboundID) - continue - } - - // stop if the nonce being processed is higher than the pending nonce - if nonce > btcObserver.GetPendingNonce() { - break - } - // stop if lookahead is reached - if int64( - idx, - ) >= lookahead { // 2 bitcoin confirmations span is 20 minutes on average. We look ahead up to 100 pending cctx to target TPM of 5. - oc.logger.Warn(). - Msgf("ScheduleCCTXBTC: lookahead reached, signing %d, earliest pending %d", nonce, cctxList[0].GetCurrentOutboundParam().TssNonce) - break - } - // schedule a TSS keysign - if nonce%interval == zetaHeight%interval && !oc.outboundProc.IsOutboundActive(outboundID) { - oc.outboundProc.StartTryProcess(outboundID) - oc.logger.Debug().Msgf("ScheduleCCTXBTC: sign outbound %s with value %d", outboundID, params.Amount) - go signer.TryProcessOutbound( - ctx, - cctx, - oc.outboundProc, - outboundID, - observer, - oc.zetacoreClient, - zetaHeight, - ) - } - } -} - // ScheduleCCTXSolana schedules solana outbound keysign on each ZetaChain block (the ticker) func (oc *Orchestrator) ScheduleCCTXSolana( ctx context.Context, diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go new file mode 100644 index 0000000000..90d19a9214 --- /dev/null +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -0,0 +1,72 @@ +package orchestrator + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" + btcsigner "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/db" +) + +func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin.Bitcoin, error) { + // should not happen + if !chain.IsBitcoin() { + return nil, errors.New("chain is not bitcoin") + } + + app, err := zctx.FromContext(ctx) + if err != nil { + return nil, err + } + + cfg, found := app.Config().GetBTCConfig(chain.ID()) + if !found { + return nil, errors.Wrap(errSkipChain, "unable to find btc config") + } + + rpcClient, err := rpc.NewRPCClient(cfg) + if err != nil { + return nil, errors.Wrap(err, "unable to create rpc client") + } + + var ( + rawChain = chain.RawChain() + rawChainParams = chain.Params() + ) + + dbName := btcDatabaseFileName(*rawChain) + + database, err := db.NewFromSqlite(oc.deps.DBPath, dbName, true) + if err != nil { + return nil, errors.Wrapf(err, "unable to open database %s", dbName) + } + + // TODO extract base observer + // TODO extract base signer + // https://github.com/zeta-chain/node/issues/3331 + observer, err := btcobserver.NewObserver( + *rawChain, + rpcClient, + *rawChainParams, + oc.deps.Zetacore, + oc.deps.TSS, + database, + oc.logger.base, + oc.deps.Telemetry, + ) + if err != nil { + return nil, errors.Wrap(err, "unable to create observer") + } + + signer := btcsigner.NewSigner(*rawChain, rpcClient, oc.deps.TSS, oc.logger.base) + if err != nil { + return nil, errors.Wrap(err, "unable to create signer") + } + + return bitcoin.New(oc.scheduler, observer, signer), nil +} diff --git a/zetaclient/orchestrator/v2_bootstrap_test.go b/zetaclient/orchestrator/v2_bootstrap_test.go new file mode 100644 index 0000000000..c1e64df23d --- /dev/null +++ b/zetaclient/orchestrator/v2_bootstrap_test.go @@ -0,0 +1,78 @@ +package orchestrator + +import ( + "testing" + "time" + + cometbfttypes "github.com/cometbft/cometbft/types" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/scheduler" + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/testutils/testrpc" +) + +func TestBootstrap(t *testing.T) { + t.Run("Bitcoin", func(t *testing.T) { + // ARRANGE + // Given orchestrator + ts := newTestSuite(t) + + // Given BTC client + btcServer, btcConfig := testrpc.NewBtcServer(t) + + ts.UpdateConfig(func(cfg *config.Config) { + cfg.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = btcConfig + }) + + mockBitcoinCalls(ts, btcServer) + + // ACT + // Start the orchestrator and wait for BTC observerSigner to bootstrap + require.NoError(t, ts.Start(ts.ctx)) + + // ASSERT + // Check that btc observerSigner is bootstrapped. + check := func() bool { + ts.V2.mu.RLock() + defer ts.V2.mu.RUnlock() + + _, ok := ts.V2.chains[chains.BitcoinMainnet.ChainId] + return ok + } + + assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) + + // Check that the scheduler has some tasks for this + tasksHaveGroup(t, ts.scheduler.Tasks(), "btc:8332") + + assert.Contains(t, ts.Log.String(), `"chain":8332,"chain_network":"btc","message":"Added observer-signer"`) + }) +} + +func tasksHaveGroup(t *testing.T, tasks map[uuid.UUID]*scheduler.Task, group string) { + var found bool + for _, task := range tasks { + // t.Logf("Task %s:%s", task.Group(), task.Name()) + if !found && task.Group() == scheduler.Group(group) { + found = true + } + } + + assert.True(t, found, "Group %s not found in tasks", group) +} + +func mockBitcoinCalls(ts *testSuite, client *testrpc.BtcServer) { + client.SetBlockCount(100) + + blockChan := make(chan cometbfttypes.EventDataNewBlock) + ts.zetacore.On("NewBlockSubscriber", mock.Anything).Return(blockChan, nil) + + ts.zetacore.On("GetInboundTrackersForChain", mock.Anything, mock.Anything).Return(nil, nil) + ts.zetacore.On("GetPendingNoncesByChain", mock.Anything, mock.Anything).Return(observertypes.PendingNonces{}, nil) + ts.zetacore.On("GetAllOutboundTrackerByChain", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) +} diff --git a/zetaclient/orchestrator/v2_orchestrator.go b/zetaclient/orchestrator/v2_orchestrator.go new file mode 100644 index 0000000000..4f970dddb7 --- /dev/null +++ b/zetaclient/orchestrator/v2_orchestrator.go @@ -0,0 +1,307 @@ +package orchestrator + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/scheduler" + "github.com/zeta-chain/node/pkg/ticker" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/logs" + "github.com/zeta-chain/node/zetaclient/metrics" +) + +// V2 represents the orchestrator V2 while they co-exist with Orchestrator. +type V2 struct { + deps *Dependencies + scheduler *scheduler.Scheduler + + chains map[int64]ObserverSigner + mu sync.RWMutex + + logger loggers +} + +type loggers struct { + zerolog.Logger + sampled zerolog.Logger + base base.Logger +} + +const schedulerGroup = scheduler.Group("orchestrator") + +type ObserverSigner interface { + Chain() chains.Chain + Start(ctx context.Context) error + Stop() +} + +type Dependencies struct { + Zetacore interfaces.ZetacoreClient + TSS interfaces.TSSSigner + DBPath string + Telemetry *metrics.TelemetryServer +} + +func NewV2(scheduler *scheduler.Scheduler, deps *Dependencies, logger base.Logger) (*V2, error) { + if err := validateConstructor(scheduler, deps); err != nil { + return nil, errors.Wrap(err, "invalid args") + } + + return &V2{ + scheduler: scheduler, + deps: deps, + chains: make(map[int64]ObserverSigner), + logger: newLoggers(logger), + }, nil +} + +func (oc *V2) Start(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + // syntax sugar + opts := func(name string, opts ...scheduler.Opt) []scheduler.Opt { + return append(opts, scheduler.GroupName(schedulerGroup), scheduler.Name(name)) + } + + contextInterval := scheduler.IntervalUpdater(func() time.Duration { + return ticker.DurationFromUint64Seconds(app.Config().ConfigUpdateTicker) + }) + + // every other block + syncInterval := scheduler.Interval(2 * constant.ZetaBlockTime) + + oc.scheduler.Register(ctx, oc.UpdateContext, opts("update_context", contextInterval)...) + oc.scheduler.Register(ctx, oc.SyncChains, opts("sync_chains", syncInterval)...) + + return nil +} + +func (oc *V2) Stop() { + oc.logger.Info().Msg("Stopping orchestrator") + + // stops *all* scheduler tasks + oc.scheduler.Stop() +} + +func (oc *V2) UpdateContext(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + err = UpdateAppContext(ctx, app, oc.deps.Zetacore, oc.logger.Logger) + + switch { + case errors.Is(err, ErrUpgradeRequired): + const msg = "Upgrade detected. Kill the process, " + + "replace the binary with upgraded version, and restart zetaclientd" + + oc.logger.Warn().Str("upgrade", err.Error()).Msg(msg) + + // stop the orchestrator + go oc.Stop() + + return nil + case err != nil: + return errors.Wrap(err, "unable to update app context") + default: + return nil + } +} + +var errSkipChain = errors.New("skip chain") + +func (oc *V2) SyncChains(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + var ( + added, removed int + presentChainIDs = make([]int64, 0) + ) + + for _, chain := range app.ListChains() { + // skip zetachain + if chain.IsZeta() { + continue + } + + presentChainIDs = append(presentChainIDs, chain.ID()) + + // skip existing chain + if oc.hasChain(chain.ID()) { + continue + } + + var observerSigner ObserverSigner + + switch { + case chain.IsBitcoin(): + observerSigner, err = oc.bootstrapBitcoin(ctx, chain) + case chain.IsEVM(): + // TODO + // https://github.com/zeta-chain/node/issues/3302 + continue + case chain.IsSolana(): + // TODO + // https://github.com/zeta-chain/node/issues/3301 + continue + case chain.IsTON(): + // TODO + // https://github.com/zeta-chain/node/issues/3300 + continue + } + + switch { + case errors.Is(errSkipChain, err): + // TODO use throttled logger instead of sampled one. + // https://github.com/zeta-chain/node/issues/3336 + oc.logger.sampled.Warn().Err(err).Fields(chain.LogFields()).Msg("Skipping observer-signer") + continue + case err != nil: + oc.logger.Error().Err(err).Fields(chain.LogFields()).Msg("Failed to bootstrap observer-signer") + continue + case observerSigner == nil: + // should not happen + oc.logger.Error().Fields(chain.LogFields()).Msg("Nil observer-signer") + continue + } + + if err = observerSigner.Start(ctx); err != nil { + oc.logger.Error().Err(err).Fields(chain.LogFields()).Msg("Failed to start observer-signer") + continue + } + + oc.addChain(observerSigner) + added++ + } + + removed = oc.removeMissingChains(presentChainIDs) + + if (added + removed) > 0 { + oc.logger.Info(). + Int("chains.added", added). + Int("chains.removed", removed). + Msg("Synced observer-signers") + } + + return nil +} + +func (oc *V2) hasChain(chainID int64) bool { + oc.mu.RLock() + defer oc.mu.RUnlock() + + _, ok := oc.chains[chainID] + return ok +} + +func (oc *V2) chainIDs() []int64 { + oc.mu.RLock() + defer oc.mu.RUnlock() + + ids := make([]int64, 0, len(oc.chains)) + for chainID := range oc.chains { + ids = append(ids, chainID) + } + + return ids +} + +func (oc *V2) addChain(observerSigner ObserverSigner) { + chain := observerSigner.Chain() + + oc.mu.Lock() + defer oc.mu.Unlock() + + // noop + if _, ok := oc.chains[chain.ChainId]; ok { + return + } + + oc.chains[chain.ChainId] = observerSigner + oc.logger.Info().Fields(chain.LogFields()).Msg("Added observer-signer") +} + +func (oc *V2) removeChain(chainID int64) { + // noop, should not happen + if !oc.hasChain(chainID) { + return + } + + // blocking call + oc.chains[chainID].Stop() + + oc.mu.Lock() + delete(oc.chains, chainID) + oc.mu.Unlock() + + oc.logger.Info().Int64(logs.FieldChain, chainID).Msg("Removed observer-signer") +} + +// removeMissingChains stops and deletes chains +// that are not present in the list of chainIDs (e.g. after governance proposal) +func (oc *V2) removeMissingChains(presentChainIDs []int64) int { + presentChainsSet := make(map[int64]struct{}) + for _, chainID := range presentChainIDs { + presentChainsSet[chainID] = struct{}{} + } + + existingIDs := oc.chainIDs() + removed := 0 + + for _, chainID := range existingIDs { + if _, ok := presentChainsSet[chainID]; ok { + // all good, chain is present + continue + } + + oc.removeChain(chainID) + removed++ + } + + return removed +} + +func validateConstructor(s *scheduler.Scheduler, dep *Dependencies) error { + switch { + case s == nil: + return errors.New("scheduler is nil") + case dep == nil: + return errors.New("dependencies are nil") + case dep.Zetacore == nil: + return errors.New("zetacore is nil") + case dep.TSS == nil: + return errors.New("tss is nil") + case dep.Telemetry == nil: + return errors.New("telemetry is nil") + case dep.DBPath == "": + return errors.New("db path is empty") + } + + return nil +} + +func newLoggers(baseLogger base.Logger) loggers { + std := baseLogger.Std.With().Str(logs.FieldModule, "orchestrator").Logger() + + return loggers{ + Logger: std, + sampled: std.Sample(&zerolog.BasicSampler{N: 10}), + base: baseLogger, + } +} diff --git a/zetaclient/orchestrator/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go new file mode 100644 index 0000000000..c10c11481f --- /dev/null +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -0,0 +1,267 @@ +package orchestrator + +import ( + "context" + "reflect" + "sync" + "testing" + "time" + "unsafe" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/scheduler" + "github.com/zeta-chain/node/testutil/sample" + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/config" + zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/db" + "github.com/zeta-chain/node/zetaclient/metrics" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "github.com/zeta-chain/node/zetaclient/testutils/testlog" + "github.com/zeta-chain/node/zetaclient/testutils/testrpc" +) + +func TestOrchestratorV2(t *testing.T) { + t.Run("updates app context", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // ACT #1 + // Start orchestrator + err := ts.Start(ts.ctx) + + // Mimic zetacore update + ts.MockChainParams(chains.Ethereum, mocks.MockChainParams(chains.Ethereum.ChainId, 100)) + + // ASSERT #1 + require.NoError(t, err) + + // Check that eventually appContext would contain only desired chains + check := func() bool { + list := ts.appContext.ListChains() + return len(list) == 1 && chainsContain(list, chains.Ethereum.ChainId) + } + + assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) + + assert.Contains(t, ts.Log.String(), "Chain list changed at the runtime!") + assert.Contains(t, ts.Log.String(), `"chains.new":[1]`) + + // ACT #2 + // Mimic zetacore update that adds bitcoin chain with chain params + ts.MockChainParams( + chains.Ethereum, + mocks.MockChainParams(chains.Ethereum.ChainId, 100), + chains.BitcoinMainnet, + mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 100), + ) + + check = func() bool { + list := ts.appContext.ListChains() + return len(list) == 2 && chainsContain(list, chains.Ethereum.ChainId, chains.BitcoinMainnet.ChainId) + } + + assert.Eventually(t, check, 5*time.Second, 100*time.Millisecond) + + assert.Contains(t, ts.Log.String(), `"chains.new":[1,8332],"message":"Chain list changed at the runtime!"`) + }) +} + +type testSuite struct { + *V2 + *testlog.Log + + t *testing.T + + ctx context.Context + appContext *zctx.AppContext + + chains []chains.Chain + chainParams []*observertypes.ChainParams + + zetacore *mocks.ZetacoreClient + scheduler *scheduler.Scheduler + tss *mocks.TSS + + mu sync.Mutex +} + +var defaultChainsWithParams = []any{ + chains.Ethereum, + chains.BitcoinMainnet, + chains.SolanaMainnet, + chains.TONMainnet, + + mocks.MockChainParams(chains.Ethereum.ChainId, 100), + mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 3), + mocks.MockChainParams(chains.SolanaMainnet.ChainId, 10), + mocks.MockChainParams(chains.TONMainnet.ChainId, 1), +} + +func newTestSuite(t *testing.T) *testSuite { + logger := testlog.New(t) + baseLogger := base.Logger{ + Std: logger.Logger, + Compliance: logger.Logger, + } + + testrpc.NewBtcServer(t) + + chainList, chainParams := parseChainsWithParams(t, defaultChainsWithParams...) + + ctx, appCtx := newAppContext(t, logger.Logger, chainList, chainParams) + + ctx, cancel := context.WithCancel(ctx) + t.Cleanup(cancel) + + // Services + var ( + schedulerService = scheduler.New(logger.Logger) + zetacore = mocks.NewZetacoreClient(t) + tss = mocks.NewTSS(t) + ) + + deps := &Dependencies{ + Zetacore: zetacore, + TSS: tss, + DBPath: db.SqliteInMemory, + Telemetry: metrics.NewTelemetryServer(), + } + + v2, err := NewV2(schedulerService, deps, baseLogger) + require.NoError(t, err) + + ts := &testSuite{ + V2: v2, + Log: logger, + + t: t, + + ctx: ctx, + appContext: appCtx, + + chains: chainList, + chainParams: chainParams, + + scheduler: schedulerService, + zetacore: zetacore, + tss: tss, + } + + // Mock basic zetacore methods + zetacore.On("GetBlockHeight", mock.Anything).Return(int64(123), nil).Maybe() + zetacore.On("GetUpgradePlan", mock.Anything).Return(nil, nil).Maybe() + zetacore.On("GetAdditionalChains", mock.Anything).Return(nil, nil).Maybe() + zetacore.On("GetCrosschainFlags", mock.Anything).Return(appCtx.GetCrossChainFlags(), nil).Maybe() + + // Mock chain-related methods as dynamic getters + zetacore.On("GetSupportedChains", mock.Anything).Return(ts.getSupportedChains).Maybe() + zetacore.On("GetChainParams", mock.Anything).Return(ts.getChainParams).Maybe() + + t.Cleanup(ts.Stop) + + return ts +} + +func (ts *testSuite) MockChainParams(newValues ...any) { + chainList, chainParams := parseChainsWithParams(ts.t, newValues...) + + ts.mu.Lock() + defer ts.mu.Unlock() + + ts.chains = chainList + ts.chainParams = chainParams +} + +func (ts *testSuite) getSupportedChains(_ context.Context) ([]chains.Chain, error) { + ts.mu.Lock() + defer ts.mu.Unlock() + return ts.chains, nil +} + +func (ts *testSuite) getChainParams(_ context.Context) ([]*observertypes.ChainParams, error) { + ts.mu.Lock() + defer ts.mu.Unlock() + return ts.chainParams, nil +} + +// UpdateConfig updates "global" config.Config for test suite. +func (ts *testSuite) UpdateConfig(fn func(cfg *config.Config)) { + cfg := ts.appContext.Config() + fn(&cfg) + + // The config is sealed i.e. we can't alter it after starting zetaclientd. + // But for test purposes we use `reflect` to mimic + // that it was set by the validator *before* starting the app. + field := reflect.ValueOf(ts.appContext).Elem().FieldByName("config") + ptr := unsafe.Pointer(field.UnsafeAddr()) + configPtr := (*config.Config)(ptr) + + *configPtr = cfg +} + +func newAppContext( + t *testing.T, + logger zerolog.Logger, + chainList []chains.Chain, + chainParams []*observertypes.ChainParams, +) (context.Context, *zctx.AppContext) { + // Mock config + cfg := config.New(false) + + cfg.ConfigUpdateTicker = 1 + + for _, c := range chainList { + switch { + case chains.IsEVMChain(c.ChainId, nil): + cfg.EVMChainConfigs[c.ChainId] = config.EVMConfig{Endpoint: "localhost"} + case chains.IsBitcoinChain(c.ChainId, nil): + cfg.BTCChainConfigs[c.ChainId] = config.BTCConfig{RPCHost: "localhost"} + case chains.IsSolanaChain(c.ChainId, nil): + cfg.SolanaConfig = config.SolanaConfig{Endpoint: "localhost"} + case chains.IsTONChain(c.ChainId, nil): + cfg.TONConfig = config.TONConfig{LiteClientConfigURL: "localhost"} + default: + t.Fatalf("create app context: unsupported chain %d", c.ChainId) + } + } + + // chain params + params := map[int64]*observertypes.ChainParams{} + for i := range chainParams { + cp := chainParams[i] + params[cp.ChainId] = cp + } + + // new AppContext + appContext := zctx.New(cfg, nil, logger) + + ccFlags := sample.CrosschainFlags() + + err := appContext.Update(chainList, nil, params, *ccFlags) + require.NoError(t, err, "failed to update app context") + + ctx := zctx.WithAppContext(context.Background(), appContext) + + return ctx, appContext +} + +func chainsContain(list []zctx.Chain, ids ...int64) bool { + set := make(map[int64]struct{}, len(list)) + for _, chain := range list { + set[chain.ID()] = struct{}{} + } + + for _, chainID := range ids { + if _, found := set[chainID]; !found { + return false + } + } + + return true +} diff --git a/zetaclient/testutils/mocks/chain_params.go b/zetaclient/testutils/mocks/chain_params.go index 15568c6e61..c421097c5a 100644 --- a/zetaclient/testutils/mocks/chain_params.go +++ b/zetaclient/testutils/mocks/chain_params.go @@ -32,7 +32,7 @@ func MockChainParams(chainID int64, confirmation uint64) observertypes.ChainPara Erc20CustodyContractAddress: erc20CustodyAddr, InboundTicker: 12, OutboundTicker: 15, - WatchUtxoTicker: 0, + WatchUtxoTicker: 1, GasPriceTicker: 30, OutboundScheduleInterval: 30, OutboundScheduleLookahead: 60, diff --git a/zetaclient/testutils/testlog/log.go b/zetaclient/testutils/testlog/log.go new file mode 100644 index 0000000000..b3d9555a90 --- /dev/null +++ b/zetaclient/testutils/testlog/log.go @@ -0,0 +1,50 @@ +package testlog + +import ( + "bytes" + "io" + "sync" + "testing" + + "github.com/rs/zerolog" +) + +type Log struct { + zerolog.Logger + buf *concurrentBytesBuffer +} + +type concurrentBytesBuffer struct { + buf *bytes.Buffer + mu sync.RWMutex +} + +// New creates a new Log instance with a buffer and a test writer. +func New(t *testing.T) *Log { + buf := &concurrentBytesBuffer{ + buf: &bytes.Buffer{}, + mu: sync.RWMutex{}, + } + + log := zerolog.New(io.MultiWriter(zerolog.NewTestWriter(t), buf)) + + return &Log{Logger: log, buf: buf} +} + +func (log *Log) String() string { + return log.buf.string() +} + +func (b *concurrentBytesBuffer) Write(p []byte) (n int, err error) { + b.mu.Lock() + defer b.mu.Unlock() + + return b.buf.Write(p) +} + +func (b *concurrentBytesBuffer) string() string { + b.mu.RLock() + defer b.mu.RUnlock() + + return b.buf.String() +} diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index 7a8391ff89..4938376fff 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -23,6 +23,7 @@ import ( // KeySigner signs messages using TSS (subset of go-tss) type KeySigner interface { KeySign(req keysign.Request) (keysign.Response, error) + Stop() } // Zetacore zeta core client. @@ -225,6 +226,12 @@ func (s *Service) SignBatch( return sigs, nil } +func (s *Service) Stop() { + s.logger.Info().Msg("Stopping TSS service") + s.tss.Stop() + s.logger.Info().Msg("TSS service stopped") +} + var ( signLabelsSuccess = prometheus.Labels{"result": "success"} signLabelsError = prometheus.Labels{"result": "error"} diff --git a/zetaclient/tss/service_test.go b/zetaclient/tss/service_test.go index e0ccde6954..87e3c86db7 100644 --- a/zetaclient/tss/service_test.go +++ b/zetaclient/tss/service_test.go @@ -153,6 +153,8 @@ func newKeySignerMock(t *testing.T) *keySignerMock { } } +func (*keySignerMock) Stop() { return } + func (m *keySignerMock) PubKeyBech32() string { cosmosPrivateKey := &secp256k1.PrivKey{Key: m.privateKey.D.Bytes()} pk := cosmosPrivateKey.PubKey() diff --git a/zetaclient/zetacore/client.go b/zetaclient/zetacore/client.go index df5b6dbeb6..a883aca855 100644 --- a/zetaclient/zetacore/client.go +++ b/zetaclient/zetacore/client.go @@ -8,6 +8,7 @@ import ( cometbftrpc "github.com/cometbft/cometbft/rpc/client" cometbfthttp "github.com/cometbft/cometbft/rpc/client/http" + ctypes "github.com/cometbft/cometbft/types" cosmosclient "github.com/cosmos/cosmos-sdk/client" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/pkg/errors" @@ -19,6 +20,7 @@ import ( "github.com/zeta-chain/node/app" "github.com/zeta-chain/node/pkg/authz" "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/fanout" zetacorerpc "github.com/zeta-chain/node/pkg/rpc" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/config" @@ -47,6 +49,9 @@ type Client struct { chainID string chain chains.Chain + // blocksFanout that receives new block events from Zetacore via websockets + blocksFanout *fanout.FanOut[ctypes.EventDataNewBlock] + mu sync.RWMutex } diff --git a/zetaclient/zetacore/client_subscriptions.go b/zetaclient/zetacore/client_subscriptions.go index cb4229b31b..7bf5ea1c25 100644 --- a/zetaclient/zetacore/client_subscriptions.go +++ b/zetaclient/zetacore/client_subscriptions.go @@ -3,33 +3,97 @@ package zetacore import ( "context" - cometbfttypes "github.com/cometbft/cometbft/types" + "cosmossdk.io/errors" + ctypes "github.com/cometbft/cometbft/types" + + "github.com/zeta-chain/node/pkg/fanout" ) -// NewBlockSubscriber subscribes to cometbft new block events -func (c *Client) NewBlockSubscriber(ctx context.Context) (chan cometbfttypes.EventDataNewBlock, error) { - rawBlockEventChan, err := c.cometBFTClient.Subscribe(ctx, "", cometbfttypes.EventQueryNewBlock.String()) +// NewBlockSubscriber subscribes to comet bft new block events. +// Subscribes share the same websocket connection but their channels are independent (fanout) +func (c *Client) NewBlockSubscriber(ctx context.Context) (chan ctypes.EventDataNewBlock, error) { + blockSubscriber, err := c.resolveBlockSubscriber() if err != nil { - return nil, err + return nil, errors.Wrap(err, "unable to resolve block subscriber") } - blockEventChan := make(chan cometbfttypes.EventDataNewBlock) + // we need a "proxy" chan instead of directly returning blockSubscriber.Add() + // to support context cancellation + blocksChan := make(chan ctypes.EventDataNewBlock) go func() { + consumer := blockSubscriber.Add() + for { select { case <-ctx.Done(): + // fixme: MEMORY LEAK: this might be dangerous because the consumer is not closed. + // Fanout will spawn "zombie" goroutines to push to the chan, but nobody is reading from it, + // Will be addressed in future orchestrator V2 PRs (not urgent as of now) return - case event := <-rawBlockEventChan: - newBlockEvent, ok := event.Data.(cometbfttypes.EventDataNewBlock) - if !ok { - c.logger.Error().Msgf("expecting new block event, got %T", event.Data) - continue - } - blockEventChan <- newBlockEvent + case block := <-consumer: + blocksChan <- block } } }() - return blockEventChan, nil + return blocksChan, nil +} + +// resolveBlockSubscriber returns the block subscriber channel +// or subscribes to it for the first time. +func (c *Client) resolveBlockSubscriber() (*fanout.FanOut[ctypes.EventDataNewBlock], error) { + // noop + if blocksFanout := c.blockFanOutThreadSafe(); blocksFanout != nil { + c.logger.Info().Msg("Resolved existing block subscriber") + return blocksFanout, nil + } + + // we need this lock to prevent 2 Subscribe calls at the same time + c.mu.Lock() + defer c.mu.Unlock() + + c.logger.Info().Msg("Subscribing to block events") + + // Subscribe to comet bft events + eventsChan, err := c.cometBFTClient.Subscribe(context.Background(), "", ctypes.EventQueryNewBlock.String()) + if err != nil { + return nil, errors.Wrap(err, "unable to subscribe to new block events") + } + + c.logger.Info().Msg("Subscribed to block events") + + // Create block chan + blockChan := make(chan ctypes.EventDataNewBlock) + + // Spin up a pipeline to forward block events to the blockChan + go func() { + for event := range eventsChan { + newBlockEvent, ok := event.Data.(ctypes.EventDataNewBlock) + if !ok { + c.logger.Error().Msgf("expecting new block event, got %T", event.Data) + continue + } + + c.logger.Info().Int64("height", newBlockEvent.Block.Height).Msg("Received new block event") + + blockChan <- newBlockEvent + } + }() + + // Create a fanout + // It allows a "global" chan (i.e. blockChan) to stream to multiple consumers independently. + fo := fanout.New[ctypes.EventDataNewBlock](blockChan, fanout.DefaultBuffer) + fo.Start() + + c.blocksFanout = fo + + return fo, nil +} + +func (c *Client) blockFanOutThreadSafe() *fanout.FanOut[ctypes.EventDataNewBlock] { + c.mu.Lock() + defer c.mu.Unlock() + + return c.blocksFanout }