diff --git a/Makefile b/Makefile index 04c25ed88..8184f72b7 100644 --- a/Makefile +++ b/Makefile @@ -137,9 +137,9 @@ $(BUILDDIR)/: mkdir -p $(BUILDDIR)/ build-test-tool: - go build $(BUILD_FLAGS) $(BUILD_ARGS) ./cmd/test-tool + go build $(BUILD_FLAGS) $(BUILD_ARGS) ./cmd/exocore-test-tool install-test-tool: - go install $(BUILD_FLAGS) $(BUILD_ARGS) ./cmd/test-tool + go install $(BUILD_FLAGS) $(BUILD_ARGS) ./cmd/exocore-test-tool build-reproducible: go.sum $(DOCKER) rm latest-build || true diff --git a/cmd/test-tool/README.md b/cmd/exocore-test-tool/README.md similarity index 73% rename from cmd/test-tool/README.md rename to cmd/exocore-test-tool/README.md index cfd43505c..298d332b6 100644 --- a/cmd/test-tool/README.md +++ b/cmd/exocore-test-tool/README.md @@ -1,11 +1,12 @@ -# test-tool +# exocore-test-tool This is a custom tool designed to batch-send test transactions to the Exocore chain. It can be used for stress testing or routine automated testing of the Exocore chain. Currently, all test transactions are executed by directly calling precompiles and are signed using automatically generated private keys. Therefore, a customized Exocore node is required for use, with the node configured to disable -the precompile's gateway contract address check. +the precompile's gateway contract address check. The branch of customized Exocore is as below: +https://github.com/ExocoreNetwork/exocore/tree/pressure-test When using the test tool to batch-send test transactions, you can dynamically adjust the number of test objects and the transaction sending rate in the configuration file to control the test volume. This allows for routine automated testing @@ -51,6 +52,58 @@ The current implementation primarily provides the following functionalities: command handles the creation, funding, registration, and opting-in of test objects, while the `batch-test` command allows for manual batch testing of different transaction types. +The specific processes for the two types of testing are as follows: +A local node needs to be started through `local_node.sh` before testing. + +1. automated testing: + Step 1: Initialize the configuration file in current directory + + ```shell + exocore-test-tool init --home + ``` + + Step 2: Start the test tool + + ```shell + exocore-test-tool start --home . + ``` + +2. manual testing: + + Step 1: Initialize the configuration file in current directory + + ```shell + exocore-test-tool init --home + ``` + + Step 2: Prepare test environment + + ```shell + exocore-test-tool prepare --home . + ``` + + Step 3: Run batch tests + + ```shell + exocore-test-tool batch-test depositLST --home . + ``` + +We can query transactions for a specific batch and status using the following command, either during or after the test: + +```shell +exocore-test-tool query-tx-record +``` + +## go binding generation + +In the implementation of the test tool, since it needs to directly call precompiled contracts, the Go binding file for +the contract will be used. This binding file is automatically generated using abigen. For example, the Go binding for +the asset precompile can be generated using the following command: + +```shell +abigen --abi abi.json --pkg assets --out assets_binding.go +``` + ## todo * Feed prices for all test assets. Currently, the test does not enable the oracle. We might consider providing a fake diff --git a/cmd/test-tool/main.go b/cmd/exocore-test-tool/main.go similarity index 95% rename from cmd/test-tool/main.go rename to cmd/exocore-test-tool/main.go index 7b055d66c..59e3b6f1a 100644 --- a/cmd/test-tool/main.go +++ b/cmd/exocore-test-tool/main.go @@ -71,7 +71,7 @@ var initCmd = &cobra.Command{ Short: "init the default config for the test tool", Long: "init the default config for the test tool, using test-tool-config.toml " + "as the default name of the config file", - Example: "test-tool init --home .", + Example: "exocore-test-tool init --home .", Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { configFilePath := filepath.Join(homePath, batch.ConfigFileName) @@ -87,6 +87,7 @@ var initCmd = &cobra.Command{ encoder := toml.NewEncoder(file) if err := encoder.Encode(batch.DefaultTestToolConfig); err != nil { fmt.Printf("failed to encode config to TOML: %s\r\n", err) + return } }, } @@ -97,7 +98,7 @@ var startCmd = &cobra.Command{ Short: "start the test tool", Long: "Start the testing tool to automatically perform preparation steps " + "and batch tests for multiple message types.", - Example: "test-tool start --home .", + Example: "exocore-test-tool start --home .", Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { // Start the app manager in a separate goroutine @@ -115,7 +116,7 @@ var prepareCmd = &cobra.Command{ Use: "prepare", Short: "prepare for the batch test", Long: "prepare the test objects, funding, registration and opting-in for the test tool", - Example: "test-tool prepare --home .", + Example: "exocore-test-tool prepare --home .", Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { if err := appManager.Prepare(); err != nil { @@ -131,7 +132,7 @@ var batchTestCmd = &cobra.Command{ Short: "batch test", Long: "batch test the multiple functions, the msgType should be: \r\n" + "depositLST,delegate,undelegate and withdrawLST", - Example: "test-tool batch-test depositLST --home .", + Example: "exocore-test-tool batch-test depositLST --home .", Args: cobra.ExactArgs(1), Run: func(_ *cobra.Command, args []string) { // Start the app manager in a separate goroutine @@ -150,7 +151,7 @@ var QueryHelperRecordCmd = &cobra.Command{ Use: "query-helper-record", Short: "query the helper record info", Long: "query the helper record info, the info includes: current-batch-id", - Example: "test-tool query-helper-record --home .", + Example: "exocore-test-tool query-helper-record --home .", Args: cobra.NoArgs, Run: func(_ *cobra.Command, _ []string) { helperRecord, err := batch.LoadObjectByID[batch.HelperRecord](sqliteDB, batch.SqliteDefaultStartID) @@ -170,7 +171,7 @@ var QueryTestObjectsCmd = &cobra.Command{ Use: "query-test-objects ", Short: "query the specified test objects", Long: "query the specified test objects, the object type is: asset, staker, operator and AVS", - Example: "test-tool query-test-objects staker --home .", + Example: "exocore-test-tool query-test-objects staker --home .", Args: cobra.ExactArgs(1), Run: func(_ *cobra.Command, args []string) { var err error @@ -228,7 +229,7 @@ var QueryTxRecordCmd = &cobra.Command{ "1: pending\r\n" + "2: OnChainButFailed\r\n" + "3: OnChainAndSuccessful", - Example: "test-tool query-tx-record depositLST 1 1 --home .", + Example: "exocore-test-tool query-tx-record depositLST 1 1 --home .", Args: cobra.ExactArgs(3), Run: func(_ *cobra.Command, args []string) { batchID, err := strconv.ParseUint(args[1], 10, 32) diff --git a/go.mod b/go.mod index b417f1c4a..9729eec99 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,8 @@ require ( cosmossdk.io/math v1.2.0 cosmossdk.io/simapp v0.0.0-20230608160436-666c345ad23d cosmossdk.io/tools/rosetta v0.2.1 - github.com/ExocoreNetwork/price-feeder v0.1.15 github.com/BurntSushi/toml v1.3.2 - github.com/ExocoreNetwork/price-feeder v0.0.0-20241009094357-40e58e6f1694 + github.com/ExocoreNetwork/price-feeder v0.1.15 github.com/agiledragon/gomonkey/v2 v2.11.0 github.com/armon/go-metrics v0.4.1 github.com/cometbft/cometbft v0.37.4 diff --git a/precompiles/avs/tx.go b/precompiles/avs/tx.go index b666f777e..94eb5ead4 100644 --- a/precompiles/avs/tx.go +++ b/precompiles/avs/tx.go @@ -49,7 +49,6 @@ func (p Precompile) RegisterAVS( // The AVS registration is done by the calling contract. avsParams.AvsAddress = contract.CallerAddress avsParams.Action = avstypes.RegisterAction - fmt.Println("call RegisterAVS, the avs addr is:", avsParams.AvsAddress) // Finally, update the AVS information in the keeper. err = p.avsKeeper.UpdateAVSInfo(ctx, avsParams) if err != nil { diff --git a/testutil/batch/batch_tx.go b/testutil/batch/batch_tx.go index 5e8360e32..5704c39dc 100644 --- a/testutil/batch/batch_tx.go +++ b/testutil/batch/batch_tx.go @@ -207,8 +207,11 @@ func (m *Manager) EnqueueDelegationTxs(batchID uint, msgType string) error { delegatedAmount, err := m.QueryDelegatedAmount(uint64(asset.ClientChainID), staker.EvmAddress().String(), asset.Address.String(), operator.Address) if msgType == delegation.MethodUndelegate { if err == nil { - // undelegates all amount, the expected check value will be zero - opAmount = delegatedAmount + // Undelegates half of the total amount. The expected check value will also be half. + // The reason for keeping some amount delegated is to ensure there is always a portion + // delegated to the operators, which helps in testing the Exocore chain. + opAmount = delegatedAmount.Quo(sdkmath.NewInt(2)) + expectedCheckValue = delegatedAmount.Sub(opAmount) } // opAmount will be zero if the delegation amount can't be quried, then the undelegation // will be skipped when checking the opAmount. @@ -266,7 +269,7 @@ func (m *Manager) EnqueueDelegationTxs(batchID uint, msgType string) error { return nil } -func (m *Manager) SignAndSendTxs(tx interface{}) error { +func (m *Manager) SignAndSendTxs(tx interface{}) (string, string, error) { // sign and send the transaction var txID string evmTx, ok := tx.(*EvmTxInQueue) @@ -274,18 +277,18 @@ func (m *Manager) SignAndSendTxs(tx interface{}) error { txHash, err := m.SignAndSendEvmTx(evmTx) if err != nil { logger.Error("can't sign and send the evm tx", "txHash", txHash, "err", err) - return err + return txID, evmTx.TxRecord.Type, err } txID = txHash.String() } else { - return xerrors.Errorf("unsupported transaction type: %v", reflect.TypeOf(tx)) + return txID, "", xerrors.Errorf("unsupported transaction type: %v", reflect.TypeOf(tx)) } // todo: address the cosmos transaction for the delegation/undelegation of Exo token. // update the tx record in the local db for future check height, err := m.NodeEVMHTTPClients[DefaultNodeIndex].BlockNumber(m.ctx) if err != nil { - return err + return txID, evmTx.TxRecord.Type, err } evmTx.TxRecord.SendTime = time.Now().String() evmTx.TxRecord.Status = Pending @@ -293,9 +296,9 @@ func (m *Manager) SignAndSendTxs(tx interface{}) error { evmTx.TxRecord.TxHash = txID err = SaveObject[Transaction](m.GetDB(), *evmTx.TxRecord) if err != nil { - return err + return txID, evmTx.TxRecord.Type, err } - return nil + return txID, evmTx.TxRecord.Type, nil } func (m *Manager) TickHandle(handleRate int, handle func() (bool, error)) error { @@ -336,12 +339,12 @@ func (m *Manager) DequeueAndSignSendTxs() error { select { case tx := <-m.TxsQueue: m.QueueSize.Add(-1) - err := m.SignAndSendTxs(tx) + txID, txType, err := m.SignAndSendTxs(tx) if err != nil { logger.Error("DequeueAndSignSendTxs: can't sign and send the tx", "err", err) return false, err } - logger.Info("DequeueAndSignSendTxs, sign and send tx successfully") + logger.Info("DequeueAndSignSendTxs, sign and send tx successfully", "txType", txType, "txID", txID) default: return false, nil } diff --git a/testutil/batch/manager.go b/testutil/batch/manager.go index 9413b8781..ffd8af308 100644 --- a/testutil/batch/manager.go +++ b/testutil/batch/manager.go @@ -595,8 +595,11 @@ func (m *Manager) Start() error { if err := m.EnqueueAndCheckTxsInBatch(assets.MethodWithdrawLST); err != nil { return xerrors.Errorf("withdrawal batch failed: %w", err) } - logger.Info("Start: finish enqueuing and checking all withdrawal txs") + logger.Info("Start: finish enqueuing and checking all withdrawal txs", "EachTestInterval", m.config.EachTestInterval) time.Sleep(time.Duration(m.config.EachTestInterval) * time.Second) + if err := m.FundAndCheckStakers(); err != nil { + return xerrors.Errorf("failed to fund and check the stakers %w", err) + } return nil }) diff --git a/testutil/batch/prepare.go b/testutil/batch/prepare.go index 16a7d75b6..6086fb724 100644 --- a/testutil/batch/prepare.go +++ b/testutil/batch/prepare.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" "strings" + "time" dogfoodtypes "github.com/ExocoreNetwork/exocore/x/dogfood/types" "github.com/ethereum/go-ethereum/common" @@ -85,6 +86,20 @@ func (m *Manager) LoadSequence(addr common.Address) (uint64, error) { return 0, xerrors.Errorf("can't load the sequence from the sync map, addr:%s", addr) } +func (m *Manager) FundAndCheckStakers() error { + logger.Info("start funding stakers") + err := FundingObjects(m, &Staker{}, m.config.StakerExoAmount) + if err != nil { + return xerrors.Errorf("can't fund stakers,err:%w", err) + } + time.Sleep(time.Duration(m.config.BatchTxsCheckInterval) * time.Second) + err = CheckObjectsBalance(m, &Staker{}, m.config.StakerExoAmount) + if err != nil { + return err + } + return nil +} + // Funding : send Exo token to the test objects, which can be used for the tx fee in the next tests. // all Exo token is sent from the faucet sk. func (m *Manager) Funding() error { diff --git a/testutil/batch/send_txs.go b/testutil/batch/send_txs.go index c6cbb8111..ca31220cf 100644 --- a/testutil/batch/send_txs.go +++ b/testutil/batch/send_txs.go @@ -45,7 +45,7 @@ func WaitForEvmTxReceipt(client *ethclient.Client, txHash common.Hash, waitDurat } // Log a message indicating the receipt is not yet available - logger.Info("can't get the receipt of EVM transaction, continue waiting", "err", err) + logger.Info("can't get the receipt of EVM transaction, continue waiting", "waitDuration", waitDuration, "err", err) // Wait for the specified duration before retrying time.Sleep(waitDuration) @@ -103,7 +103,7 @@ func (m *Manager) SignAndSendEvmTx(txInfo *EvmTxInQueue) (common.Hash, error) { GasPrice: gasPrice, Data: msg.Data, }) - logger.Info("SignAndSendEvmTx", "from", txInfo.From, "to", retTx.To(), + logger.Info("SignAndSendEvmTx", "from", strings.ToLower(txInfo.From.String()), "to", retTx.To(), "nonce", retTx.Nonce(), "value", retTx.Value(), "gasPrice", retTx.GasPrice(), "gas", retTx.Gas(), "dataLength", len(retTx.Data()), "time", time.Now().String()) signTx, err := types.SignTx(retTx, m.EthSigner, sk) diff --git a/testutil/batch/tx_check.go b/testutil/batch/tx_check.go index 796462c61..6e9b09dfa 100644 --- a/testutil/batch/tx_check.go +++ b/testutil/batch/tx_check.go @@ -124,12 +124,12 @@ func (m *Manager) PrecompileTxOnChainCheck(batchID uint, msgType string) error { // DepositWithdrawLSTCheck : By default, we require each batch to follow the order of // deposits -> delegations -> undelegations -> withdrawals for batch testing. -// During delegation, only half of the deposit amount is used, with the other half -// reserved for withdrawals. Therefore, when checking: -// 1. After the deposits are completed, the totalDepositAmount should be equal to: -// DefaultDepositAmount + batchID * (DefaultDepositAmount / 2) where batchID starts from 0. -// 2. After the withdrawal tests are completed, the totalDepositAmount should be: -// (DefaultDepositAmount / 2) * (batchID + 1). +// `DefaultDepositAmount` is used as the amount for batch deposit tests. +// Therefore, we only need to verify if `StakerAssetInfo.total_deposit_amount` +// has increased by `DefaultDepositAmount`. +// For batch withdrawal tests, we will withdraw the total withdrawable amount +// and record the opAmount. Thus, when checking withdrawal test transactions, +// we only need to verify if `StakerAssetInfo.total_deposit_amount` has decreased by opAmount. func (m *Manager) DepositWithdrawLSTCheck(batchID uint, msgType string) error { stakerOpFunc := func(_ uint, _ int64, staker *Staker) error { assetOpFunc := func(_ uint, _ int64, asset *Asset) error { @@ -205,7 +205,10 @@ func (m *Manager) EvmDelegationCheck(batchID uint, msgType string) error { operatorOpFunc := func(_ uint, _ int64, operator *Operator) error { delegatedAmount, err := m.QueryDelegatedAmount(uint64(asset.ClientChainID), staker.EvmAddress().String(), asset.Address.String(), operator.Address) if err != nil { - return err + logger.Error("EvmDelegationCheck, error occurs when querying the delegated amount", + "staker", staker.Name, "asset", asset.Name, "operator", operator.Name, "err", err) + // return nil to continue the next check + return nil } var transaction Transaction err = m.GetDB(). diff --git a/testutil/batch/type.go b/testutil/batch/type.go index 3ac2b2037..24cd6f8e7 100644 --- a/testutil/batch/type.go +++ b/testutil/batch/type.go @@ -58,11 +58,18 @@ type TestToolConfig struct { // 3. undelegation -> wait for transaction check(only check whether the transaction is on chain) // 4. withdraw -> wait for transaction check // TxNumberPerSec indicates the number of transactions sent to Exocore chain per second. + // Currently, since we are using the `NoOpMempool`, it does not support sending transactions + // with different nonces from the same sender at a very high rate. Therefore, it is recommended + // to set this parameter to 1 for now. In the future,consider modifying this parameter configuration + // to support slower transaction sending rates. TxNumberPerSec int `mapstructure:"tx-number-per-second" toml:"tx-number-per-second"` TxChecksPerSecond int `mapstructure:"tx-checks-per-second" toml:"tx-checks-per-second"` // EachTestInterval indicates the interval of single test. The staker will start another test // After this interval. EachTestInterval int64 `mapstructure:"each-test-interval" toml:"each-test-interval"` + // configure this parameter to test the voting power and reward distribution, the delegations will soon be + // undelegated in the next batch test. + IntervalAfterDelegations int64 `mapstructure:"interval-after-delegations" toml:"interval-after-delegations"` // SingleTxCheckInterval is an interval waiting to check the transaction. SingleTxCheckInterval int64 `mapstructure:"single-tx-check-interval" toml:"single-tx-check-interval"` // TxWaitExpiration defines the timeout period for checking a transaction. @@ -84,24 +91,25 @@ var DefaultTestToolConfig = TestToolConfig{ AVSNumber: 2, AssetNumber: 5, // this private key is from the local_node.sh - FaucetSk: "D196DCA836F8AC2FFF45B3C9F0113825CCBB33FA1B39737B948503B263ED75AE", - StakerExoAmount: 100, - OperatorExoAmount: 10, - AVSExoAmount: 10, - ChainValidatorNumber: 1, - ChainID: "exocoretestnet_233-1", - DefaultClientChainID: 101, - NodesRPC: []string{"http://127.0.0.1:26657"}, - NodesEVMRPCHTTP: []string{"http://127.0.0.1:8545"}, - NodesEVMRPCWebsocket: []string{"ws://127.0.0.1:8546"}, - TxNumberPerSec: 10, - TxChecksPerSecond: 10, - EachTestInterval: 600, // 10 minutes - SingleTxCheckInterval: 6, // 6 seconds - TxWaitExpiration: 60, // 1 minutes - BatchTxsCheckInterval: 120, // 2 minutes - AddrNumberInMultiSend: 10, - TxsQueueBufferSize: 100, + FaucetSk: "D196DCA836F8AC2FFF45B3C9F0113825CCBB33FA1B39737B948503B263ED75AE", + StakerExoAmount: 100, + OperatorExoAmount: 10, + AVSExoAmount: 10, + ChainValidatorNumber: 1, + ChainID: "exocoretestnet_233-1", + DefaultClientChainID: 101, + NodesRPC: []string{"http://127.0.0.1:26657"}, + NodesEVMRPCHTTP: []string{"http://127.0.0.1:8545"}, + NodesEVMRPCWebsocket: []string{"ws://127.0.0.1:8546"}, + TxNumberPerSec: 1, + TxChecksPerSecond: 10, + EachTestInterval: 10 * 60, // 10 minutes + IntervalAfterDelegations: 3 * 60 * 60, // 3 hours + SingleTxCheckInterval: 6, // 6 seconds + TxWaitExpiration: 60, // 1 minutes + BatchTxsCheckInterval: 2 * 60, // 2 minutes + AddrNumberInMultiSend: 10, + TxsQueueBufferSize: 100, } type HelperRecord struct {