diff --git a/Dockerfile-localnet b/Dockerfile-localnet index 793b555c22..65ca4188b4 100644 --- a/Dockerfile-localnet +++ b/Dockerfile-localnet @@ -11,21 +11,12 @@ RUN ssh-keygen -b 2048 -t rsa -f /root/.ssh/localtest.pem -q -N "" WORKDIR /go/delivery/zeta-node COPY go.mod . COPY go.sum . -#RUN --mount=type=cache,target=/root/.cache/go-build \ -# go mod download RUN go mod download COPY . . -#RUN --mount=type=cache,target=/root/.cache/go-build \ -# make install -#RUN --mount=type=cache,target=/root/.cache/go-build \ -# make install-zetae2e RUN make install RUN make install-zetae2e -# -#FROM golang:1.20-alpine -#RUN apk --no-cache add openssh jq tmux vim curl bash RUN ssh-keygen -A WORKDIR /root diff --git a/changelog.md b/changelog.md index bc3070f7e1..4dde927265 100644 --- a/changelog.md +++ b/changelog.md @@ -33,6 +33,7 @@ * [1755](https://github.com/zeta-chain/node/issues/1755) - use evm JSON RPC for inbound tx (including blob tx) observation. * [1815](https://github.com/zeta-chain/node/pull/1815) - add authority module for authorized actions * [1884](https://github.com/zeta-chain/node/pull/1884) - added zetatool cmd, added subcommand to filter deposits +* [1942](https://github.com/zeta-chain/node/pull/1982) - support Bitcoin P2TR, P2WSH, P2SH, P2PKH addresses * [1935](https://github.com/zeta-chain/node/pull/1935) - add an operational authority group * [1954](https://github.com/zeta-chain/node/pull/1954) - add metric for concurrent keysigns @@ -50,6 +51,7 @@ * [1805](https://github.com/zeta-chain/node/pull/1805) - add admin and performance test and fix upgrade test * [1879](https://github.com/zeta-chain/node/pull/1879) - full coverage for messages in types packages * [1899](https://github.com/zeta-chain/node/pull/1899) - add empty test files so packages are included in coverage +* [1900](https://github.com/zeta-chain/node/pull/1900) - add testing for external chain migration * [1903](https://github.com/zeta-chain/node/pull/1903) - common package tests * [1961](https://github.com/zeta-chain/node/pull/1961) - improve observer module coverage * [1967](https://github.com/zeta-chain/node/pull/1967) - improve crosschain module coverage @@ -59,6 +61,7 @@ * [1861](https://github.com/zeta-chain/node/pull/1861) - fix `ObserverSlashAmount` invalid read * [1880](https://github.com/zeta-chain/node/issues/1880) - lower the gas price multiplier for EVM chains. +* [1883](https://github.com/zeta-chain/node/issues/1883) - zetaclient should check 'IsSupported' flag to pause/unpause a specific chain * [1633](https://github.com/zeta-chain/node/issues/1633) - zetaclient should be able to pick up new connector and erc20Custody addresses * [1944](https://github.com/zeta-chain/node/pull/1944) - fix evm signer unit tests @@ -175,6 +178,7 @@ * [1675](https://github.com/zeta-chain/node/issues/1675) - use chain param ConfirmationCount for bitcoin confirmation ## Chores + * [1694](https://github.com/zeta-chain/node/pull/1694) - remove standalone network, use require testing package for the entire node folder ## Version: v12.1.0 @@ -187,6 +191,7 @@ * [1658](https://github.com/zeta-chain/node/pull/1658) - modify emission distribution to use fixed block rewards ### Fixes + * [1535](https://github.com/zeta-chain/node/issues/1535) - Avoid voting on wrong ballots due to false blockNumber in EVM tx receipt * [1588](https://github.com/zeta-chain/node/pull/1588) - fix chain params comparison logic * [1650](https://github.com/zeta-chain/node/pull/1605) - exempt (discounted) *system txs* from min gas price check and gas fee deduction diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 1ea6eb8c5b..a9fc63e1f8 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -67,9 +67,6 @@ func CreateSignerMap( loggers.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } - if !evmChainParams.IsSupported { - continue - } mpiAddress := ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress) erc20CustodyAddress := ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress) signer, err := evm.NewEVMSigner( @@ -117,14 +114,11 @@ func CreateChainClientMap( if evmConfig.Chain.IsZetaChain() { continue } - evmChainParams, found := appContext.ZetaCoreContext().GetEVMChainParams(evmConfig.Chain.ChainId) + _, found := appContext.ZetaCoreContext().GetEVMChainParams(evmConfig.Chain.ChainId) if !found { loggers.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } - if !evmChainParams.IsSupported { - continue - } co, err := evm.NewEVMChainClient(appContext, bridge, tss, dbpath, loggers, evmConfig, ts) if err != nil { loggers.Std.Error().Err(err).Msgf("NewEVMChainClient error for chain %s", evmConfig.Chain.String()) diff --git a/cmd/zetae2e/local/accounts.go b/cmd/zetae2e/local/accounts.go index f179e02859..bcdd741e7f 100644 --- a/cmd/zetae2e/local/accounts.go +++ b/cmd/zetae2e/local/accounts.go @@ -30,8 +30,9 @@ var ( UserMiscPrivateKey = "853c0945b8035a501b1161df65a17a0a20fc848bda8975a8b4e9222cc6f84cd4" // #nosec G101 - used for testing // UserAdminAddress is the address of the account for testing admin function features - UserAdminAddress = ethcommon.HexToAddress("0xcC8487562AAc220ea4406196Ee902C7c076966af") - UserAdminPrivateKey = "95409f1f0e974871cc26ba98ffd31f613aa1287d40c0aea6a87475fc3521d083" // #nosec G101 - used for testing + // NOTE: this is the default account using Anvil + UserAdminAddress = ethcommon.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266") + UserAdminPrivateKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" // #nosec G101 - used for testing FungibleAdminMnemonic = "snow grace federal cupboard arrive fancy gym lady uniform rotate exercise either leave alien grass" // #nosec G101 - used for testing ) diff --git a/cmd/zetae2e/local/admin.go b/cmd/zetae2e/local/admin.go index 50c8b2b8f7..fa2df0b75c 100644 --- a/cmd/zetae2e/local/admin.go +++ b/cmd/zetae2e/local/admin.go @@ -38,7 +38,7 @@ func adminTestRoutine( deployerRunner, UserAdminAddress, UserAdminPrivateKey, - runner.NewLogger(verbose, color.FgGreen, "admin"), + runner.NewLogger(verbose, color.FgHiGreen, "admin"), ) if err != nil { return err @@ -48,7 +48,8 @@ func adminTestRoutine( startTime := time.Now() // funding the account - txZetaSend := deployerRunner.SendZetaOnEvm(UserAdminAddress, 1000) + // we transfer around the total supply of Zeta to the admin for the chain migration test + txZetaSend := deployerRunner.SendZetaOnEvm(UserAdminAddress, 20_500_000_000) txERC20Send := deployerRunner.SendERC20OnEvm(UserAdminAddress, 1000) adminRunner.WaitForTxReceiptOnEvm(txZetaSend) adminRunner.WaitForTxReceiptOnEvm(txERC20Send) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 6aef91156a..d3ddca09a7 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -188,7 +188,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipSetup { logger.Print("⚙️ setting up networks") startTime := time.Now() - deployerRunner.SetupEVM(contractsDeployed) + deployerRunner.SetupEVM(contractsDeployed, true) deployerRunner.SetZEVMContracts() // NOTE: this method return an error so we handle it and panic if it occurs unlike other method that panics directly @@ -250,7 +250,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestZetaDepositRestrictedName, } bitcoinTests := []string{ - e2etests.TestBitcoinWithdrawName, + e2etests.TestBitcoinWithdrawSegWitName, + e2etests.TestBitcoinWithdrawTaprootName, + e2etests.TestBitcoinWithdrawLegacyName, + e2etests.TestBitcoinWithdrawP2SHName, + e2etests.TestBitcoinWithdrawP2WSHName, e2etests.TestBitcoinWithdrawInvalidAddressName, e2etests.TestZetaWithdrawBTCRevertName, e2etests.TestCrosschainSwapName, @@ -285,6 +289,12 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestPauseZRC20Name, e2etests.TestUpdateBytecodeName, e2etests.TestDepositEtherLiquidityCapName, + + // TestMigrateChainSupportName tests EVM chain migration. Currently this test doesn't work with Anvil because pre-EIP1559 txs are not supported + // See issue below for details + // TODO: renenable this test as per the issue below + // https://github.com/zeta-chain/node/issues/1980 + // e2etests.TestMigrateChainSupportName, )) } if testPerformance { diff --git a/cmd/zetae2e/stress.go b/cmd/zetae2e/stress.go index b85d0e78d7..793b9ec3c4 100644 --- a/cmd/zetae2e/stress.go +++ b/cmd/zetae2e/stress.go @@ -155,7 +155,7 @@ func StressTest(cmd *cobra.Command, _ []string) { panic(err) } - e2eTest.SetupEVM(stressTestArgs.contractsDeployed) + e2eTest.SetupEVM(stressTestArgs.contractsDeployed, true) // If stress test is running on local docker environment if stressTestArgs.network == "LOCAL" { diff --git a/contrib/localnet/anvil/Dockerfile b/contrib/localnet/anvil/Dockerfile new file mode 100644 index 0000000000..5426174fdf --- /dev/null +++ b/contrib/localnet/anvil/Dockerfile @@ -0,0 +1,11 @@ +# This Dockerfile is used to build a Docker image for Anvil, a localnet for testing purposes. +# Currently we directly set the chain ID to 11155111 and expose the default Anvil port specifically for the chain migration test. + +# Start from the latest Rust image as Anvil is built with Rust +FROM ghcr.io/foundry-rs/foundry:latest + +# Expose the default Anvil port +EXPOSE 8545 + +# Run Anvil with specified chain ID and a prefunded account when the container starts +ENTRYPOINT ["anvil", "--host", "0.0.0.0", "--chain-id", "11155111"] \ No newline at end of file diff --git a/contrib/localnet/docker-compose-admin.yml b/contrib/localnet/docker-compose-admin.yml index 46b24bfb24..23503cf305 100644 --- a/contrib/localnet/docker-compose-admin.yml +++ b/contrib/localnet/docker-compose-admin.yml @@ -2,8 +2,25 @@ version: "3" # This docker-compose file overrides the orchestrator service to specify the flag to test the admin functions # and skip the regular tests +# it also adds another local Ethereum network to test EVM chain migration and use the additional-evm flag services: orchestrator: entrypoint: ["/work/start-zetae2e.sh", "local --skip-regular --test-admin"] + eth2: + build: + context: ./anvil + container_name: eth2 + hostname: eth2 + ports: + - "8546:8545" + networks: + mynetwork: + ipv4_address: 172.20.0.102 + + zetaclient0: + entrypoint: [ "/root/start-zetaclientd.sh", "additional-evm" ] + + zetaclient1: + entrypoint: [ "/root/start-zetaclientd.sh", "additional-evm" ] \ No newline at end of file diff --git a/contrib/localnet/orchestrator/Dockerfile b/contrib/localnet/orchestrator/Dockerfile index e159d8ba84..e268738ef2 100644 --- a/contrib/localnet/orchestrator/Dockerfile +++ b/contrib/localnet/orchestrator/Dockerfile @@ -13,6 +13,7 @@ COPY --from=zeta /root/.ssh/localtest.pem /root/.ssh/localtest.pem COPY contrib/localnet/orchestrator/start-zetae2e.sh /work/ COPY contrib/localnet/orchestrator/restart-zetaclientd.sh /work/ +COPY contrib/localnet/orchestrator/restart-zetaclientd-at-upgrade.sh /work/ RUN chmod +x /work/*.sh ENV GOPATH /go diff --git a/contrib/localnet/orchestrator/Dockerfile.fastbuild b/contrib/localnet/orchestrator/Dockerfile.fastbuild index 64d9e9c87e..36cf07b8cc 100644 --- a/contrib/localnet/orchestrator/Dockerfile.fastbuild +++ b/contrib/localnet/orchestrator/Dockerfile.fastbuild @@ -13,6 +13,7 @@ COPY --from=zeta /root/.ssh/localtest.pem /root/.ssh/localtest.pem COPY contrib/localnet/orchestrator/start-zetae2e.sh /work/ COPY contrib/localnet/orchestrator/restart-zetaclientd.sh /work/ +COPY contrib/localnet/orchestrator/restart-zetaclientd-at-upgrade.sh /work/ RUN chmod +x /work/*.sh COPY --from=zeta /usr/local/bin/zetae2e /usr/local/bin/ diff --git a/contrib/localnet/orchestrator/restart-zetaclientd-at-upgrade.sh b/contrib/localnet/orchestrator/restart-zetaclientd-at-upgrade.sh new file mode 100644 index 0000000000..7911deca01 --- /dev/null +++ b/contrib/localnet/orchestrator/restart-zetaclientd-at-upgrade.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# This script is used to restart zetaclientd after an upgrade +# It waits for the upgrade height to be reached and then restarts the zetaclientd on all nodes in the network +# It interacts with the network using the zetaclientd binary + +clibuilder() +{ + echo "" + echo "Usage: $0 -u UPGRADE_HEIGHT" + echo -e "\t-u Height of upgrade, should match governance proposal" + echo -e "\t-n Number of clients in the network" + exit 1 # Exit script after printing help +} + +while getopts "u:n:" opt +do + case "$opt" in + u ) UPGRADE_HEIGHT="$OPTARG" ;; + n ) NUM_OF_NODES="$OPTARG" ;; + ? ) clibuilder ;; # Print cliBuilder in case parameter is non-existent + esac +done + +# generate client list +START=0 +END=$((NUM_OF_NODES-1)) +CLIENT_LIST=() +for i in $(eval echo "{$START..$END}") +do + CLIENT_LIST+=("zetaclient$i") +done + +echo "$UPGRADE_HEIGHT" + +CURRENT_HEIGHT=0 + +while [[ $CURRENT_HEIGHT -lt $UPGRADE_HEIGHT ]] +do + CURRENT_HEIGHT=$(curl -s zetacore0:26657/status | jq '.result.sync_info.latest_block_height' | tr -d '"') + echo current height is "$CURRENT_HEIGHT", waiting for "$UPGRADE_HEIGHT" + sleep 5 +done + +echo upgrade height reached, restarting zetaclients + +for NODE in "${CLIENT_LIST[@]}"; do + ssh -o "StrictHostKeyChecking no" "$NODE" -i ~/.ssh/localtest.pem killall zetaclientd + ssh -o "StrictHostKeyChecking no" "$NODE" -i ~/.ssh/localtest.pem "$GOPATH/bin/new/zetaclientd start < /root/password.file > $HOME/zetaclient.log 2>&1 &" +done diff --git a/contrib/localnet/orchestrator/restart-zetaclientd.sh b/contrib/localnet/orchestrator/restart-zetaclientd.sh index 7911deca01..6071b07570 100644 --- a/contrib/localnet/orchestrator/restart-zetaclientd.sh +++ b/contrib/localnet/orchestrator/restart-zetaclientd.sh @@ -1,50 +1,11 @@ #!/bin/bash -# This script is used to restart zetaclientd after an upgrade -# It waits for the upgrade height to be reached and then restarts the zetaclientd on all nodes in the network -# It interacts with the network using the zetaclientd binary +# This script immediately restarts the zetaclientd on zetaclient0 and zetaclient1 containers in the network -clibuilder() -{ - echo "" - echo "Usage: $0 -u UPGRADE_HEIGHT" - echo -e "\t-u Height of upgrade, should match governance proposal" - echo -e "\t-n Number of clients in the network" - exit 1 # Exit script after printing help -} +echo restarting zetaclients -while getopts "u:n:" opt -do - case "$opt" in - u ) UPGRADE_HEIGHT="$OPTARG" ;; - n ) NUM_OF_NODES="$OPTARG" ;; - ? ) clibuilder ;; # Print cliBuilder in case parameter is non-existent - esac -done +ssh -o "StrictHostKeyChecking no" "zetaclient0" -i ~/.ssh/localtest.pem killall zetaclientd +ssh -o "StrictHostKeyChecking no" "zetaclient1" -i ~/.ssh/localtest.pem killall zetaclientd +ssh -o "StrictHostKeyChecking no" "zetaclient0" -i ~/.ssh/localtest.pem "/usr/local/bin/zetaclientd start < /root/password.file > $HOME/zetaclient.log 2>&1 &" +ssh -o "StrictHostKeyChecking no" "zetaclient1" -i ~/.ssh/localtest.pem "/usr/local/bin/zetaclientd start < /root/password.file > $HOME/zetaclient.log 2>&1 &" -# generate client list -START=0 -END=$((NUM_OF_NODES-1)) -CLIENT_LIST=() -for i in $(eval echo "{$START..$END}") -do - CLIENT_LIST+=("zetaclient$i") -done - -echo "$UPGRADE_HEIGHT" - -CURRENT_HEIGHT=0 - -while [[ $CURRENT_HEIGHT -lt $UPGRADE_HEIGHT ]] -do - CURRENT_HEIGHT=$(curl -s zetacore0:26657/status | jq '.result.sync_info.latest_block_height' | tr -d '"') - echo current height is "$CURRENT_HEIGHT", waiting for "$UPGRADE_HEIGHT" - sleep 5 -done - -echo upgrade height reached, restarting zetaclients - -for NODE in "${CLIENT_LIST[@]}"; do - ssh -o "StrictHostKeyChecking no" "$NODE" -i ~/.ssh/localtest.pem killall zetaclientd - ssh -o "StrictHostKeyChecking no" "$NODE" -i ~/.ssh/localtest.pem "$GOPATH/bin/new/zetaclientd start < /root/password.file > $HOME/zetaclient.log 2>&1 &" -done diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 6f75ae7cc5..a893c689ee 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -37,9 +37,9 @@ geth --exec 'eth.sendTransaction({from: eth.coinbase, to: "0x8D47Db7390AC4D3D449 echo "funding deployer address 0x90126d02E41c9eB2a10cfc43aAb3BD3460523Cdf with 100 Ether" geth --exec 'eth.sendTransaction({from: eth.coinbase, to: "0x90126d02E41c9eB2a10cfc43aAb3BD3460523Cdf", value: web3.toWei(100,"ether")})' attach http://eth:8545 -# unlock advanced erc20 tests accounts -echo "funding deployer address 0xcC8487562AAc220ea4406196Ee902C7c076966af with 100 Ether" -geth --exec 'eth.sendTransaction({from: eth.coinbase, to: "0xcC8487562AAc220ea4406196Ee902C7c076966af", value: web3.toWei(100,"ether")})' attach http://eth:8545 +# unlock admin erc20 tests accounts +echo "funding deployer address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 with 100 Ether" +geth --exec 'eth.sendTransaction({from: eth.coinbase, to: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", value: web3.toWei(100,"ether")})' attach http://eth:8545 # unlock the TSS account echo "funding TSS address 0xF421292cb0d3c97b90EEEADfcD660B893592c6A2 with 100 Ether" @@ -74,7 +74,7 @@ if [ "$OPTION" == "upgrade" ]; then echo "E2E setup passed, waiting for upgrade height..." # Restart zetaclients at upgrade height - /work/restart-zetaclientd.sh -u "$UPGRADE_HEIGHT" -n 2 + /work/restart-zetaclientd-at-upgrade.sh -u "$UPGRADE_HEIGHT" -n 2 echo "waiting 10 seconds for node to restart..." diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index 761a58098d..a43e8e120f 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -9,6 +9,11 @@ HOSTNAME=$(hostname) OPTION=$1 +# sepolia is used in chain migration tests, this functions set the sepolia endpoint in the zetaclient_config.json +set_sepolia_endpoint() { + jq '.EVMChainConfigs."11155111".Endpoint = "http://eth2:8545"' /root/.zetacored/config/zetaclient_config.json > tmp.json && mv tmp.json /root/.zetacored/config/zetaclient_config.json +} + # read HOTKEY_BACKEND env var for hotkey keyring backend and set default to test BACKEND="test" if [ "$HOTKEY_BACKEND" == "file" ]; then @@ -30,6 +35,14 @@ then rm ~/.tss/* MYIP=$(/sbin/ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1) zetaclientd init --zetacore-url zetacore0 --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --keyring-backend "$BACKEND" + + # check if the option is additional-evm + # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) + # in /root/.zetacored/config/zetaclient_config.json + if [ "$OPTION" == "additional-evm" ]; then + set_sepolia_endpoint + fi + zetaclientd start < /root/password.file else num=$(echo $HOSTNAME | tr -dc '0-9') @@ -42,11 +55,20 @@ else done rm ~/.tss/* zetaclientd init --peer /ip4/172.20.0.21/tcp/6668/p2p/"$SEED" --zetacore-url "$node" --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --log-level 1 --keyring-backend "$BACKEND" + + # check if the option is additional-evm + # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) + # in /root/.zetacored/config/zetaclient_config.json + if [ "$OPTION" == "additional-evm" ]; then + set_sepolia_endpoint + fi + zetaclientd start < /root/password.file fi +# check if the option is background +# in this case, we tail the zetaclientd log file if [ "$OPTION" == "background" ]; then sleep 3 tail -f $HOME/zetaclient.log fi - diff --git a/docs/spec/observer/messages.md b/docs/spec/observer/messages.md index 82c6423757..a73d3a6134 100644 --- a/docs/spec/observer/messages.md +++ b/docs/spec/observer/messages.md @@ -111,7 +111,6 @@ message MsgAddBlockHeader { ## MsgResetChainNonces ResetChainNonces handles resetting chain nonces -Authorized: admin policy group 2 (admin update) ```proto message MsgResetChainNonces { diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 70bdc6de94..404616535f 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -15,23 +15,25 @@ const ( TestZetaWithdrawBTCRevertName = "zeta_withdraw_btc_revert" // #nosec G101 - not a hardcoded password TestMessagePassingName = "message_passing" TestZRC20SwapName = "zrc20_swap" - TestBitcoinWithdrawName = "bitcoin_withdraw" + TestBitcoinWithdrawSegWitName = "bitcoin_withdraw_segwit" + TestBitcoinWithdrawTaprootName = "bitcoin_withdraw_taproot" + TestBitcoinWithdrawLegacyName = "bitcoin_withdraw_legacy" + TestBitcoinWithdrawP2WSHName = "bitcoin_withdraw_p2wsh" + TestBitcoinWithdrawP2SHName = "bitcoin_withdraw_p2sh" TestBitcoinWithdrawInvalidAddressName = "bitcoin_withdraw_invalid" TestBitcoinWithdrawRestrictedName = "bitcoin_withdraw_restricted" TestCrosschainSwapName = "crosschain_swap" TestMessagePassingRevertFailName = "message_passing_revert_fail" TestMessagePassingRevertSuccessName = "message_passing_revert_success" - TestPauseZRC20Name = "pause_zrc20" TestERC20DepositAndCallRefundName = "erc20_deposit_and_call_refund" - TestUpdateBytecodeName = "update_bytecode" TestEtherDepositAndCallName = "eth_deposit_and_call" TestDepositEtherLiquidityCapName = "deposit_eth_liquidity_cap" TestMyTestName = "my_test" TestERC20WithdrawName = "erc20_withdraw" TestERC20DepositName = "erc20_deposit" - // #nosec G101: Potential hardcoded credentials (gosec), not a credential - TestERC20DepositRestrictedName = "erc20_deposit_restricted" + + TestERC20DepositRestrictedName = "erc20_deposit_restricted" // #nosec G101: Potential hardcoded credentials (gosec), not a credential TestEtherDepositName = "eth_deposit" TestEtherWithdrawName = "eth_withdraw" TestEtherWithdrawRestrictedName = "eth_withdraw_restricted" @@ -45,6 +47,11 @@ const ( TestStressBTCWithdrawName = "stress_btc_withdraw" TestStressEtherDepositName = "stress_eth_deposit" TestStressBTCDepositName = "stress_btc_deposit" + + // Admin test + TestMigrateChainSupportName = "migrate_chain_support" + TestPauseZRC20Name = "pause_zrc20" + TestUpdateBytecodeName = "update_bytecode" ) // AllE2ETests is an ordered list of all e2e tests @@ -130,12 +137,49 @@ var AllE2ETests = []runner.E2ETest{ TestZRC20Swap, ), runner.NewE2ETest( - TestBitcoinWithdrawName, - "withdraw BTC from ZEVM", + TestBitcoinWithdrawSegWitName, + "withdraw BTC from ZEVM to a SegWit address", []runner.ArgDefinition{ - runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.01"}, + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, }, - TestBitcoinWithdraw, + TestBitcoinWithdrawSegWit, + ), + runner.NewE2ETest( + TestBitcoinWithdrawTaprootName, + "withdraw BTC from ZEVM to a Taproot address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawTaproot, + ), + runner.NewE2ETest( + TestBitcoinWithdrawLegacyName, + "withdraw BTC from ZEVM to a legacy address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawLegacy, + ), + runner.NewE2ETest( + TestBitcoinWithdrawP2WSHName, + "withdraw BTC from ZEVM to a P2WSH address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawP2WSH, + ), + runner.NewE2ETest( + TestBitcoinWithdrawP2SHName, + "withdraw BTC from ZEVM to a P2SH address", + []runner.ArgDefinition{ + runner.ArgDefinition{Description: "receiver address", DefaultValue: ""}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, + }, + TestBitcoinWithdrawP2SH, ), runner.NewE2ETest( TestBitcoinWithdrawInvalidAddressName, @@ -311,8 +355,14 @@ var AllE2ETests = []runner.E2ETest{ TestBitcoinWithdrawRestrictedName, "withdraw Bitcoin from ZEVM to restricted address", []runner.ArgDefinition{ - runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.01"}, + runner.ArgDefinition{Description: "amount in btc", DefaultValue: "0.001"}, }, TestBitcoinWithdrawRestricted, ), + runner.NewE2ETest( + TestMigrateChainSupportName, + "migrate the evm chain from goerli to sepolia", + []runner.ArgDefinition{}, + TestMigrateChainSupport, + ), } diff --git a/e2e/e2etests/test_bitcoin_withdraw.go b/e2e/e2etests/test_bitcoin_withdraw.go index 3920b7b3ce..cfb43eb72d 100644 --- a/e2e/e2etests/test_bitcoin_withdraw.go +++ b/e2e/e2etests/test_bitcoin_withdraw.go @@ -11,28 +11,98 @@ import ( "github.com/zeta-chain/zetacore/e2e/runner" "github.com/zeta-chain/zetacore/e2e/utils" "github.com/zeta-chain/zetacore/pkg/chains" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/testutils" ) -func TestBitcoinWithdraw(r *runner.E2ERunner, args []string) { - if len(args) != 1 { - panic("TestBitcoinWithdraw requires exactly one argument for the amount.") +func TestBitcoinWithdrawSegWit(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawSegWit requires two arguments: [receiver, amount]") } + r.SetBtcAddress(r.Name, false) - withdrawalAmount, err := strconv.ParseFloat(args[0], 64) - if err != nil { - panic("Invalid withdrawal amount specified for TestBitcoinWithdraw.") + // parse arguments + defaultReceiver := r.BTCDeployerAddress.EncodeAddress() + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressWitnessPubKeyHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawSegWit.") } - withdrawalAmountSat, err := btcutil.NewAmount(withdrawalAmount) - if err != nil { - panic(err) + withdrawBTCZRC20(r, receiver, amount) +} + +func TestBitcoinWithdrawTaproot(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawTaproot requires two arguments: [receiver, amount]") } - amount := big.NewInt(int64(withdrawalAmountSat)) + r.SetBtcAddress(r.Name, false) + // parse arguments and withdraw BTC + defaultReceiver := "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*chains.AddressTaproot) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawTaproot.") + } + + withdrawBTCZRC20(r, receiver, amount) +} + +func TestBitcoinWithdrawLegacy(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawLegacy requires two arguments: [receiver, amount]") + } r.SetBtcAddress(r.Name, false) - WithdrawBitcoin(r, amount) + // parse arguments and withdraw BTC + defaultReceiver := "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressPubKeyHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawLegacy.") + } + + withdrawBTCZRC20(r, receiver, amount) +} + +func TestBitcoinWithdrawP2WSH(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawP2WSH requires two arguments: [receiver, amount]") + } + r.SetBtcAddress(r.Name, false) + + // parse arguments and withdraw BTC + defaultReceiver := "bcrt1qm9mzhyky4w853ft2ms6dtqdyyu3z2tmrq8jg8xglhyuv0dsxzmgs2f0sqy" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressWitnessScriptHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawP2WSH.") + } + + withdrawBTCZRC20(r, receiver, amount) +} + +func TestBitcoinWithdrawP2SH(r *runner.E2ERunner, args []string) { + // check length of arguments + if len(args) != 2 { + panic("TestBitcoinWithdrawP2SH requires two arguments: [receiver, amount]") + } + r.SetBtcAddress(r.Name, false) + + // parse arguments and withdraw BTC + defaultReceiver := "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd" + receiver, amount := parseBitcoinWithdrawArgs(args, defaultReceiver) + _, ok := receiver.(*btcutil.AddressScriptHash) + if !ok { + panic("Invalid receiver address specified for TestBitcoinWithdrawP2SH.") + } + + withdrawBTCZRC20(r, receiver, amount) } func TestBitcoinWithdrawRestricted(r *runner.E2ERunner, args []string) { @@ -53,7 +123,37 @@ func TestBitcoinWithdrawRestricted(r *runner.E2ERunner, args []string) { r.SetBtcAddress(r.Name, false) - WithdrawBitcoinRestricted(r, amount) + withdrawBitcoinRestricted(r, amount) +} + +func parseBitcoinWithdrawArgs(args []string, defaultReceiver string) (btcutil.Address, *big.Int) { + // parse receiver address + var err error + var receiver btcutil.Address + if args[0] == "" { + // use the default receiver + receiver, err = chains.DecodeBtcAddress(defaultReceiver, chains.BtcRegtestChain().ChainId) + if err != nil { + panic("Invalid default receiver address specified for TestBitcoinWithdraw.") + } + } else { + receiver, err = chains.DecodeBtcAddress(args[0], chains.BtcRegtestChain().ChainId) + if err != nil { + panic("Invalid receiver address specified for TestBitcoinWithdraw.") + } + } + // parse the withdrawal amount + withdrawalAmount, err := strconv.ParseFloat(args[1], 64) + if err != nil { + panic("Invalid withdrawal amount specified for TestBitcoinWithdraw.") + } + withdrawalAmountSat, err := btcutil.NewAmount(withdrawalAmount) + if err != nil { + panic(err) + } + amount := big.NewInt(int64(withdrawalAmountSat)) + + return receiver, amount } func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) *btcjson.TxRawResult { @@ -85,7 +185,13 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) panic(err) } + // get cctx and check status cctx := utils.WaitCctxMinedByInTxHash(r.Ctx, receipt.TxHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_OutboundMined { + panic(fmt.Errorf("cctx status is not OutboundMined")) + } + + // get bitcoin tx according to the outTxHash in cctx outTxHash := cctx.GetCurrentOutTxParam().OutboundTxHash hash, err := chainhash.NewHashFromStr(outTxHash) if err != nil { @@ -116,11 +222,7 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) return rawTx } -func WithdrawBitcoin(r *runner.E2ERunner, amount *big.Int) { - withdrawBTCZRC20(r, r.BTCDeployerAddress, amount) -} - -func WithdrawBitcoinRestricted(r *runner.E2ERunner, amount *big.Int) { +func withdrawBitcoinRestricted(r *runner.E2ERunner, amount *big.Int) { // use restricted BTC P2WPKH address addressRestricted, err := chains.DecodeBtcAddress(testutils.RestrictedBtcAddressTest, chains.BtcRegtestChain().ChainId) if err != nil { diff --git a/e2e/e2etests/test_bitcoin_withdraw_invalid.go b/e2e/e2etests/test_bitcoin_withdraw_invalid.go index d168826493..39ff5e195b 100644 --- a/e2e/e2etests/test_bitcoin_withdraw_invalid.go +++ b/e2e/e2etests/test_bitcoin_withdraw_invalid.go @@ -48,6 +48,7 @@ func WithdrawToInvalidAddress(r *runner.E2ERunner, amount *big.Int) { stop := r.MineBlocks() // withdraw amount provided as test arg BTC from ZRC20 to BTC legacy address + // the address "1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3" is for mainnet, not regtest tx, err = r.BTCZRC20.Withdraw(r.ZEVMAuth, []byte("1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3"), amount) if err != nil { panic(err) diff --git a/e2e/e2etests/test_eth_withdraw.go b/e2e/e2etests/test_eth_withdraw.go index 66e0385642..e9a4211461 100644 --- a/e2e/e2etests/test_eth_withdraw.go +++ b/e2e/e2etests/test_eth_withdraw.go @@ -12,6 +12,8 @@ import ( // TestEtherWithdraw tests the withdraw of ether func TestEtherWithdraw(r *runner.E2ERunner, args []string) { + r.Logger.Info("TestEtherWithdraw") + approvedAmount := big.NewInt(1e18) if len(args) != 1 { panic("TestEtherWithdraw requires exactly one argument for the withdrawal amount.") @@ -59,6 +61,8 @@ func TestEtherWithdraw(r *runner.E2ERunner, args []string) { if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_OutboundMined { panic("cctx status is not outbound mined") } + + r.Logger.Info("TestEtherWithdraw completed") } // TestEtherWithdrawRestricted tests the withdrawal to a restricted receiver address diff --git a/e2e/e2etests/test_migrate_chain_support.go b/e2e/e2etests/test_migrate_chain_support.go new file mode 100644 index 0000000000..ae1acdde93 --- /dev/null +++ b/e2e/e2etests/test_migrate_chain_support.go @@ -0,0 +1,318 @@ +package e2etests + +import ( + "context" + "fmt" + "math/big" + "os/exec" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/fatih/color" + "github.com/zeta-chain/protocol-contracts/pkg/contracts/zevm/zrc20.sol" + "github.com/zeta-chain/zetacore/e2e/runner" + "github.com/zeta-chain/zetacore/e2e/txserver" + "github.com/zeta-chain/zetacore/e2e/utils" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" + fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +// EVM2RPCURL is the RPC URL for the additional EVM localnet +// Only this test currently uses a additional EVM localnet, and this test is only run locally +// Therefore, we hardcode RPC urls and addresses for simplicity +const EVM2RPCURL = "http://eth2:8545" + +// EVM2ChainID is the chain ID for the additional EVM localnet +// We set Sepolia testnet although the value is not important, only used to differentiate +var EVM2ChainID = chains.SepoliaChain().ChainId + +func TestMigrateChainSupport(r *runner.E2ERunner, _ []string) { + // deposit most of the ZETA supply on ZetaChain + zetaAmount := big.NewInt(1e18) + zetaAmount = zetaAmount.Mul(zetaAmount, big.NewInt(20_000_000_000)) // 20B Zeta + r.DepositZetaWithAmount(r.DeployerAddress, zetaAmount) + + // do an ethers withdraw on the previous chain (0.01eth) for some interaction + TestEtherWithdraw(r, []string{"10000000000000000"}) + + // create runner for the new EVM and set it up + newRunner, err := configureEVM2(r) + if err != nil { + panic(err) + } + newRunner.SetupEVM(false, false) + + // mint some ERC20 + newRunner.MintERC20OnEvm(10000) + + // we deploy connectorETH in this test to simulate a new "canonical" chain emitting ZETA + // to represent the ZETA already existing on ZetaChain we manually send the minted ZETA to the connector + newRunner.SendZetaOnEvm(newRunner.ConnectorEthAddr, 20_000_000_000) + + // update the chain params to set up the chain + chainParams := getNewEVMChainParams(newRunner) + adminAddr, err := newRunner.ZetaTxServer.GetAccountAddressFromName(utils.FungibleAdminName) + if err != nil { + panic(err) + } + _, err = newRunner.ZetaTxServer.BroadcastTx(utils.FungibleAdminName, observertypes.NewMsgUpdateChainParams( + adminAddr, + chainParams, + )) + if err != nil { + panic(err) + } + + // setup the gas token + if err != nil { + panic(err) + } + _, err = newRunner.ZetaTxServer.BroadcastTx(utils.FungibleAdminName, fungibletypes.NewMsgDeployFungibleCoinZRC20( + adminAddr, + "", + chainParams.ChainId, + 18, + "Sepolia ETH", + "sETH", + coin.CoinType_Gas, + 100000, + )) + if err != nil { + panic(err) + } + + // set the gas token in the runner + ethZRC20Addr, err := newRunner.SystemContract.GasCoinZRC20ByChainId(&bind.CallOpts{}, big.NewInt(chainParams.ChainId)) + if err != nil { + panic(err) + } + if (ethZRC20Addr == ethcommon.Address{}) { + panic("eth zrc20 not found") + } + newRunner.ETHZRC20Addr = ethZRC20Addr + ethZRC20, err := zrc20.NewZRC20(ethZRC20Addr, newRunner.ZEVMClient) + if err != nil { + panic(err) + } + newRunner.ETHZRC20 = ethZRC20 + + // set the chain nonces for the new chain + _, err = r.ZetaTxServer.BroadcastTx(utils.FungibleAdminName, observertypes.NewMsgResetChainNonces( + adminAddr, + chainParams.ChainId, + 0, + 0, + )) + if err != nil { + panic(err) + } + + // deactivate the previous chain + chainParams = observertypes.GetDefaultGoerliLocalnetChainParams() + chainParams.IsSupported = false + _, err = newRunner.ZetaTxServer.BroadcastTx(utils.FungibleAdminName, observertypes.NewMsgUpdateChainParams( + adminAddr, + chainParams, + )) + if err != nil { + panic(err) + } + + // restart ZetaClient to pick up the new chain + r.Logger.Print("🔄 restarting ZetaClient to pick up the new chain") + if err := restartZetaClient(); err != nil { + panic(err) + } + + // wait 10 set for the chain to start + time.Sleep(10 * time.Second) + + // emitting a withdraw with the previous chain should fail + txWithdraw, err := r.ETHZRC20.Withdraw(r.ZEVMAuth, r.DeployerAddress.Bytes(), big.NewInt(10000000000000000)) + if err == nil { + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, txWithdraw, r.Logger, r.ReceiptTimeout) + if receipt.Status == 1 { + panic("withdraw should have failed on the previous chain") + } + } + + // test cross-chain functionalities on the new network + // we use a Go routine to manually mine blocks because Anvil network only mine blocks on tx by default + // we need automatic block mining to get the necessary confirmations for the cross-chain functionalities + stopMining, err := newRunner.AnvilMineBlocks(EVM2RPCURL, 3) + if err != nil { + panic(err) + } + + // deposit Ethers and ERC20 on ZetaChain + etherAmount := big.NewInt(1e18) + etherAmount = etherAmount.Mul(etherAmount, big.NewInt(10)) + txEtherDeposit := newRunner.DepositEtherWithAmount(false, etherAmount) + newRunner.WaitForMinedCCTX(txEtherDeposit) + + // perform withdrawals on the new chain + TestZetaWithdraw(newRunner, []string{"10000000000000000000"}) + TestEtherWithdraw(newRunner, []string{"50000000000000000"}) + + // finally try to deposit Zeta back + TestZetaDeposit(newRunner, []string{"100000000000000000"}) + + // ERC20 test + + // whitelist erc20 zrc20 + newRunner.Logger.Info("whitelisting ERC20 on new network") + res, err := newRunner.ZetaTxServer.BroadcastTx(utils.FungibleAdminName, crosschaintypes.NewMsgWhitelistERC20( + adminAddr, + newRunner.ERC20Addr.Hex(), + chains.SepoliaChain().ChainId, + "USDT", + "USDT", + 18, + 100000, + )) + if err != nil { + panic(err) + } + + // retrieve zrc20 and cctx from event + whitelistCCTXIndex, err := txserver.FetchAttributeFromTxResponse(res, "whitelist_cctx_index") + if err != nil { + panic(err) + } + + erc20zrc20Addr, err := txserver.FetchAttributeFromTxResponse(res, "zrc20_address") + if err != nil { + panic(err) + } + + // wait for the whitelist cctx to be mined + newRunner.WaitForMinedCCTXFromIndex(whitelistCCTXIndex) + + // set erc20 zrc20 contract address + if !ethcommon.IsHexAddress(erc20zrc20Addr) { + panic(fmt.Errorf("invalid contract address: %s", erc20zrc20Addr)) + } + erc20ZRC20, err := zrc20.NewZRC20(ethcommon.HexToAddress(erc20zrc20Addr), newRunner.ZEVMClient) + if err != nil { + panic(err) + } + newRunner.ERC20ZRC20 = erc20ZRC20 + + // deposit ERC20 on ZetaChain + txERC20Deposit := newRunner.DepositERC20() + newRunner.WaitForMinedCCTX(txERC20Deposit) + + // stop mining + stopMining() +} + +// configureEVM2 takes a runner and configures it to use the additional EVM localnet +func configureEVM2(r *runner.E2ERunner) (*runner.E2ERunner, error) { + // initialize a new runner with previous runner values + newRunner := runner.NewE2ERunner( + r.Ctx, + "admin-evm2", + r.CtxCancel, + r.DeployerAddress, + r.DeployerPrivateKey, + r.FungibleAdminMnemonic, + r.EVMClient, + r.ZEVMClient, + r.CctxClient, + r.ZetaTxServer, + r.FungibleClient, + r.AuthClient, + r.BankClient, + r.ObserverClient, + r.EVMAuth, + r.ZEVMAuth, + r.BtcRPCClient, + runner.NewLogger(true, color.FgHiYellow, "admin-evm2"), + ) + + // All existing fields of the runner are the same except for the RPC URL and client for EVM + ewvmClient, evmAuth, err := getEVMClient(newRunner.Ctx, EVM2RPCURL, r.DeployerPrivateKey) + if err != nil { + return nil, err + } + newRunner.EVMClient = ewvmClient + newRunner.EVMAuth = evmAuth + + // Copy the ZetaChain contract addresses from the original runner + if err := newRunner.CopyAddressesFrom(r); err != nil { + return nil, err + } + + // reset evm contracts to ensure they are re-initialized + newRunner.ZetaEthAddr = ethcommon.Address{} + newRunner.ZetaEth = nil + newRunner.ConnectorEthAddr = ethcommon.Address{} + newRunner.ConnectorEth = nil + newRunner.ERC20CustodyAddr = ethcommon.Address{} + newRunner.ERC20Custody = nil + newRunner.ERC20Addr = ethcommon.Address{} + newRunner.ERC20 = nil + + return newRunner, nil +} + +// getEVMClient get evm client from rpc and private key +func getEVMClient(ctx context.Context, rpc, privKey string) (*ethclient.Client, *bind.TransactOpts, error) { + evmClient, err := ethclient.Dial(rpc) + if err != nil { + return nil, nil, err + } + + chainid, err := evmClient.ChainID(ctx) + if err != nil { + return nil, nil, err + } + deployerPrivkey, err := crypto.HexToECDSA(privKey) + if err != nil { + return nil, nil, err + } + evmAuth, err := bind.NewKeyedTransactorWithChainID(deployerPrivkey, chainid) + if err != nil { + return nil, nil, err + } + + return evmClient, evmAuth, nil +} + +// getNewEVMChainParams returns the chain params for the new EVM chain +func getNewEVMChainParams(r *runner.E2ERunner) *observertypes.ChainParams { + // goerli local as base + chainParams := observertypes.GetDefaultGoerliLocalnetChainParams() + + // set the chain id to the new chain id + chainParams.ChainId = EVM2ChainID + + // set contracts + chainParams.ConnectorContractAddress = r.ConnectorEthAddr.Hex() + chainParams.Erc20CustodyContractAddress = r.ERC20CustodyAddr.Hex() + chainParams.ZetaTokenContractAddress = r.ZetaEthAddr.Hex() + + // set supported + chainParams.IsSupported = true + + return chainParams +} + +// restartZetaClient restarts the Zeta client +func restartZetaClient() error { + sshCommandFilePath := "/work/restart-zetaclientd.sh" + cmd := exec.Command("/bin/sh", sshCommandFilePath) + + // Execute the command + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error restarting ZetaClient: %s - %s", err.Error(), output) + } + return nil +} diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index df0c2d1a23..6462b2f1db 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -267,14 +267,18 @@ func (runner *E2ERunner) SendToTSSFromDeployerWithMemo( } depositorFee := zetabitcoin.DefaultDepositorFee - events := zetabitcoin.FilterAndParseIncomingTx( + events, err := zetabitcoin.FilterAndParseIncomingTx( + btcRPC, []btcjson.TxRawResult{*rawtx}, 0, runner.BTCTSSAddress.EncodeAddress(), - &log.Logger, + log.Logger, runner.BitcoinParams, depositorFee, ) + if err != nil { + panic(err) + } runner.Logger.Info("bitcoin intx events:") for _, event := range events { runner.Logger.Info(" TxHash: %s", event.TxHash) diff --git a/e2e/runner/evm.go b/e2e/runner/evm.go index 8a931eeccd..c0253905cc 100644 --- a/e2e/runner/evm.go +++ b/e2e/runner/evm.go @@ -1,9 +1,12 @@ package runner import ( + "log" "math/big" "time" + "github.com/ethereum/go-ethereum/rpc" + ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -77,7 +80,7 @@ func (runner *E2ERunner) DepositERC20() ethcommon.Hash { } func (runner *E2ERunner) DepositERC20WithAmountAndMessage(to ethcommon.Address, amount *big.Int, msg []byte) ethcommon.Hash { - // reset allowance, necessary for ERC20 + // reset allowance, necessary for USDT tx, err := runner.ERC20.Approve(runner.EVMAuth, runner.ERC20CustodyAddr, big.NewInt(0)) if err != nil { panic(err) @@ -169,7 +172,7 @@ func (runner *E2ERunner) SendEther(_ ethcommon.Address, value *big.Int, data []b } tx := ethtypes.NewTransaction(nonce, runner.TSSAddress, value, gasLimit, gasPrice, data) - chainID, err := evmClient.NetworkID(runner.Ctx) + chainID, err := evmClient.ChainID(runner.Ctx) if err != nil { return nil, err } @@ -256,3 +259,36 @@ func (runner *E2ERunner) ProveEthTransaction(receipt *ethtypes.Receipt) { } runner.Logger.Info("OK: txProof verified") } + +// AnvilMineBlocks mines blocks on Anvil localnet +// the block time is provided in seconds +// the method returns a function to stop the mining +func (runner *E2ERunner) AnvilMineBlocks(url string, blockTime int) (func(), error) { + stop := make(chan struct{}) + + client, err := rpc.Dial(url) + if err != nil { + return nil, err + } + defer client.Close() + + go func() { + for { + select { + case <-stop: + return + default: + time.Sleep(time.Duration(blockTime) * time.Second) + + var result interface{} + err = client.CallContext(runner.Ctx, &result, "evm_mine") + if err != nil { + log.Fatalf("Failed to mine a new block: %v", err) + } + } + } + }() + return func() { + close(stop) + }, nil +} diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 22f549b296..f66af62ff0 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -62,15 +62,17 @@ type E2ERunner struct { EVMAuth *bind.TransactOpts ZEVMAuth *bind.TransactOpts - // contracts - ZetaEthAddr ethcommon.Address - ZetaEth *zetaeth.ZetaEth - ConnectorEthAddr ethcommon.Address - ConnectorEth *zetaconnectoreth.ZetaConnectorEth - ERC20CustodyAddr ethcommon.Address - ERC20Custody *erc20custody.ERC20Custody - ERC20Addr ethcommon.Address - ERC20 *erc20.ERC20 + // contracts evm + ZetaEthAddr ethcommon.Address + ZetaEth *zetaeth.ZetaEth + ConnectorEthAddr ethcommon.Address + ConnectorEth *zetaconnectoreth.ZetaConnectorEth + ERC20CustodyAddr ethcommon.Address + ERC20Custody *erc20custody.ERC20Custody + ERC20Addr ethcommon.Address + ERC20 *erc20.ERC20 + + // contracts zevm ERC20ZRC20Addr ethcommon.Address ERC20ZRC20 *zrc20.ZRC20 ETHZRC20Addr ethcommon.Address @@ -85,14 +87,13 @@ type E2ERunner struct { ConnectorZEVM *connectorzevm.ZetaConnectorZEVM WZetaAddr ethcommon.Address WZeta *wzeta.WETH9 - - TestDAppAddr ethcommon.Address - ZEVMSwapAppAddr ethcommon.Address - ZEVMSwapApp *zevmswap.ZEVMSwapApp - ContextAppAddr ethcommon.Address - ContextApp *contextapp.ContextApp - SystemContractAddr ethcommon.Address - SystemContract *systemcontract.SystemContract + TestDAppAddr ethcommon.Address + ZEVMSwapAppAddr ethcommon.Address + ZEVMSwapApp *zevmswap.ZEVMSwapApp + ContextAppAddr ethcommon.Address + ContextApp *contextapp.ContextApp + SystemContractAddr ethcommon.Address + SystemContract *systemcontract.SystemContract // config CctxTimeout time.Duration diff --git a/e2e/runner/setup_evm.go b/e2e/runner/setup_evm.go index e8ff909169..8e76ac7409 100644 --- a/e2e/runner/setup_evm.go +++ b/e2e/runner/setup_evm.go @@ -42,7 +42,7 @@ func (runner *E2ERunner) SetEVMContractsFromConfig() { } // SetupEVM setup contracts on EVM for e2e test -func (runner *E2ERunner) SetupEVM(contractsDeployed bool) { +func (runner *E2ERunner) SetupEVM(contractsDeployed bool, whitelistERC20 bool) { runner.Logger.Print("⚙️ setting up EVM network") startTime := time.Now() defer func() { @@ -157,12 +157,14 @@ func (runner *E2ERunner) SetupEVM(contractsDeployed bool) { // initialize custody contract runner.Logger.Info("Whitelist ERC20") - txWhitelist, err := ERC20Custody.Whitelist(runner.EVMAuth, erc20Addr) - if err != nil { - panic(err) - } - if receipt := utils.MustWaitForTxReceipt(runner.Ctx, runner.EVMClient, txWhitelist, runner.Logger, runner.ReceiptTimeout); receipt.Status != 1 { - panic("ERC20 whitelist failed") + if whitelistERC20 { + txWhitelist, err := ERC20Custody.Whitelist(runner.EVMAuth, erc20Addr) + if err != nil { + panic(err) + } + if receipt := utils.MustWaitForTxReceipt(runner.Ctx, runner.EVMClient, txWhitelist, runner.Logger, runner.ReceiptTimeout); receipt.Status != 1 { + panic("ERC20 whitelist failed") + } } runner.Logger.Info("Set TSS address") diff --git a/e2e/runner/setup_zeta.go b/e2e/runner/setup_zeta.go index 28d9f25658..3789babf06 100644 --- a/e2e/runner/setup_zeta.go +++ b/e2e/runner/setup_zeta.go @@ -4,9 +4,6 @@ import ( "math/big" "time" - "github.com/zeta-chain/zetacore/e2e/txserver" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/btcsuite/btcutil" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -18,7 +15,9 @@ import ( uniswapv2router "github.com/zeta-chain/protocol-contracts/pkg/uniswap/v2-periphery/contracts/uniswapv2router02.sol" "github.com/zeta-chain/zetacore/e2e/contracts/contextapp" "github.com/zeta-chain/zetacore/e2e/contracts/zevmswap" + "github.com/zeta-chain/zetacore/e2e/txserver" e2eutils "github.com/zeta-chain/zetacore/e2e/utils" + "github.com/zeta-chain/zetacore/pkg/chains" fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) diff --git a/e2e/runner/zeta.go b/e2e/runner/zeta.go index 046e14d706..d7fdbdcdf4 100644 --- a/e2e/runner/zeta.go +++ b/e2e/runner/zeta.go @@ -40,6 +40,22 @@ func (runner *E2ERunner) WaitForMinedCCTX(txHash ethcommon.Hash) { } } +// WaitForMinedCCTXFromIndex waits for a cctx to be mined from its index +func (runner *E2ERunner) WaitForMinedCCTXFromIndex(index string) { + defer func() { + runner.Unlock() + }() + runner.Lock() + + cctx := utils.WaitCCTXMinedByIndex(runner.Ctx, index, runner.CctxClient, runner.Logger, runner.CctxTimeout) + if cctx.CctxStatus.Status != types.CctxStatus_OutboundMined { + panic(fmt.Sprintf("expected cctx status to be mined; got %s, message: %s", + cctx.CctxStatus.Status.String(), + cctx.CctxStatus.StatusMessage), + ) + } +} + // SendZetaOnEvm sends ZETA to an address on EVM // this allows the ZETA contract deployer to funds other accounts on EVM func (runner *E2ERunner) SendZetaOnEvm(address ethcommon.Address, zetaAmount int64) *ethtypes.Transaction { diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index f610b8b2bf..f3d946ff5c 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -128,6 +128,20 @@ func (zts ZetaTxServer) GetAccountAddress(index int) string { return zts.address[index] } +// GetAccountAddressFromName returns the account address from the given name +func (zts ZetaTxServer) GetAccountAddressFromName(name string) (string, error) { + acc, err := zts.clientCtx.Keyring.Key(name) + if err != nil { + return "", err + } + addr, err := acc.GetAddress() + if err != nil { + return "", err + } + return addr.String(), nil +} + +// GetAllAccountAddress returns all account addresses func (zts ZetaTxServer) GetAllAccountAddress() []string { return zts.address @@ -197,7 +211,7 @@ func (zts ZetaTxServer) DeploySystemContractsAndZRC20(account, erc20Addr string) return "", "", "", "", "", fmt.Errorf("failed to deploy system contracts: %s", err.Error()) } - systemContractAddress, err := fetchAttribute(res, "system_contract") + systemContractAddress, err := FetchAttributeFromTxResponse(res, "system_contract") if err != nil { return "", "", "", "", "", fmt.Errorf("failed to fetch system contract address: %s; rawlog %s", err.Error(), res.RawLog) } @@ -209,23 +223,23 @@ func (zts ZetaTxServer) DeploySystemContractsAndZRC20(account, erc20Addr string) } // get uniswap contract addresses - uniswapV2FactoryAddr, err := fetchAttribute(res, "uniswap_v2_factory") + uniswapV2FactoryAddr, err := FetchAttributeFromTxResponse(res, "uniswap_v2_factory") if err != nil { return "", "", "", "", "", fmt.Errorf("failed to fetch uniswap v2 factory address: %s", err.Error()) } - uniswapV2RouterAddr, err := fetchAttribute(res, "uniswap_v2_router") + uniswapV2RouterAddr, err := FetchAttributeFromTxResponse(res, "uniswap_v2_router") if err != nil { return "", "", "", "", "", fmt.Errorf("failed to fetch uniswap v2 router address: %s", err.Error()) } // get zevm connector address - zevmConnectorAddr, err := fetchAttribute(res, "connector_zevm") + zevmConnectorAddr, err := FetchAttributeFromTxResponse(res, "connector_zevm") if err != nil { return "", "", "", "", "", fmt.Errorf("failed to fetch zevm connector address: %s, txResponse: %s", err.Error(), res.String()) } // get wzeta address - wzetaAddr, err := fetchAttribute(res, "wzeta") + wzetaAddr, err := FetchAttributeFromTxResponse(res, "wzeta") if err != nil { return "", "", "", "", "", fmt.Errorf("failed to fetch wzeta address: %s, txResponse: %s", err.Error(), res.String()) } @@ -276,7 +290,7 @@ func (zts ZetaTxServer) DeploySystemContractsAndZRC20(account, erc20Addr string) } // fetch the erc20 zrc20 contract address and remove the quotes - erc20zrc20Addr, err := fetchAttribute(res, "Contract") + erc20zrc20Addr, err := FetchAttributeFromTxResponse(res, "Contract") if err != nil { return "", "", "", "", "", fmt.Errorf("failed to fetch erc20 zrc20 contract address: %s", err.Error()) } @@ -395,8 +409,8 @@ type attribute struct { Value string `json:"value"` } -// fetchAttribute fetches the attribute from the tx response -func fetchAttribute(res *sdktypes.TxResponse, key string) (string, error) { +// FetchAttributeFromTxResponse fetches the attribute from the tx response +func FetchAttributeFromTxResponse(res *sdktypes.TxResponse, key string) (string, error) { var logs []messageLog err := json.Unmarshal([]byte(res.RawLog), &logs) if err != nil { diff --git a/e2e/utils/zetacore.go b/e2e/utils/zetacore.go index 6a491d1ebd..4aeac71ba3 100644 --- a/e2e/utils/zetacore.go +++ b/e2e/utils/zetacore.go @@ -49,7 +49,11 @@ func WaitCctxsMinedByInTxHash( // fetch cctxs by inTxHash for i := 0; ; i++ { + if time.Since(startTime) > timeout { + panic(fmt.Sprintf("waiting cctx timeout, cctx not mined, inTxHash: %s", inTxHash)) + } time.Sleep(1 * time.Second) + res, err := cctxClient.InTxHashToCctxData(ctx, &crosschaintypes.QueryInTxHashToCctxDataRequest{ InTxHash: inTxHash, }) @@ -93,19 +97,65 @@ func WaitCctxsMinedByInTxHash( cctxs = append(cctxs, &cctx) } if !allFound { - if time.Since(startTime) > timeout { - panic(fmt.Sprintf( - "waiting cctx timeout, cctx not mined, inTxHash: %s, current cctxs: %v", - inTxHash, - cctxs, - )) - } continue } return cctxs } } +// WaitCCTXMinedByIndex waits until cctx is mined; returns the cctxIndex +func WaitCCTXMinedByIndex( + ctx context.Context, + cctxIndex string, + cctxClient crosschaintypes.QueryClient, + logger infoLogger, + cctxTimeout time.Duration, +) *crosschaintypes.CrossChainTx { + startTime := time.Now() + + timeout := DefaultCctxTimeout + if cctxTimeout != 0 { + timeout = cctxTimeout + } + + for i := 0; ; i++ { + if time.Since(startTime) > timeout { + panic(fmt.Sprintf( + "waiting cctx timeout, cctx not mined, cctx: %s", + cctxIndex, + )) + } + time.Sleep(1 * time.Second) + + // fetch cctx by index + res, err := cctxClient.Cctx(ctx, &crosschaintypes.QueryGetCctxRequest{ + Index: cctxIndex, + }) + if err != nil { + // prevent spamming logs + if i%10 == 0 { + logger.Info("Error getting cctx by inTxHash: %s", err.Error()) + } + continue + } + cctx := res.CrossChainTx + if !IsTerminalStatus(cctx.CctxStatus.Status) { + // prevent spamming logs + if i%10 == 0 { + logger.Info( + "waiting for cctx to be mined from index: %s, cctx status: %s, message: %s", + cctxIndex, + cctx.CctxStatus.Status.String(), + cctx.CctxStatus.StatusMessage, + ) + } + continue + } + + return cctx + } +} + func IsTerminalStatus(status crosschaintypes.CctxStatus) bool { return status == crosschaintypes.CctxStatus_OutboundMined || status == crosschaintypes.CctxStatus_Aborted || diff --git a/pkg/chains/address.go b/pkg/chains/address.go index 6ee8647f8b..5eb0040485 100644 --- a/pkg/chains/address.go +++ b/pkg/chains/address.go @@ -51,6 +51,7 @@ func ConvertRecoverToError(r interface{}) error { } } +// DecodeBtcAddress decodes a BTC address from a given string and chainID func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Address, err error) { defer func() { if r := recover(); r != nil { @@ -63,13 +64,43 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre if err != nil { return nil, err } + if chainParams == nil { + return nil, fmt.Errorf("chain params not found") + } + // test taproot address type + address, err = DecodeTaprootAddress(inputAddress) + if err == nil { + if address.IsForNet(chainParams) { + return address, nil + } + return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name) + } + // test taproot address failed; continue testing other types: P2WSH, P2WPKH, P2SH, P2PKH address, err = btcutil.DecodeAddress(inputAddress, chainParams) if err != nil { - return nil, fmt.Errorf("decode address failed: %s , for input address %s", err.Error(), inputAddress) + return nil, fmt.Errorf("decode address failed: %s, for input address %s", err.Error(), inputAddress) } ok := address.IsForNet(chainParams) if !ok { - return nil, fmt.Errorf("address is not for network %s", chainParams.Name) + return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name) } return } + +// IsBtcAddressSupported returns true if the given BTC address is supported +func IsBtcAddressSupported(addr btcutil.Address) bool { + switch addr.(type) { + // P2TR address + case *AddressTaproot, + // P2WSH address + *btcutil.AddressWitnessScriptHash, + // P2WPKH address + *btcutil.AddressWitnessPubKeyHash, + // P2SH address + *btcutil.AddressScriptHash, + // P2PKH address + *btcutil.AddressPubKeyHash: + return true + } + return false +} diff --git a/pkg/chains/address_taproot.go b/pkg/chains/address_taproot.go new file mode 100644 index 0000000000..b79118ea85 --- /dev/null +++ b/pkg/chains/address_taproot.go @@ -0,0 +1,218 @@ +package chains + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/btcsuite/btcd/btcutil/bech32" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" +) + +// taproot address type + +type AddressSegWit struct { + hrp string + witnessVersion byte + witnessProgram []byte +} + +type AddressTaproot struct { + AddressSegWit +} + +var _ btcutil.Address = &AddressTaproot{} + +// NewAddressTaproot returns a new AddressTaproot. +func NewAddressTaproot(witnessProg []byte, + net *chaincfg.Params) (*AddressTaproot, error) { + + return newAddressTaproot(net.Bech32HRPSegwit, witnessProg) +} + +// newAddressWitnessScriptHash is an internal helper function to create an +// AddressWitnessScriptHash with a known human-readable part, rather than +// looking it up through its parameters. +func newAddressTaproot(hrp string, witnessProg []byte) (*AddressTaproot, error) { + // Check for valid program length for witness version 1, which is 32 + // for P2TR. + if len(witnessProg) != 32 { + return nil, errors.New("witness program must be 32 bytes for " + + "p2tr") + } + + addr := &AddressTaproot{ + AddressSegWit{ + hrp: strings.ToLower(hrp), + witnessVersion: 0x01, + witnessProgram: witnessProg, + }, + } + + return addr, nil +} + +// EncodeAddress returns the bech32 (or bech32m for SegWit v1) string encoding +// of an AddressSegWit. +// +// NOTE: This method is part of the Address interface. +func (a AddressSegWit) EncodeAddress() string { + str, err := encodeSegWitAddress( + a.hrp, a.witnessVersion, a.witnessProgram[:], + ) + if err != nil { + return "" + } + return str +} + +// encodeSegWitAddress creates a bech32 (or bech32m for SegWit v1) encoded +// address string representation from witness version and witness program. +func encodeSegWitAddress(hrp string, witnessVersion byte, witnessProgram []byte) (string, error) { + // Group the address bytes into 5 bit groups, as this is what is used to + // encode each character in the address string. + converted, err := bech32.ConvertBits(witnessProgram, 8, 5, true) + if err != nil { + return "", err + } + + // Concatenate the witness version and program, and encode the resulting + // bytes using bech32 encoding. + combined := make([]byte, len(converted)+1) + combined[0] = witnessVersion + copy(combined[1:], converted) + + var bech string + switch witnessVersion { + case 0: + bech, err = bech32.Encode(hrp, combined) + + case 1: + bech, err = bech32.EncodeM(hrp, combined) + + default: + return "", fmt.Errorf("unsupported witness version %d", + witnessVersion) + } + if err != nil { + return "", err + } + + // Check validity by decoding the created address. + _, version, program, err := decodeSegWitAddress(bech) + if err != nil { + return "", fmt.Errorf("invalid segwit address: %v", err) + } + + if version != witnessVersion || !bytes.Equal(program, witnessProgram) { + return "", fmt.Errorf("invalid segwit address") + } + + return bech, nil +} + +// decodeSegWitAddress parses a bech32 encoded segwit address string and +// returns the witness version and witness program byte representation. +func decodeSegWitAddress(address string) (string, byte, []byte, error) { + // Decode the bech32 encoded address. + hrp, data, bech32version, err := bech32.DecodeGeneric(address) + if err != nil { + return "", 0, nil, err + } + + // The first byte of the decoded address is the witness version, it must + // exist. + if len(data) < 1 { + return "", 0, nil, fmt.Errorf("no witness version") + } + + // ...and be <= 16. + version := data[0] + if version > 16 { + return "", 0, nil, fmt.Errorf("invalid witness version: %v", version) + } + + // The remaining characters of the address returned are grouped into + // words of 5 bits. In order to restore the original witness program + // bytes, we'll need to regroup into 8 bit words. + regrouped, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return "", 0, nil, err + } + + // The regrouped data must be between 2 and 40 bytes. + if len(regrouped) < 2 || len(regrouped) > 40 { + return "", 0, nil, fmt.Errorf("invalid data length") + } + + // For witness version 0, address MUST be exactly 20 or 32 bytes. + if version == 0 && len(regrouped) != 20 && len(regrouped) != 32 { + return "", 0, nil, fmt.Errorf("invalid data length for witness "+ + "version 0: %v", len(regrouped)) + } + + // For witness version 0, the bech32 encoding must be used. + if version == 0 && bech32version != bech32.Version0 { + return "", 0, nil, fmt.Errorf("invalid checksum expected bech32 " + + "encoding for address with witness version 0") + } + + // For witness version 1, the bech32m encoding must be used. + if version == 1 && bech32version != bech32.VersionM { + return "", 0, nil, fmt.Errorf("invalid checksum expected bech32m " + + "encoding for address with witness version 1") + } + + return hrp, version, regrouped, nil +} + +// ScriptAddress returns the witness program for this address. +// +// NOTE: This method is part of the Address interface. +func (a *AddressSegWit) ScriptAddress() []byte { + return a.witnessProgram[:] +} + +// IsForNet returns whether the AddressSegWit is associated with the passed +// bitcoin network. +// +// NOTE: This method is part of the Address interface. +func (a *AddressSegWit) IsForNet(net *chaincfg.Params) bool { + return a.hrp == net.Bech32HRPSegwit +} + +// String returns a human-readable string for the AddressWitnessPubKeyHash. +// This is equivalent to calling EncodeAddress, but is provided so the type +// can be used as a fmt.Stringer. +// +// NOTE: This method is part of the Address interface. +func (a *AddressSegWit) String() string { + return a.EncodeAddress() +} + +// DecodeTaprootAddress decodes taproot address only and returns error on non-taproot address +func DecodeTaprootAddress(addr string) (*AddressTaproot, error) { + hrp, version, program, err := decodeSegWitAddress(addr) + if err != nil { + return nil, err + } + if version != 1 { + return nil, errors.New("invalid witness version; taproot address must be version 1") + } + return &AddressTaproot{ + AddressSegWit{ + hrp: hrp, + witnessVersion: version, + witnessProgram: program, + }, + }, nil +} + +// PayToWitnessTaprootScript creates a new script to pay to a version 1 +// (taproot) witness program. The passed hash is expected to be valid. +func PayToWitnessTaprootScript(rawKey []byte) ([]byte, error) { + return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(rawKey).Script() +} diff --git a/pkg/chains/address_taproot_test.go b/pkg/chains/address_taproot_test.go new file mode 100644 index 0000000000..d29e91a67d --- /dev/null +++ b/pkg/chains/address_taproot_test.go @@ -0,0 +1,75 @@ +package chains + +import ( + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/require" +) + +func TestAddressTaproot(t *testing.T) { + { + // should parse mainnet taproot address + addrStr := "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + require.Equal(t, addrStr, addr.String()) + require.Equal(t, addrStr, addr.EncodeAddress()) + require.True(t, addr.IsForNet(&chaincfg.MainNetParams)) + } + { + // should parse testnet taproot address + addrStr := "tb1pzeclkt6upu8xwuksjcz36y4q56dd6jw5r543eu8j8238yaxpvcvq7t8f33" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + require.Equal(t, addrStr, addr.String()) + require.Equal(t, addrStr, addr.EncodeAddress()) + require.True(t, addr.IsForNet(&chaincfg.TestNet3Params)) + } + { + // should parse regtest taproot address + addrStr := "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + require.Equal(t, addrStr, addr.String()) + require.Equal(t, addrStr, addr.EncodeAddress()) + require.True(t, addr.IsForNet(&chaincfg.RegressionNetParams)) + } + + { + // should fail to parse invalid taproot address + // should parse mainnet taproot address + addrStr := "bc1qysd4sp9q8my59ul9wsf5rvs9p387hf8vfwatzu" + _, err := DecodeTaprootAddress(addrStr) + require.Error(t, err) + } + { + var witnessProg [32]byte + for i := 0; i < 32; i++ { + witnessProg[i] = byte(i) + } + _, err := newAddressTaproot("bcrt", witnessProg[:]) + require.Nil(t, err) + //t.Logf("addr: %v", addr) + } + { + // should create correct taproot address from given witness program + // these hex string comes from link + // https://mempool.space/tx/41f7cbaaf9a8d378d09ee86de32eebef455225520cb71015cc9a7318fb42e326 + witnessProg, err := hex.DecodeString("af06f3d4c726a3b952f2f648d86398af5ddfc3df27aa531d97203987add8db2e") + require.Nil(t, err) + addr, err := NewAddressTaproot(witnessProg[:], &chaincfg.MainNetParams) + require.Nil(t, err) + require.Equal(t, addr.EncodeAddress(), "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c") + } + { + // should give correct ScriptAddress for taproot address + // example comes from + // https://blockstream.info/tx/09298a2f32f5267f419aeaf8a58c4807dcf6cac3edb59815a3b129cd8f1219b0?expand + addrStr := "bc1p6pls9gpm24g8ntl37pajpjtuhd3y08hs5rnf9a4n0wq595hwdh9suw7m2h" + addr, err := DecodeTaprootAddress(addrStr) + require.Nil(t, err) + require.Equal(t, "d07f02a03b555079aff1f07b20c97cbb62479ef0a0e692f6b37b8142d2ee6dcb", hex.EncodeToString(addr.ScriptAddress())) + } +} diff --git a/pkg/chains/address_test.go b/pkg/chains/address_test.go index f34b1145c5..bc9e8d8171 100644 --- a/pkg/chains/address_test.go +++ b/pkg/chains/address_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/btcsuite/btcutil" "github.com/stretchr/testify/require" . "gopkg.in/check.v1" @@ -65,12 +66,263 @@ func TestDecodeBtcAddress(t *testing.T) { t.Run("non legacy valid address with incorrect params", func(t *testing.T) { _, err := DecodeBtcAddress("bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", BtcMainnetChain().ChainId) - require.ErrorContains(t, err, "address is not for network mainnet") + require.ErrorContains(t, err, "not for network mainnet") }) t.Run("non legacy valid address with correct params", func(t *testing.T) { _, err := DecodeBtcAddress("bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", BtcRegtestChain().ChainId) require.NoError(t, err) }) + + t.Run("taproot address with correct params", func(t *testing.T) { + _, err := DecodeBtcAddress("bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c", BtcMainnetChain().ChainId) + require.NoError(t, err) + }) + t.Run("taproot address with incorrect params", func(t *testing.T) { + _, err := DecodeBtcAddress("bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c", BtcTestNetChain().ChainId) + require.ErrorContains(t, err, "not for network testnet") + }) +} + +func Test_IsBtcAddressSupported_P2TR(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + name: "mainnet taproot address", + addr: "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/24991bd2fdc4f744bf7bbd915d4915925eecebdae249f81e057c0a6ffb700ab9 + name: "testnet taproot address", + addr: "tb1p7qqaucx69xtwkx7vwmhz03xjmzxxpy3hk29y7q06mt3k6a8sehhsu5lacw", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "regtest taproot address", + addr: "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a taproot address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*AddressTaproot) + require.True(t, ok) + + // it should be supported + require.NoError(t, err) + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2WSH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + name: "mainnet P2WSH address", + addr: "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/78fac3f0d4c0174c88d21c4bb1e23a8f007e890c6d2cfa64c97389ead16c51ed + name: "testnet P2WSH address", + addr: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "regtest P2WSH address", + addr: "bcrt1qm9mzhyky4w853ft2ms6dtqdyyu3z2tmrq8jg8xglhyuv0dsxzmgs2f0sqy", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2WSH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressWitnessScriptHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2WPKH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b + name: "mainnet P2WPKH address", + addr: "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/508b4d723c754bad001eae9b7f3c12377d3307bd5b595c27fd8a90089094f0e9 + name: "testnet P2WPKH address", + addr: "tb1q6rufg6myrxurdn0h57d2qhtm9zfmjw2mzcm05q", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "regtest P2WPKH address", + addr: "bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2WPKH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressWitnessPubKeyHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2SH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + name: "mainnet P2SH address", + addr: "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/0c8c8f94817e0288a5273f5c971adaa3cee18a895c3ec8544785dddcd96f3848 + name: "testnet P2SH address 1", + addr: "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/b5e074c5e021fcbd91ea14b1db29dfe5d14e1a6e046039467bf6ada7f8cc01b3 + name: "testnet P2SH address 2", + addr: "2MwbFpRpZWv4zREjbdLB9jVW3Q8xonpVeyE", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "testnet P2SH address 1 should also be supported in regtest", + addr: "2N6AoUj3KPS7wNGZXuCckh8YEWcSYNsGbqd", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + { + name: "testnet P2SH address 2 should also be supported in regtest", + addr: "2MwbFpRpZWv4zREjbdLB9jVW3Q8xonpVeyE", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2SH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressScriptHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } +} + +func Test_IsBtcAddressSupported_P2PKH(t *testing.T) { + tests := []struct { + name string + addr string + chainId int64 + supported bool + }{ + { + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + name: "mainnet P2PKH address 1", + addr: "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", + chainId: BtcMainnetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/1e3974386f071de7f65cabb57346c1a22ec9b3e211a96928a98149673f681237 + name: "testnet P2PKH address 1", + addr: "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + // https://mempool.space/testnet/tx/e48459f372727f2253b0ea8c71ded83e8270873b8a044feb3435fc7a799a648f + name: "testnet P2PKH address 2", + addr: "n1gXcqxmzwqHmqmgobe1XXuJaweSu69tZz", + chainId: BtcTestNetChain().ChainId, + supported: true, + }, + { + name: "testnet P2PKH address should also be supported in regtest", + addr: "mxpYha3UJKUgSwsAz2qYRqaDSwAkKZ3YEY", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + { + name: "testnet P2PKH address should also be supported in regtest", + addr: "n1gXcqxmzwqHmqmgobe1XXuJaweSu69tZz", + chainId: BtcRegtestChain().ChainId, + supported: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // it should be a P2PKH address + addr, err := DecodeBtcAddress(tt.addr, tt.chainId) + require.NoError(t, err) + _, ok := addr.(*btcutil.AddressPubKeyHash) + require.True(t, ok) + + // it should be supported + supported := IsBtcAddressSupported(addr) + require.Equal(t, tt.supported, supported) + }) + } } func TestConvertRecoverToError(t *testing.T) { diff --git a/proto/crosschain/events.proto b/proto/crosschain/events.proto index 05786446ed..3c955bf0f7 100644 --- a/proto/crosschain/events.proto +++ b/proto/crosschain/events.proto @@ -63,3 +63,8 @@ message EventCCTXGasPriceIncreased { string gas_price_increase = 2; string additional_fees = 3; } + +message EventERC20Whitelist { + string whitelist_cctx_index = 1; + string zrc20_address = 2; +} diff --git a/typescript/crosschain/events_pb.d.ts b/typescript/crosschain/events_pb.d.ts index 285f476b30..37a8e7cf2a 100644 --- a/typescript/crosschain/events_pb.d.ts +++ b/typescript/crosschain/events_pb.d.ts @@ -325,3 +325,32 @@ export declare class EventCCTXGasPriceIncreased extends Message | undefined, b: EventCCTXGasPriceIncreased | PlainMessage | undefined): boolean; } +/** + * @generated from message zetachain.zetacore.crosschain.EventERC20Whitelist + */ +export declare class EventERC20Whitelist extends Message { + /** + * @generated from field: string whitelist_cctx_index = 1; + */ + whitelistCctxIndex: string; + + /** + * @generated from field: string zrc20_address = 2; + */ + zrc20Address: string; + + constructor(data?: PartialMessage); + + static readonly runtime: typeof proto3; + static readonly typeName = "zetachain.zetacore.crosschain.EventERC20Whitelist"; + static readonly fields: FieldList; + + static fromBinary(bytes: Uint8Array, options?: Partial): EventERC20Whitelist; + + static fromJson(jsonValue: JsonValue, options?: Partial): EventERC20Whitelist; + + static fromJsonString(jsonString: string, options?: Partial): EventERC20Whitelist; + + static equals(a: EventERC20Whitelist | PlainMessage | undefined, b: EventERC20Whitelist | PlainMessage | undefined): boolean; +} + diff --git a/x/crosschain/keeper/evm_hooks.go b/x/crosschain/keeper/evm_hooks.go index a662352532..5ca8c8e814 100644 --- a/x/crosschain/keeper/evm_hooks.go +++ b/x/crosschain/keeper/evm_hooks.go @@ -7,7 +7,6 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" - "github.com/btcsuite/btcutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -310,9 +309,8 @@ func ValidateZrc20WithdrawEvent(event *zrc20.ZRC20Withdrawal, chainID int64) err if err != nil { return fmt.Errorf("ParseZRC20WithdrawalEvent: invalid address %s: %s", event.To, err) } - _, ok := addr.(*btcutil.AddressWitnessPubKeyHash) - if !ok { - return fmt.Errorf("ParseZRC20WithdrawalEvent: invalid address %s (not P2WPKH address)", event.To) + if !chains.IsBtcAddressSupported(addr) { + return fmt.Errorf("ParseZRC20WithdrawalEvent: unsupported address %s", string(event.To)) } } return nil diff --git a/x/crosschain/keeper/evm_hooks_test.go b/x/crosschain/keeper/evm_hooks_test.go index c75f638d3f..bd22eab38c 100644 --- a/x/crosschain/keeper/evm_hooks_test.go +++ b/x/crosschain/keeper/evm_hooks_test.go @@ -158,15 +158,16 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { btcMainNetWithdrawalEvent, err := crosschainkeeper.ParseZRC20WithdrawalEvent(*sample.GetValidZRC20WithdrawToBTC(t).Logs[3]) require.NoError(t, err) err = crosschainkeeper.ValidateZrc20WithdrawEvent(btcMainNetWithdrawalEvent, chains.BtcTestNetChain().ChainId) - require.ErrorContains(t, err, "address is not for network testnet3") + require.ErrorContains(t, err, "invalid address") }) - t.Run("unable to validate a event with an invalid address type", func(t *testing.T) { + t.Run("unable to validate an unsupported address type", func(t *testing.T) { btcMainNetWithdrawalEvent, err := crosschainkeeper.ParseZRC20WithdrawalEvent(*sample.GetValidZRC20WithdrawToBTC(t).Logs[3]) require.NoError(t, err) - btcMainNetWithdrawalEvent.To = []byte("1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3") - err = crosschainkeeper.ValidateZrc20WithdrawEvent(btcMainNetWithdrawalEvent, chains.BtcTestNetChain().ChainId) - require.ErrorContains(t, err, "decode address failed: unknown address type") + btcMainNetWithdrawalEvent.To = []byte("04b2891ba8cb491828db3ebc8a780d43b169e7b3974114e6e50f9bab6ec" + + "63c2f20f6d31b2025377d05c2a704d3bd799d0d56f3a8543d79a01ab6084a1cb204f260") + err = crosschainkeeper.ValidateZrc20WithdrawEvent(btcMainNetWithdrawalEvent, chains.BtcMainnetChain().ChainId) + require.ErrorContains(t, err, "unsupported address") }) } @@ -734,7 +735,8 @@ func TestKeeper_ProcessLogs(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) - chain := chains.BtcMainnetChain() + // use the wrong (testnet) chain ID to make the btc address parsing fail + chain := chains.BtcTestNetChain() chainID := chain.ChainId setSupportedChain(ctx, zk, chainID) SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index 0687aedbb1..fb2de5e45e 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -5,18 +5,15 @@ import ( "fmt" "math/big" - "github.com/zeta-chain/zetacore/pkg/coin" - "github.com/zeta-chain/zetacore/pkg/constant" - authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" - errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - + "github.com/zeta-chain/zetacore/pkg/coin" + "github.com/zeta-chain/zetacore/pkg/constant" + authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" "github.com/zeta-chain/zetacore/x/crosschain/types" fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" ) @@ -174,6 +171,16 @@ func (k msgServer) WhitelistERC20(goCtx context.Context, msg *types.MsgWhitelist commit() + err = ctx.EventManager().EmitTypedEvent( + &types.EventERC20Whitelist{ + Zrc20Address: zrc20Addr.Hex(), + WhitelistCctxIndex: index, + }, + ) + if err != nil { + return nil, errorsmod.Wrapf(err, "failed to emit event") + } + return &types.MsgWhitelistERC20Response{ Zrc20Address: zrc20Addr.Hex(), CctxIndex: index, diff --git a/x/crosschain/types/events.pb.go b/x/crosschain/types/events.pb.go index 39d521d4a7..6fd40fd641 100644 --- a/x/crosschain/types/events.pb.go +++ b/x/crosschain/types/events.pb.go @@ -568,6 +568,58 @@ func (m *EventCCTXGasPriceIncreased) GetAdditionalFees() string { return "" } +type EventERC20Whitelist struct { + WhitelistCctxIndex string `protobuf:"bytes,1,opt,name=whitelist_cctx_index,json=whitelistCctxIndex,proto3" json:"whitelist_cctx_index,omitempty"` + Zrc20Address string `protobuf:"bytes,2,opt,name=zrc20_address,json=zrc20Address,proto3" json:"zrc20_address,omitempty"` +} + +func (m *EventERC20Whitelist) Reset() { *m = EventERC20Whitelist{} } +func (m *EventERC20Whitelist) String() string { return proto.CompactTextString(m) } +func (*EventERC20Whitelist) ProtoMessage() {} +func (*EventERC20Whitelist) Descriptor() ([]byte, []int) { + return fileDescriptor_7398db8b12b87b9e, []int{6} +} +func (m *EventERC20Whitelist) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EventERC20Whitelist) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EventERC20Whitelist.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EventERC20Whitelist) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventERC20Whitelist.Merge(m, src) +} +func (m *EventERC20Whitelist) XXX_Size() int { + return m.Size() +} +func (m *EventERC20Whitelist) XXX_DiscardUnknown() { + xxx_messageInfo_EventERC20Whitelist.DiscardUnknown(m) +} + +var xxx_messageInfo_EventERC20Whitelist proto.InternalMessageInfo + +func (m *EventERC20Whitelist) GetWhitelistCctxIndex() string { + if m != nil { + return m.WhitelistCctxIndex + } + return "" +} + +func (m *EventERC20Whitelist) GetZrc20Address() string { + if m != nil { + return m.Zrc20Address + } + return "" +} + func init() { proto.RegisterType((*EventInboundFinalized)(nil), "zetachain.zetacore.crosschain.EventInboundFinalized") proto.RegisterType((*EventZrcWithdrawCreated)(nil), "zetachain.zetacore.crosschain.EventZrcWithdrawCreated") @@ -575,52 +627,57 @@ func init() { proto.RegisterType((*EventOutboundFailure)(nil), "zetachain.zetacore.crosschain.EventOutboundFailure") proto.RegisterType((*EventOutboundSuccess)(nil), "zetachain.zetacore.crosschain.EventOutboundSuccess") proto.RegisterType((*EventCCTXGasPriceIncreased)(nil), "zetachain.zetacore.crosschain.EventCCTXGasPriceIncreased") + proto.RegisterType((*EventERC20Whitelist)(nil), "zetachain.zetacore.crosschain.EventERC20Whitelist") } func init() { proto.RegisterFile("crosschain/events.proto", fileDescriptor_7398db8b12b87b9e) } var fileDescriptor_7398db8b12b87b9e = []byte{ - // 640 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x95, 0xdf, 0x4e, 0x14, 0x31, - 0x14, 0xc6, 0x19, 0xd8, 0x5d, 0x76, 0x0b, 0x2c, 0x66, 0x82, 0x52, 0x89, 0x4c, 0x90, 0xc4, 0x3f, - 0x17, 0xba, 0x1b, 0xe3, 0x1b, 0xb0, 0x11, 0x21, 0xc6, 0x60, 0x00, 0xa3, 0xe1, 0xa6, 0xe9, 0x4e, - 0x8f, 0x33, 0x8d, 0xb3, 0xed, 0xa6, 0xed, 0xc0, 0xc0, 0x53, 0x18, 0xdf, 0xc3, 0xc4, 0x07, 0xf0, - 0x01, 0xbc, 0xe4, 0xc2, 0x0b, 0x2f, 0x0d, 0xbc, 0x88, 0x69, 0x3b, 0x23, 0xec, 0x60, 0xf4, 0xc2, - 0x68, 0xe2, 0x5d, 0xcf, 0x77, 0x4e, 0x0f, 0xbf, 0x7e, 0x67, 0xd8, 0x83, 0x96, 0x63, 0x25, 0xb5, - 0x8e, 0x53, 0xca, 0x45, 0x1f, 0x0e, 0x41, 0x18, 0xdd, 0x1b, 0x2b, 0x69, 0x64, 0xb8, 0x7a, 0x02, - 0x86, 0x3a, 0xbd, 0xe7, 0x4e, 0x52, 0x41, 0xef, 0xa2, 0x76, 0x65, 0x29, 0x91, 0x89, 0x74, 0x95, - 0x7d, 0x7b, 0xf2, 0x97, 0xd6, 0xbf, 0xcc, 0xa0, 0xeb, 0x4f, 0x6c, 0x97, 0x6d, 0x31, 0x94, 0xb9, - 0x60, 0x9b, 0x5c, 0xd0, 0x8c, 0x9f, 0x00, 0x0b, 0xd7, 0xd0, 0xfc, 0x48, 0x27, 0xc4, 0x1c, 0x8f, - 0x81, 0xe4, 0x2a, 0xc3, 0xc1, 0x5a, 0x70, 0xbf, 0xb3, 0x8b, 0x46, 0x3a, 0xd9, 0x3f, 0x1e, 0xc3, - 0x4b, 0x95, 0x85, 0xab, 0x08, 0xc5, 0xb1, 0x29, 0x08, 0x17, 0x0c, 0x0a, 0x3c, 0xed, 0xf2, 0x1d, - 0xab, 0x6c, 0x5b, 0x21, 0xbc, 0x81, 0x5a, 0x1a, 0x04, 0x03, 0x85, 0x67, 0x5c, 0xaa, 0x8c, 0xc2, - 0x9b, 0xa8, 0x6d, 0x0a, 0x22, 0x55, 0xc2, 0x05, 0x6e, 0xb8, 0xcc, 0xac, 0x29, 0x76, 0x6c, 0x18, - 0x2e, 0xa1, 0x26, 0xd5, 0x1a, 0x0c, 0x6e, 0x3a, 0xdd, 0x07, 0xe1, 0x2d, 0x84, 0xb8, 0x20, 0xa6, - 0x20, 0x29, 0xd5, 0x29, 0x6e, 0xb9, 0x54, 0x9b, 0x8b, 0xfd, 0x62, 0x8b, 0xea, 0x34, 0xbc, 0x8b, - 0x16, 0xb9, 0x20, 0xc3, 0x4c, 0xc6, 0x6f, 0x49, 0x0a, 0x3c, 0x49, 0x0d, 0x9e, 0x75, 0x25, 0x0b, - 0x5c, 0x6c, 0x58, 0x75, 0xcb, 0x89, 0xe1, 0x0a, 0x6a, 0x2b, 0x88, 0x81, 0x1f, 0x82, 0xc2, 0x6d, - 0xdf, 0xa3, 0x8a, 0xc3, 0x3b, 0xa8, 0x5b, 0x9d, 0x89, 0x73, 0x0b, 0x77, 0x7c, 0x8b, 0x4a, 0x1d, - 0x58, 0xd1, 0xbe, 0x88, 0x8e, 0x64, 0x2e, 0x0c, 0x46, 0xfe, 0x45, 0x3e, 0x0a, 0xef, 0xa1, 0x45, - 0x05, 0x19, 0x3d, 0x06, 0x46, 0x46, 0xa0, 0x35, 0x4d, 0x00, 0xcf, 0xb9, 0x82, 0x6e, 0x29, 0x3f, - 0xf7, 0xaa, 0x75, 0x4c, 0xc0, 0x11, 0xd1, 0x86, 0x9a, 0x5c, 0xe3, 0x79, 0xef, 0x98, 0x80, 0xa3, - 0x3d, 0x27, 0x58, 0x0c, 0x9f, 0xfa, 0xd1, 0x66, 0xc1, 0x63, 0x78, 0xb5, 0xea, 0x72, 0x1b, 0xcd, - 0x7b, 0x2b, 0x4b, 0xd6, 0xae, 0x2b, 0x9a, 0xf3, 0x9a, 0x23, 0x5d, 0xff, 0x30, 0x8d, 0x96, 0xdd, - 0x58, 0x0f, 0x54, 0xfc, 0x8a, 0x9b, 0x94, 0x29, 0x7a, 0x34, 0x50, 0x40, 0xcd, 0xdf, 0x1c, 0x6c, - 0x9d, 0xab, 0x71, 0x85, 0xab, 0x36, 0xca, 0x66, 0x6d, 0x94, 0x97, 0x47, 0xd4, 0xfa, 0xed, 0x88, - 0x66, 0x7f, 0x3d, 0xa2, 0xf6, 0xc4, 0x88, 0x26, 0x9d, 0xef, 0xd4, 0x9c, 0x5f, 0xff, 0x18, 0x20, - 0xec, 0xfd, 0x02, 0x43, 0xff, 0x99, 0x61, 0x93, 0x6e, 0x34, 0x6a, 0x6e, 0x4c, 0x22, 0x37, 0xeb, - 0xc8, 0x9f, 0x02, 0xb4, 0xe4, 0x90, 0x77, 0x72, 0xe3, 0xff, 0x75, 0x29, 0xcf, 0x72, 0x05, 0x7f, - 0x8e, 0xbb, 0x8a, 0x90, 0xcc, 0x58, 0xf5, 0x87, 0x3d, 0x72, 0x47, 0x66, 0xac, 0xfc, 0x4a, 0x27, - 0xb9, 0x1a, 0x3f, 0xf9, 0x88, 0x0f, 0x69, 0x96, 0x03, 0x29, 0x07, 0xc3, 0x4a, 0xf4, 0x05, 0xa7, - 0xee, 0x96, 0xe2, 0x55, 0xfc, 0xbd, 0x3c, 0x8e, 0x41, 0xeb, 0xff, 0x04, 0xff, 0x7d, 0x80, 0x56, - 0x1c, 0xfe, 0x60, 0xb0, 0xff, 0xfa, 0x29, 0xd5, 0x2f, 0x14, 0x8f, 0x61, 0x5b, 0xc4, 0x0a, 0xa8, - 0x06, 0x56, 0x43, 0x0c, 0xea, 0x88, 0x0f, 0x50, 0x98, 0x50, 0x4d, 0xc6, 0xf6, 0x12, 0xe1, 0xe5, - 0xad, 0xf2, 0x25, 0xd7, 0x92, 0x5a, 0x37, 0xfb, 0xf3, 0x42, 0x19, 0xe3, 0x86, 0x4b, 0x41, 0x33, - 0xf2, 0x06, 0xa0, 0x7a, 0x55, 0xf7, 0x42, 0xde, 0x04, 0xd0, 0x1b, 0xcf, 0x3e, 0x9f, 0x45, 0xc1, - 0xe9, 0x59, 0x14, 0x7c, 0x3b, 0x8b, 0x82, 0x77, 0xe7, 0xd1, 0xd4, 0xe9, 0x79, 0x34, 0xf5, 0xf5, - 0x3c, 0x9a, 0x3a, 0x78, 0x94, 0x70, 0x93, 0xe6, 0xc3, 0x5e, 0x2c, 0x47, 0x7d, 0xbb, 0x1c, 0x1e, - 0xfa, 0xfd, 0x51, 0xed, 0x89, 0x7e, 0xd1, 0xbf, 0xb4, 0x55, 0xac, 0xf5, 0x7a, 0xd8, 0x72, 0x0b, - 0xe2, 0xf1, 0xf7, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd0, 0xf3, 0x4a, 0xbd, 0x70, 0x06, 0x00, 0x00, + // 690 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x95, 0xdf, 0x4e, 0x13, 0x41, + 0x14, 0xc6, 0x59, 0x68, 0x4b, 0x3b, 0xb4, 0xc5, 0xac, 0x55, 0x56, 0x22, 0x0d, 0xd6, 0xf8, 0xe7, + 0x42, 0x5b, 0xc4, 0x27, 0x90, 0x06, 0x84, 0x18, 0x83, 0x01, 0x0c, 0x86, 0x9b, 0xc9, 0x74, 0xf7, + 0xb8, 0x3b, 0x71, 0x3b, 0xd3, 0xcc, 0xcc, 0xd2, 0x85, 0xa7, 0x30, 0xbe, 0x87, 0x89, 0x0f, 0xe0, + 0x03, 0x78, 0xc9, 0x85, 0x17, 0x5e, 0x1a, 0x78, 0x11, 0x33, 0x33, 0xbb, 0x40, 0x17, 0xa3, 0x17, + 0x46, 0x13, 0xef, 0xe6, 0x7c, 0x67, 0xe6, 0xf0, 0x9b, 0xef, 0x5b, 0x3a, 0x68, 0xc1, 0x17, 0x5c, + 0x4a, 0x3f, 0x22, 0x94, 0xf5, 0xe0, 0x10, 0x98, 0x92, 0xdd, 0x91, 0xe0, 0x8a, 0xbb, 0x4b, 0xc7, + 0xa0, 0x88, 0xd1, 0xbb, 0x66, 0xc5, 0x05, 0x74, 0x2f, 0xf6, 0x2e, 0xb6, 0x42, 0x1e, 0x72, 0xb3, + 0xb3, 0xa7, 0x57, 0xf6, 0x50, 0xe7, 0xeb, 0x0c, 0xba, 0xb1, 0xae, 0xa7, 0x6c, 0xb1, 0x01, 0x4f, + 0x58, 0xb0, 0x41, 0x19, 0x89, 0xe9, 0x31, 0x04, 0xee, 0x32, 0xaa, 0x0f, 0x65, 0x88, 0xd5, 0xd1, + 0x08, 0x70, 0x22, 0x62, 0xcf, 0x59, 0x76, 0x1e, 0xd6, 0x76, 0xd0, 0x50, 0x86, 0x7b, 0x47, 0x23, + 0x78, 0x2d, 0x62, 0x77, 0x09, 0x21, 0xdf, 0x57, 0x29, 0xa6, 0x2c, 0x80, 0xd4, 0x9b, 0x36, 0xfd, + 0x9a, 0x56, 0xb6, 0xb4, 0xe0, 0xde, 0x44, 0x15, 0x09, 0x2c, 0x00, 0xe1, 0xcd, 0x98, 0x56, 0x56, + 0xb9, 0xb7, 0x50, 0x55, 0xa5, 0x98, 0x8b, 0x90, 0x32, 0xaf, 0x64, 0x3a, 0xb3, 0x2a, 0xdd, 0xd6, + 0xa5, 0xdb, 0x42, 0x65, 0x22, 0x25, 0x28, 0xaf, 0x6c, 0x74, 0x5b, 0xb8, 0xb7, 0x11, 0xa2, 0x0c, + 0xab, 0x14, 0x47, 0x44, 0x46, 0x5e, 0xc5, 0xb4, 0xaa, 0x94, 0xed, 0xa5, 0x9b, 0x44, 0x46, 0xee, + 0x7d, 0x34, 0x4f, 0x19, 0x1e, 0xc4, 0xdc, 0x7f, 0x87, 0x23, 0xa0, 0x61, 0xa4, 0xbc, 0x59, 0xb3, + 0xa5, 0x41, 0xd9, 0x9a, 0x56, 0x37, 0x8d, 0xe8, 0x2e, 0xa2, 0xaa, 0x00, 0x1f, 0xe8, 0x21, 0x08, + 0xaf, 0x6a, 0x67, 0xe4, 0xb5, 0x7b, 0x0f, 0x35, 0xf3, 0x35, 0x36, 0x6e, 0x79, 0x35, 0x3b, 0x22, + 0x57, 0xfb, 0x5a, 0xd4, 0x37, 0x22, 0x43, 0x9e, 0x30, 0xe5, 0x21, 0x7b, 0x23, 0x5b, 0xb9, 0x0f, + 0xd0, 0xbc, 0x80, 0x98, 0x1c, 0x41, 0x80, 0x87, 0x20, 0x25, 0x09, 0xc1, 0x9b, 0x33, 0x1b, 0x9a, + 0x99, 0xfc, 0xd2, 0xaa, 0xda, 0x31, 0x06, 0x63, 0x2c, 0x15, 0x51, 0x89, 0xf4, 0xea, 0xd6, 0x31, + 0x06, 0xe3, 0x5d, 0x23, 0x68, 0x0c, 0xdb, 0x3a, 0x1f, 0xd3, 0xb0, 0x18, 0x56, 0xcd, 0xa7, 0xdc, + 0x41, 0x75, 0x6b, 0x65, 0xc6, 0xda, 0x34, 0x9b, 0xe6, 0xac, 0x66, 0x48, 0x3b, 0x1f, 0xa7, 0xd1, + 0x82, 0x89, 0xf5, 0x40, 0xf8, 0xfb, 0x54, 0x45, 0x81, 0x20, 0xe3, 0xbe, 0x00, 0xa2, 0xfe, 0x66, + 0xb0, 0x45, 0xae, 0xd2, 0x15, 0xae, 0x42, 0x94, 0xe5, 0x42, 0x94, 0x97, 0x23, 0xaa, 0xfc, 0x36, + 0xa2, 0xd9, 0x5f, 0x47, 0x54, 0x9d, 0x88, 0x68, 0xd2, 0xf9, 0x5a, 0xc1, 0xf9, 0xce, 0x27, 0x07, + 0x79, 0xd6, 0x2f, 0x50, 0xe4, 0x9f, 0x19, 0x36, 0xe9, 0x46, 0xa9, 0xe0, 0xc6, 0x24, 0x72, 0xb9, + 0x88, 0xfc, 0xd9, 0x41, 0x2d, 0x83, 0xbc, 0x9d, 0x28, 0xfb, 0xaf, 0x4b, 0x68, 0x9c, 0x08, 0xf8, + 0x73, 0xdc, 0x25, 0x84, 0x78, 0x1c, 0xe4, 0x7f, 0xd8, 0x22, 0xd7, 0x78, 0x1c, 0x64, 0x5f, 0xe9, + 0x24, 0x57, 0xe9, 0x27, 0x1f, 0xf1, 0x21, 0x89, 0x13, 0xc0, 0x59, 0x30, 0x41, 0x86, 0xde, 0x30, + 0xea, 0x4e, 0x26, 0x5e, 0xc5, 0xdf, 0x4d, 0x7c, 0x1f, 0xa4, 0xfc, 0x4f, 0xf0, 0x3f, 0x38, 0x68, + 0xd1, 0xe0, 0xf7, 0xfb, 0x7b, 0x6f, 0x9e, 0x13, 0xf9, 0x4a, 0x50, 0x1f, 0xb6, 0x98, 0x2f, 0x80, + 0x48, 0x08, 0x0a, 0x88, 0x4e, 0x11, 0xf1, 0x11, 0x72, 0x43, 0x22, 0xf1, 0x48, 0x1f, 0xc2, 0x34, + 0x3b, 0x95, 0xdd, 0xe4, 0x5a, 0x58, 0x98, 0xa6, 0x7f, 0x5e, 0x48, 0x10, 0x50, 0x45, 0x39, 0x23, + 0x31, 0x7e, 0x0b, 0x90, 0xdf, 0xaa, 0x79, 0x21, 0x6f, 0x00, 0xc8, 0x4e, 0x8c, 0xae, 0x1b, 0xa6, + 0xf5, 0x9d, 0xfe, 0xea, 0xca, 0x7e, 0x44, 0x15, 0xc4, 0x54, 0x2a, 0x77, 0x05, 0xb5, 0xc6, 0x79, + 0x81, 0xaf, 0x60, 0xb9, 0xe7, 0xbd, 0xfe, 0x39, 0xdf, 0x5d, 0xd4, 0x38, 0x16, 0xfe, 0xea, 0x0a, + 0x26, 0x41, 0x20, 0x40, 0xca, 0x0c, 0xad, 0x6e, 0xc4, 0x67, 0x56, 0x5b, 0x7b, 0xf1, 0xe5, 0xb4, + 0xed, 0x9c, 0x9c, 0xb6, 0x9d, 0xef, 0xa7, 0x6d, 0xe7, 0xfd, 0x59, 0x7b, 0xea, 0xe4, 0xac, 0x3d, + 0xf5, 0xed, 0xac, 0x3d, 0x75, 0xf0, 0x24, 0xa4, 0x2a, 0x4a, 0x06, 0x5d, 0x9f, 0x0f, 0x7b, 0xfa, + 0x29, 0x7a, 0x6c, 0x5f, 0xab, 0xfc, 0x55, 0xea, 0xa5, 0xbd, 0x4b, 0x6f, 0x98, 0x0e, 0x5a, 0x0e, + 0x2a, 0xe6, 0x39, 0x7a, 0xfa, 0x23, 0x00, 0x00, 0xff, 0xff, 0xab, 0x7b, 0xbd, 0xa8, 0xde, 0x06, + 0x00, 0x00, } func (m *EventInboundFinalized) Marshal() (dAtA []byte, err error) { @@ -1048,6 +1105,43 @@ func (m *EventCCTXGasPriceIncreased) MarshalToSizedBuffer(dAtA []byte) (int, err return len(dAtA) - i, nil } +func (m *EventERC20Whitelist) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EventERC20Whitelist) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EventERC20Whitelist) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Zrc20Address) > 0 { + i -= len(m.Zrc20Address) + copy(dAtA[i:], m.Zrc20Address) + i = encodeVarintEvents(dAtA, i, uint64(len(m.Zrc20Address))) + i-- + dAtA[i] = 0x12 + } + if len(m.WhitelistCctxIndex) > 0 { + i -= len(m.WhitelistCctxIndex) + copy(dAtA[i:], m.WhitelistCctxIndex) + i = encodeVarintEvents(dAtA, i, uint64(len(m.WhitelistCctxIndex))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintEvents(dAtA []byte, offset int, v uint64) int { offset -= sovEvents(v) base := offset @@ -1277,6 +1371,23 @@ func (m *EventCCTXGasPriceIncreased) Size() (n int) { return n } +func (m *EventERC20Whitelist) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.WhitelistCctxIndex) + if l > 0 { + n += 1 + l + sovEvents(uint64(l)) + } + l = len(m.Zrc20Address) + if l > 0 { + n += 1 + l + sovEvents(uint64(l)) + } + return n +} + func sovEvents(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -2895,6 +3006,120 @@ func (m *EventCCTXGasPriceIncreased) Unmarshal(dAtA []byte) error { } return nil } +func (m *EventERC20Whitelist) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EventERC20Whitelist: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EventERC20Whitelist: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field WhitelistCctxIndex", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvents + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvents + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.WhitelistCctxIndex = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Zrc20Address", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvents + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvents + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Zrc20Address = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipEvents(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthEvents + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipEvents(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/crosschain/types/validate.go b/x/crosschain/types/validate.go index 713eb8d432..fb83c0d1d6 100644 --- a/x/crosschain/types/validate.go +++ b/x/crosschain/types/validate.go @@ -5,7 +5,6 @@ import ( "regexp" "cosmossdk.io/errors" - "github.com/btcsuite/btcutil" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/zeta-chain/zetacore/pkg/chains" @@ -58,9 +57,8 @@ func ValidateAddressForChain(address string, chainID int64) error { if err != nil { return fmt.Errorf("invalid address %s , chain %d: %s", address, chainID, err) } - _, ok := addr.(*btcutil.AddressWitnessPubKeyHash) - if !ok { - return fmt.Errorf(" invalid address %s (not P2WPKH address)", address) + if !chains.IsBtcAddressSupported(addr) { + return fmt.Errorf("unsupported address %s", address) } return nil } diff --git a/x/crosschain/types/validate_test.go b/x/crosschain/types/validate_test.go index 206f99b39c..c768a7d4ab 100644 --- a/x/crosschain/types/validate_test.go +++ b/x/crosschain/types/validate_test.go @@ -10,15 +10,22 @@ import ( ) func TestValidateAddressForChain(t *testing.T) { + // test for eth chain require.Error(t, types.ValidateAddressForChain("0x123", chains.GoerliChain().ChainId)) require.Error(t, types.ValidateAddressForChain("", chains.GoerliChain().ChainId)) require.Error(t, types.ValidateAddressForChain("%%%%", chains.GoerliChain().ChainId)) require.NoError(t, types.ValidateAddressForChain("0x792c127Fa3AC1D52F904056Baf1D9257391e7D78", chains.GoerliChain().ChainId)) - require.Error(t, types.ValidateAddressForChain("1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3", chains.BtcMainnetChain().ChainId)) + + // test for btc chain + require.NoError(t, types.ValidateAddressForChain("bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", chains.BtcMainnetChain().ChainId)) + require.NoError(t, types.ValidateAddressForChain("327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", chains.BtcMainnetChain().ChainId)) + require.NoError(t, types.ValidateAddressForChain("1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3", chains.BtcMainnetChain().ChainId)) require.Error(t, types.ValidateAddressForChain("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw", chains.BtcMainnetChain().ChainId)) require.Error(t, types.ValidateAddressForChain("", chains.BtcRegtestChain().ChainId)) require.NoError(t, types.ValidateAddressForChain("bc1qysd4sp9q8my59ul9wsf5rvs9p387hf8vfwatzu", chains.BtcMainnetChain().ChainId)) require.NoError(t, types.ValidateAddressForChain("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw", chains.BtcRegtestChain().ChainId)) + + // test for zeta chain require.NoError(t, types.ValidateAddressForChain("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw", chains.ZetaChainMainnet().ChainId)) require.NoError(t, types.ValidateAddressForChain("0x792c127Fa3AC1D52F904056Baf1D9257391e7D78", chains.ZetaChainMainnet().ChainId)) } diff --git a/x/observer/keeper/msg_server_reset_chain_nonces.go b/x/observer/keeper/msg_server_reset_chain_nonces.go index d57cee6593..853ef1ad16 100644 --- a/x/observer/keeper/msg_server_reset_chain_nonces.go +++ b/x/observer/keeper/msg_server_reset_chain_nonces.go @@ -10,7 +10,6 @@ import ( ) // ResetChainNonces handles resetting chain nonces -// Authorized: admin policy group 2 (admin update) func (k msgServer) ResetChainNonces(goCtx context.Context, msg *types.MsgResetChainNonces) (*types.MsgResetChainNoncesResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) if !k.GetAuthorityKeeper().IsAuthorized(ctx, msg.Creator, authoritytypes.PolicyType_groupOperational) { diff --git a/zetaclient/bitcoin/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index 2fe823811b..51bc1ab758 100644 --- a/zetaclient/bitcoin/bitcoin_client.go +++ b/zetaclient/bitcoin/bitcoin_client.go @@ -8,7 +8,6 @@ import ( "math/big" "os" "sort" - "strconv" "sync" "sync/atomic" "time" @@ -26,7 +25,6 @@ import ( "github.com/rs/zerolog" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" - "github.com/zeta-chain/zetacore/pkg/constant" "github.com/zeta-chain/zetacore/pkg/proofs" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -45,19 +43,22 @@ import ( ) const ( - // The starting height (Bitcoin mainnet) from which dynamic depositor fee will take effect - DynamicDepositorFeeHeight = 834500 + DynamicDepositorFeeHeight = 834500 // The starting height (Bitcoin mainnet) from which dynamic depositor fee will take effect + maxHeightDiff = 10000 // in case the last block is too old when the observer starts + btcBlocksPerDay = 144 // for LRU block cache size + bigValueSats = 200000000 // 2 BTC + bigValueConfirmationCount = 6 // 6 confirmations for value >= 2 BTC ) var _ interfaces.ChainClient = &BTCChainClient{} type BTCLog struct { - ChainLogger zerolog.Logger - WatchInTx zerolog.Logger - ObserveOutTx zerolog.Logger - WatchUTXOS zerolog.Logger - WatchGasPrice zerolog.Logger - Compliance zerolog.Logger + Chain zerolog.Logger // The parent logger for the chain + InTx zerolog.Logger // The logger for incoming transactions + OutTx zerolog.Logger // The logger for outgoing transactions + UTXOS zerolog.Logger // The logger for UTXOs management + GasPrice zerolog.Logger // The logger for gas price + Compliance zerolog.Logger // The logger for compliance checks } // BTCChainClient represents a chain configuration for Bitcoin @@ -89,27 +90,21 @@ type BTCChainClient struct { BlockCache *lru.Cache } -const ( - maxHeightDiff = 10000 // in case the last block is too old when the observer starts - btcBlocksPerDay = 144 // for LRU block cache size - bigValueSats = 200000000 // 2 BTC - bigValueConfirmationCount = 6 // 6 confirmations for value >= 2 BTC -) - func (ob *BTCChainClient) WithZetaClient(bridge *zetabridge.ZetaCoreBridge) { ob.Mu.Lock() defer ob.Mu.Unlock() ob.zetaClient = bridge } + func (ob *BTCChainClient) WithLogger(logger zerolog.Logger) { ob.Mu.Lock() defer ob.Mu.Unlock() ob.logger = BTCLog{ - ChainLogger: logger, - WatchInTx: logger.With().Str("module", "WatchInTx").Logger(), - ObserveOutTx: logger.With().Str("module", "observeOutTx").Logger(), - WatchUTXOS: logger.With().Str("module", "WatchUTXOS").Logger(), - WatchGasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), + Chain: logger, + InTx: logger.With().Str("module", "WatchInTx").Logger(), + OutTx: logger.With().Str("module", "WatchOutTx").Logger(), + UTXOS: logger.With().Str("module", "WatchUTXOS").Logger(), + GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), } } @@ -161,12 +156,12 @@ func NewBitcoinClient( ob.Mu = &sync.Mutex{} chainLogger := loggers.Std.With().Str("chain", chain.ChainName.String()).Logger() ob.logger = BTCLog{ - ChainLogger: chainLogger, - WatchInTx: chainLogger.With().Str("module", "WatchInTx").Logger(), - ObserveOutTx: chainLogger.With().Str("module", "observeOutTx").Logger(), - WatchUTXOS: chainLogger.With().Str("module", "WatchUTXOS").Logger(), - WatchGasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: loggers.Compliance, + Chain: chainLogger, + InTx: chainLogger.With().Str("module", "WatchInTx").Logger(), + OutTx: chainLogger.With().Str("module", "WatchOutTx").Logger(), + UTXOS: chainLogger.With().Str("module", "WatchUTXOS").Logger(), + GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), + Compliance: loggers.Compliance, } ob.zetaClient = bridge @@ -181,7 +176,7 @@ func NewBitcoinClient( } ob.params = *chainParams // initialize the Client - ob.logger.ChainLogger.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) + ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) connCfg := &rpcclient.ConnConfig{ Host: btcCfg.RPCHost, User: btcCfg.RPCUsername, @@ -202,7 +197,7 @@ func NewBitcoinClient( ob.BlockCache, err = lru.New(btcBlocksPerDay) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("failed to create bitcoin block cache") + ob.logger.Chain.Error().Err(err).Msg("failed to create bitcoin block cache") return nil, err } @@ -216,55 +211,58 @@ func NewBitcoinClient( } func (ob *BTCChainClient) Start() { - ob.logger.ChainLogger.Info().Msgf("BitcoinChainClient is starting") - go ob.WatchInTx() - go ob.observeOutTx() - go ob.WatchUTXOS() - go ob.WatchGasPrice() - go ob.ExternalChainWatcherForNewInboundTrackerSuggestions() - go ob.RPCStatus() + ob.logger.Chain.Info().Msgf("BitcoinChainClient is starting") + go ob.WatchInTx() // watch bitcoin chain for incoming txs and post votes to zetacore + go ob.WatchOutTx() // watch bitcoin chain for outgoing txs status + go ob.WatchUTXOS() // watch bitcoin chain for UTXOs owned by the TSS address + go ob.WatchGasPrice() // watch bitcoin chain for gas rate and post to zetacore + go ob.WatchIntxTracker() // watch zetacore for bitcoin intx trackers + go ob.WatchRPCStatus() // watch the RPC status of the bitcoin chain } -func (ob *BTCChainClient) RPCStatus() { - ob.logger.ChainLogger.Info().Msgf("RPCStatus is starting") +// WatchRPCStatus watches the RPC status of the Bitcoin chain +func (ob *BTCChainClient) WatchRPCStatus() { + ob.logger.Chain.Info().Msgf("RPCStatus is starting") ticker := time.NewTicker(60 * time.Second) for { select { case <-ticker.C: - //ob.logger.ChainLogger.Info().Msgf("RPCStatus is running") + if !ob.GetChainParams().IsSupported { + continue + } bn, err := ob.rpcClient.GetBlockCount() if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC status check: RPC down? ") + ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } hash, err := ob.rpcClient.GetBlockHash(bn) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC status check: RPC down? ") + ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } header, err := ob.rpcClient.GetBlockHeader(hash) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC status check: RPC down? ") + ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } blockTime := header.Timestamp elapsedSeconds := time.Since(blockTime).Seconds() if elapsedSeconds > 1200 { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC status check: RPC down? ") + ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } tssAddr := ob.Tss.BTCAddressWitnessPubkeyHash() res, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC status check: can't list utxos of TSS address; wallet or loaded? TSS address is not imported? ") + ob.logger.Chain.Error().Err(err).Msg("RPC status check: can't list utxos of TSS address; wallet or loaded? TSS address is not imported? ") continue } if len(res) == 0 { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC status check: TSS address has no utxos; TSS address is not imported? ") + ob.logger.Chain.Error().Err(err).Msg("RPC status check: TSS address has no utxos; TSS address is not imported? ") continue } - ob.logger.ChainLogger.Info().Msgf("[OK] RPC status check: latest block number %d, timestamp %s (%.fs ago), tss addr %s, #utxos: %d", bn, blockTime, elapsedSeconds, tssAddr, len(res)) + ob.logger.Chain.Info().Msgf("[OK] RPC status check: latest block number %d, timestamp %s (%.fs ago), tss addr %s, #utxos: %d", bn, blockTime, elapsedSeconds, tssAddr, len(res)) case <-ob.stop: return @@ -273,9 +271,9 @@ func (ob *BTCChainClient) RPCStatus() { } func (ob *BTCChainClient) Stop() { - ob.logger.ChainLogger.Info().Msgf("ob %s is stopping", ob.chain.String()) + ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) close(ob.stop) // this notifies all goroutines to stop - ob.logger.ChainLogger.Info().Msgf("%s observer stopped", ob.chain.String()) + ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) } func (ob *BTCChainClient) SetLastBlockHeight(height int64) { @@ -322,31 +320,36 @@ func (ob *BTCChainClient) GetBaseGasPrice() *big.Int { return big.NewInt(0) } +// WatchInTx watches Bitcoin chain for incoming txs and post votes to zetacore func (ob *BTCChainClient) WatchInTx() { ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchInTx", ob.GetChainParams().InTxTicker) if err != nil { - ob.logger.WatchInTx.Error().Err(err).Msg("WatchInTx error") + ob.logger.InTx.Error().Err(err).Msg("error creating ticker") return } defer ticker.Stop() + ob.logger.InTx.Info().Msgf("WatchInTx started for chain %d", ob.chain.ChainId) for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } err := ob.ObserveInTx() if err != nil { - ob.logger.WatchInTx.Error().Err(err).Msg("WatchInTx error observing in tx") + ob.logger.InTx.Error().Err(err).Msg("WatchInTx error observing in tx") } - ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.WatchInTx) + ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.InTx) case <-ob.stop: - ob.logger.WatchInTx.Info().Msg("WatchInTx stopped") + ob.logger.InTx.Info().Msgf("WatchInTx stopped for chain %d", ob.chain.ChainId) return } } } func (ob *BTCChainClient) postBlockHeader(tip int64) error { - ob.logger.WatchInTx.Info().Msgf("postBlockHeader: tip %d", tip) + ob.logger.InTx.Info().Msgf("postBlockHeader: tip %d", tip) bn := tip res, err := ob.zetaClient.GetBlockHeaderStateByChain(ob.chain.ChainId) if err == nil && res.BlockHeaderState != nil && res.BlockHeaderState.EarliestHeight > 0 { @@ -363,7 +366,7 @@ func (ob *BTCChainClient) postBlockHeader(tip int64) error { var headerBuf bytes.Buffer err = res2.Header.Serialize(&headerBuf) if err != nil { // should never happen - ob.logger.WatchInTx.Error().Err(err).Msgf("error serializing bitcoin block header: %d", bn) + ob.logger.InTx.Error().Err(err).Msgf("error serializing bitcoin block header: %d", bn) return err } blockHash := res2.Header.BlockHash() @@ -373,9 +376,9 @@ func (ob *BTCChainClient) postBlockHeader(tip int64) error { res2.Block.Height, proofs.NewBitcoinHeader(headerBuf.Bytes()), ) - ob.logger.WatchInTx.Info().Msgf("posted block header %d: %s", bn, blockHash) + ob.logger.InTx.Info().Msgf("posted block header %d: %s", bn, blockHash) if err != nil { // error shouldn't block the process - ob.logger.WatchInTx.Error().Err(err).Msgf("error posting bitcoin block header: %d", bn) + ob.logger.InTx.Error().Err(err).Msgf("error posting bitcoin block header: %d", bn) } return err } @@ -418,18 +421,18 @@ func (ob *BTCChainClient) ObserveInTx() error { bn := lastScanned + 1 res, err := ob.GetBlockByNumberCached(bn) if err != nil { - ob.logger.WatchInTx.Error().Err(err).Msgf("observeInTxBTC: error getting bitcoin block %d", bn) + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error getting bitcoin block %d", bn) return err } - ob.logger.WatchInTx.Info().Msgf("observeInTxBTC: block %d has %d txs, current block %d, last block %d", + ob.logger.InTx.Info().Msgf("observeInTxBTC: block %d has %d txs, current block %d, last block %d", bn, len(res.Block.Tx), cnt, lastScanned) // print some debug information if len(res.Block.Tx) > 1 { for idx, tx := range res.Block.Tx { - ob.logger.WatchInTx.Debug().Msgf("BTC InTX | %d: %s\n", idx, tx.Txid) + ob.logger.InTx.Debug().Msgf("BTC InTX | %d: %s\n", idx, tx.Txid) for vidx, vout := range tx.Vout { - ob.logger.WatchInTx.Debug().Msgf("vout %d \n value: %v\n scriptPubKey: %v\n", vidx, vout.Value, vout.ScriptPubKey.Hex) + ob.logger.InTx.Debug().Msgf("vout %d \n value: %v\n scriptPubKey: %v\n", vidx, vout.Value, vout.ScriptPubKey.Hex) } } } @@ -438,25 +441,30 @@ func (ob *BTCChainClient) ObserveInTx() error { if flags.BlockHeaderVerificationFlags != nil && flags.BlockHeaderVerificationFlags.IsBtcTypeChainEnabled { err = ob.postBlockHeader(bn) if err != nil { - ob.logger.WatchInTx.Warn().Err(err).Msgf("observeInTxBTC: error posting block header %d", bn) + ob.logger.InTx.Warn().Err(err).Msgf("observeInTxBTC: error posting block header %d", bn) } } if len(res.Block.Tx) > 1 { // get depositor fee - depositorFee := CalcDepositorFee(res.Block, ob.chain.ChainId, ob.netParams, ob.logger.WatchInTx) + depositorFee := CalcDepositorFee(res.Block, ob.chain.ChainId, ob.netParams, ob.logger.InTx) // filter incoming txs to TSS address tssAddress := ob.Tss.BTCAddress() // #nosec G701 always positive - inTxs := FilterAndParseIncomingTx( + inTxs, err := FilterAndParseIncomingTx( + ob.rpcClient, res.Block.Tx, uint64(res.Block.Height), tssAddress, - &ob.logger.WatchInTx, + ob.logger.InTx, ob.netParams, depositorFee, ) + if err != nil { + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error filtering incoming txs for block %d", bn) + return err // we have to re-scan this block next time + } // post inbound vote message to zetacore for _, inTx := range inTxs { @@ -464,10 +472,10 @@ func (ob *BTCChainClient) ObserveInTx() error { if msg != nil { zetaHash, ballot, err := ob.zetaClient.PostVoteInbound(zetabridge.PostVoteInboundGasLimit, zetabridge.PostVoteInboundExecutionGasLimit, msg) if err != nil { - ob.logger.WatchInTx.Error().Err(err).Msgf("observeInTxBTC: error posting to zeta core for tx %s", inTx.TxHash) + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error posting to zeta core for tx %s", inTx.TxHash) return err // we have to re-scan this block next time } else if zetaHash != "" { - ob.logger.WatchInTx.Info().Msgf("observeInTxBTC: PostVoteInbound zeta tx hash: %s inTx %s ballot %s fee %v", + ob.logger.InTx.Info().Msgf("observeInTxBTC: PostVoteInbound zeta tx hash: %s inTx %s ballot %s fee %v", zetaHash, inTx.TxHash, ballot, depositorFee) } } @@ -478,7 +486,7 @@ func (ob *BTCChainClient) ObserveInTx() error { ob.SetLastBlockHeightScanned(bn) // #nosec G701 always positive if err := ob.db.Save(clienttypes.ToLastBlockSQLType(uint64(bn))).Error; err != nil { - ob.logger.WatchInTx.Error().Err(err).Msgf("observeInTxBTC: error writing last scanned block %d to db", bn) + ob.logger.InTx.Error().Err(err).Msgf("observeInTxBTC: error writing last scanned block %d to db", bn) } } @@ -530,7 +538,7 @@ func (ob *BTCChainClient) IsSendOutTxProcessed(cctx *types.CrossChainTx, logger if txResult == nil { // check failed, try again next time return false, false, nil } else if inMempool { // still in mempool (should avoid unnecessary Tss keysign) - ob.logger.ObserveOutTx.Info().Msgf("IsSendOutTxProcessed: outTx %s is still in mempool", outTxID) + ob.logger.OutTx.Info().Msgf("IsSendOutTxProcessed: outTx %s is still in mempool", outTxID) return true, false, nil } // included @@ -541,7 +549,7 @@ func (ob *BTCChainClient) IsSendOutTxProcessed(cctx *types.CrossChainTx, logger if res == nil { return false, false, nil } - ob.logger.ObserveOutTx.Info().Msgf("IsSendOutTxProcessed: setIncludedTx succeeded for outTx %s", outTxID) + ob.logger.OutTx.Info().Msgf("IsSendOutTxProcessed: setIncludedTx succeeded for outTx %s", outTxID) } // It's safe to use cctx's amount to post confirmation because it has already been verified in observeOutTx() @@ -573,10 +581,11 @@ func (ob *BTCChainClient) IsSendOutTxProcessed(cctx *types.CrossChainTx, logger return true, true, nil } +// WatchGasPrice watches Bitcoin chain for gas rate and post to zetacore func (ob *BTCChainClient) WatchGasPrice() { ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchGasPrice", ob.GetChainParams().GasPriceTicker) if err != nil { - ob.logger.WatchGasPrice.Error().Err(err).Msg("WatchGasPrice error") + ob.logger.GasPrice.Error().Err(err).Msg("error creating ticker") return } @@ -584,13 +593,16 @@ func (ob *BTCChainClient) WatchGasPrice() { for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } err := ob.PostGasPrice() if err != nil { - ob.logger.WatchGasPrice.Error().Err(err).Msg("PostGasPrice error on " + ob.chain.String()) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.WatchGasPrice) + ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) case <-ob.stop: - ob.logger.WatchGasPrice.Info().Msg("WatchGasPrice stopped") + ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.chain.ChainId) return } } @@ -605,7 +617,7 @@ func (ob *BTCChainClient) PostGasPrice() error { // #nosec G701 always in range zetaHash, err := ob.zetaClient.PostGasPrice(ob.chain, 1, "100", uint64(bn)) if err != nil { - ob.logger.WatchGasPrice.Err(err).Msg("PostGasPrice:") + ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err } _ = zetaHash @@ -631,7 +643,7 @@ func (ob *BTCChainClient) PostGasPrice() error { // #nosec G701 always positive zetaHash, err := ob.zetaClient.PostGasPrice(ob.chain, feeRatePerByte.Uint64(), "100", uint64(bn)) if err != nil { - ob.logger.WatchGasPrice.Err(err).Msg("PostGasPrice:") + ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err } _ = zetaHash @@ -652,33 +664,34 @@ type BTCInTxEvnet struct { // vout0: p2wpkh to the TSS address (targetAddress) // vout1: OP_RETURN memo, base64 encoded func FilterAndParseIncomingTx( + rpcClient interfaces.BTCRPCClient, txs []btcjson.TxRawResult, blockNumber uint64, - targetAddress string, - logger *zerolog.Logger, + tssAddress string, + logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, -) []*BTCInTxEvnet { +) ([]*BTCInTxEvnet, error) { inTxs := make([]*BTCInTxEvnet, 0) for idx, tx := range txs { if idx == 0 { continue // the first tx is coinbase; we do not process coinbase tx } - inTx, err := GetBtcEvent(tx, targetAddress, blockNumber, logger, netParams, depositorFee) + inTx, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, depositorFee) if err != nil { - logger.Error().Err(err).Msgf("FilterAndParseIncomingTx: error getting btc event for tx %s in block %d", tx.Txid, blockNumber) - continue + // 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) } if inTx != nil { inTxs = append(inTxs, inTx) logger.Info().Msgf("FilterAndParseIncomingTx: found btc event for tx %s in block %d", tx.Txid, blockNumber) } } - return inTxs + return inTxs, nil } func (ob *BTCChainClient) GetInboundVoteMessageFromBtcEvent(inTx *BTCInTxEvnet) *types.MsgVoteOnObservedInboundTx { - ob.logger.WatchInTx.Debug().Msgf("Processing inTx: %s", inTx.TxHash) + ob.logger.InTx.Debug().Msgf("Processing inTx: %s", inTx.TxHash) amount := big.NewFloat(inTx.Value) amount = amount.Mul(amount, big.NewFloat(1e8)) amountInt, _ := amount.Int(nil) @@ -715,18 +728,21 @@ func (ob *BTCChainClient) IsInTxRestricted(inTx *BTCInTxEvnet) bool { receiver = parsedAddress.Hex() } if config.ContainRestrictedAddress(inTx.FromAddress, receiver) { - compliance.PrintComplianceLog(ob.logger.WatchInTx, ob.logger.Compliance, + compliance.PrintComplianceLog(ob.logger.InTx, ob.logger.Compliance, false, ob.chain.ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") return true } return false } +// GetBtcEvent either returns a valid BTCInTxEvent or nil +// Note: the caller should retry the tx on error (e.g., GetSenderAddressByVin failed) func GetBtcEvent( + rpcClient interfaces.BTCRPCClient, tx btcjson.TxRawResult, - targetAddress string, + tssAddress string, blockNumber uint64, - logger *zerolog.Logger, + logger zerolog.Logger, netParams *chaincfg.Params, depositorFee float64, ) (*BTCInTxEvnet, error) { @@ -734,74 +750,46 @@ func GetBtcEvent( var value float64 var memo []byte if len(tx.Vout) >= 2 { - // first vout must to addressed to the targetAddress with p2wpkh scriptPubKey - out := tx.Vout[0] - script := out.ScriptPubKey.Hex - if len(script) == 44 && script[:4] == "0014" { // segwit output: 0x00 + 20 bytes of pubkey hash - hash, err := hex.DecodeString(script[4:]) - if err != nil { - return nil, err - } - wpkhAddress, err := btcutil.NewAddressWitnessPubKeyHash(hash, netParams) - if err != nil { + // 1st vout must have tss address as receiver with p2wpkh scriptPubKey + vout0 := tx.Vout[0] + script := vout0.ScriptPubKey.Hex + if len(script) == 44 && script[:4] == "0014" { // P2WPKH output: 0x00 + 20 bytes of pubkey hash + receiver, err := DecodeScriptP2WPKH(vout0.ScriptPubKey.Hex, netParams) + if err != nil { // should never happen return nil, err } - if wpkhAddress.EncodeAddress() != targetAddress { - return nil, nil // irrelevant tx to us, skip + // skip irrelevant tx to us + if receiver != tssAddress { + return nil, nil } // deposit amount has to be no less than the minimum depositor fee - if out.Value < depositorFee { - return nil, fmt.Errorf("btc deposit amount %v in txid %s is less than depositor fee %v", value, tx.Txid, depositorFee) + if vout0.Value < depositorFee { + logger.Info().Msgf("GetBtcEvent: btc deposit amount %v in txid %s is less than depositor fee %v", vout0.Value, tx.Txid, depositorFee) + return nil, nil } - value = out.Value - depositorFee + value = vout0.Value - depositorFee - out = tx.Vout[1] - script = out.ScriptPubKey.Hex - if len(script) >= 4 && script[:2] == "6a" { // OP_RETURN - memoSize, err := strconv.ParseInt(script[2:4], 16, 32) - if err != nil { - return nil, errors.Wrapf(err, "error decoding pubkey hash") - } - if int(memoSize) != (len(script)-4)/2 { - return nil, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(script)-4)/2) - } - memoBytes, err := hex.DecodeString(script[4:]) - if err != nil { - logger.Warn().Err(err).Msgf("error hex decoding memo") - return nil, fmt.Errorf("error hex decoding memo: %s", err) - } - if bytes.Equal(memoBytes, []byte(constant.DonationMessage)) { - logger.Info().Msgf("donation tx: %s; value %f", tx.Txid, value) - return nil, fmt.Errorf("donation tx: %s; value %f", tx.Txid, value) - } - memo = memoBytes - found = true + // 2nd vout must be a valid OP_RETURN memo + vout1 := tx.Vout[1] + memo, found, err = DecodeOpReturnMemo(vout1.ScriptPubKey.Hex, tx.Txid) + if err != nil { + logger.Error().Err(err).Msgf("GetBtcEvent: error decoding OP_RETURN memo: %s", vout1.ScriptPubKey.Hex) + return nil, nil } } } + // event found, get sender address if found { - logger.Info().Msgf("found bitcoin intx: %s", tx.Txid) - var fromAddress string - if len(tx.Vin) > 0 { - vin := tx.Vin[0] - //log.Info().Msgf("vin: %v", vin.Witness) - if len(vin.Witness) == 2 { - pk := vin.Witness[1] - pkBytes, err := hex.DecodeString(pk) - if err != nil { - return nil, errors.Wrapf(err, "error decoding pubkey") - } - hash := btcutil.Hash160(pkBytes) - addr, err := btcutil.NewAddressWitnessPubKeyHash(hash, netParams) - if err != nil { - return nil, errors.Wrapf(err, "error decoding pubkey hash") - } - fromAddress = addr.EncodeAddress() - } + if len(tx.Vin) == 0 { // should never happen + return nil, fmt.Errorf("GetBtcEvent: no input found for intx: %s", tx.Txid) + } + fromAddress, err := GetSenderAddressByVin(rpcClient, tx.Vin[0], netParams) + if err != nil { + return nil, errors.Wrapf(err, "error getting sender address for intx: %s", tx.Txid) } return &BTCInTxEvnet{ FromAddress: fromAddress, - ToAddress: targetAddress, + ToAddress: tssAddress, Value: value, MemoBytes: memo, BlockNumber: blockNumber, @@ -811,10 +799,50 @@ func GetBtcEvent( return nil, nil } +// GetSenderAddressByVin get the sender address from the previous transaction +func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, net *chaincfg.Params) (string, error) { + // query previous raw transaction by txid + // GetTransaction requires reconfiguring the bitcoin node (txindex=1), so we use GetRawTransaction instead + hash, err := chainhash.NewHashFromStr(vin.Txid) + if err != nil { + return "", err + } + tx, err := rpcClient.GetRawTransaction(hash) + if err != nil { + return "", errors.Wrapf(err, "error getting raw transaction %s", vin.Txid) + } + // #nosec G701 - always in range + if len(tx.MsgTx().TxOut) <= int(vin.Vout) { + return "", fmt.Errorf("vout index %d out of range for tx %s", vin.Vout, vin.Txid) + } + + // decode sender address from previous pkScript + pkScript := tx.MsgTx().TxOut[vin.Vout].PkScript + scriptHex := hex.EncodeToString(pkScript) + if IsPkScriptP2TR(pkScript) { + return DecodeScriptP2TR(scriptHex, net) + } + if IsPkScriptP2WSH(pkScript) { + return DecodeScriptP2WSH(scriptHex, net) + } + if IsPkScriptP2WPKH(pkScript) { + return DecodeScriptP2WPKH(scriptHex, net) + } + if IsPkScriptP2SH(pkScript) { + return DecodeScriptP2SH(scriptHex, net) + } + if IsPkScriptP2PKH(pkScript) { + return DecodeScriptP2PKH(scriptHex, net) + } + // sender address not found, return nil and move on to the next tx + return "", nil +} + +// WatchUTXOS watches bitcoin chain for UTXOs owned by the TSS address func (ob *BTCChainClient) WatchUTXOS() { ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchUTXOS", ob.GetChainParams().WatchUtxoTicker) if err != nil { - ob.logger.WatchUTXOS.Error().Err(err).Msg("WatchUTXOS error") + ob.logger.UTXOS.Error().Err(err).Msg("error creating ticker") return } @@ -822,13 +850,16 @@ func (ob *BTCChainClient) WatchUTXOS() { for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } err := ob.FetchUTXOS() if err != nil { - ob.logger.WatchUTXOS.Error().Err(err).Msg("error fetching btc utxos") + ob.logger.UTXOS.Error().Err(err).Msg("error fetching btc utxos") } - ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.WatchUTXOS) + ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.UTXOS) case <-ob.stop: - ob.logger.WatchUTXOS.Info().Msg("WatchUTXOS stopped") + ob.logger.UTXOS.Info().Msgf("WatchUTXOS stopped for chain %d", ob.chain.ChainId) return } } @@ -837,7 +868,7 @@ func (ob *BTCChainClient) WatchUTXOS() { func (ob *BTCChainClient) FetchUTXOS() error { defer func() { if err := recover(); err != nil { - ob.logger.WatchUTXOS.Error().Msgf("BTC fetchUTXOS: caught panic error: %v", err) + ob.logger.UTXOS.Error().Msgf("BTC fetchUTXOS: caught panic error: %v", err) } }() @@ -911,7 +942,7 @@ func (ob *BTCChainClient) refreshPendingNonce() { // get pending nonces from zetabridge p, err := ob.zetaClient.GetPendingNoncesByChain(ob.chain.ChainId) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") + ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") } // increase pending nonce if lagged behind @@ -925,14 +956,14 @@ func (ob *BTCChainClient) refreshPendingNonce() { // get the last included outTx hash txid, err := ob.getOutTxidByNonce(nonceLow-1, false) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("refreshPendingNonce: error getting last outTx txid") + ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting last outTx txid") } // set 'NonceLow' as the new pending nonce ob.Mu.Lock() defer ob.Mu.Unlock() ob.pendingNonce = nonceLow - ob.logger.ChainLogger.Info().Msgf("refreshPendingNonce: increase pending nonce to %d with txid %s", ob.pendingNonce, txid) + ob.logger.Chain.Info().Msgf("refreshPendingNonce: increase pending nonce to %d with txid %s", ob.pendingNonce, txid) } } @@ -954,7 +985,7 @@ func (ob *BTCChainClient) getOutTxidByNonce(nonce uint64, test bool) (string, er return "", fmt.Errorf("getOutTxidByNonce: cannot find outTx txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := ob.GetTxResultByHash(txid) + _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) if err != nil { return "", errors.Wrapf(err, "getOutTxidByNonce: error getting outTx result for nonce %d hash %s", nonce, txid) } @@ -972,10 +1003,10 @@ func (ob *BTCChainClient) findNonceMarkUTXO(nonce uint64, txid string) (int, err for i, utxo := range ob.utxos { sats, err := GetSatoshis(utxo.Amount) if err != nil { - ob.logger.ObserveOutTx.Error().Err(err).Msgf("findNonceMarkUTXO: error getting satoshis for utxo %v", utxo) + ob.logger.OutTx.Error().Err(err).Msgf("findNonceMarkUTXO: error getting satoshis for utxo %v", utxo) } - if utxo.Address == tssAddress && sats == amount && utxo.TxID == txid { - ob.logger.ObserveOutTx.Info().Msgf("findNonceMarkUTXO: found nonce-mark utxo with txid %s, amount %d satoshi", utxo.TxID, sats) + if utxo.Address == tssAddress && sats == amount && utxo.TxID == txid && utxo.Vout == 0 { + ob.logger.OutTx.Info().Msgf("findNonceMarkUTXO: found nonce-mark utxo with txid %s, amount %d satoshi", utxo.TxID, sats) return i, nil } } @@ -1077,15 +1108,16 @@ func (ob *BTCChainClient) SaveBroadcastedTx(txHash string, nonce uint64) { broadcastEntry := clienttypes.ToOutTxHashSQLType(txHash, outTxID) if err := ob.db.Save(&broadcastEntry).Error; err != nil { - ob.logger.ObserveOutTx.Error().Err(err).Msgf("SaveBroadcastedTx: error saving broadcasted txHash %s for outTx %s", txHash, outTxID) + ob.logger.OutTx.Error().Err(err).Msgf("SaveBroadcastedTx: error saving broadcasted txHash %s for outTx %s", txHash, outTxID) } - ob.logger.ObserveOutTx.Info().Msgf("SaveBroadcastedTx: saved broadcasted txHash %s for outTx %s", txHash, outTxID) + ob.logger.OutTx.Info().Msgf("SaveBroadcastedTx: saved broadcasted txHash %s for outTx %s", txHash, outTxID) } -func (ob *BTCChainClient) observeOutTx() { - ticker, err := clienttypes.NewDynamicTicker("Bitcoin_observeOutTx", ob.GetChainParams().OutTxTicker) +// WatchOutTx watches Bitcoin chain for outgoing txs status +func (ob *BTCChainClient) WatchOutTx() { + ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchOutTx", ob.GetChainParams().OutTxTicker) if err != nil { - ob.logger.ObserveOutTx.Error().Err(err).Msg("observeOutTx: error creating ticker") + ob.logger.OutTx.Error().Err(err).Msg("error creating ticker ") return } @@ -1093,9 +1125,12 @@ func (ob *BTCChainClient) observeOutTx() { for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } trackers, err := ob.zetaClient.GetAllOutTxTrackerByChain(ob.chain.ChainId, interfaces.Ascending) if err != nil { - ob.logger.ObserveOutTx.Error().Err(err).Msg("observeOutTx: error GetAllOutTxTrackerByChain") + ob.logger.OutTx.Error().Err(err).Msgf("WatchOutTx: error GetAllOutTxTrackerByChain for chain %d", ob.chain.ChainId) continue } for _, tracker := range trackers { @@ -1103,16 +1138,16 @@ func (ob *BTCChainClient) observeOutTx() { outTxID := ob.GetTxID(tracker.Nonce) cctx, err := ob.zetaClient.GetCctxByNonce(ob.chain.ChainId, tracker.Nonce) if err != nil { - ob.logger.ObserveOutTx.Info().Err(err).Msgf("observeOutTx: can't find cctx for nonce %d", tracker.Nonce) + ob.logger.OutTx.Info().Err(err).Msgf("WatchOutTx: can't find cctx for chain %d nonce %d", ob.chain.ChainId, tracker.Nonce) break } nonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce if tracker.Nonce != nonce { // Tanmay: it doesn't hurt to check - ob.logger.ObserveOutTx.Error().Msgf("observeOutTx: tracker nonce %d not match cctx nonce %d", tracker.Nonce, nonce) + ob.logger.OutTx.Error().Msgf("WatchOutTx: tracker nonce %d not match cctx nonce %d", tracker.Nonce, nonce) break } if len(tracker.HashList) > 1 { - ob.logger.ObserveOutTx.Warn().Msgf("observeOutTx: oops, outTxID %s got multiple (%d) outTx hashes", outTxID, len(tracker.HashList)) + ob.logger.OutTx.Warn().Msgf("WatchOutTx: oops, outTxID %s got multiple (%d) outTx hashes", outTxID, len(tracker.HashList)) } // iterate over all txHashes to find the truly included one. // we do it this (inefficient) way because we don't rely on the first one as it may be a false positive (for unknown reason). @@ -1123,10 +1158,10 @@ func (ob *BTCChainClient) observeOutTx() { if result != nil && !inMempool { // included txCount++ txResult = result - ob.logger.ObserveOutTx.Info().Msgf("observeOutTx: included outTx %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, tracker.Nonce) + ob.logger.OutTx.Info().Msgf("WatchOutTx: included outTx %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, tracker.Nonce) if txCount > 1 { - ob.logger.ObserveOutTx.Error().Msgf( - "observeOutTx: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, ob.chain.ChainId, tracker.Nonce, result) + ob.logger.OutTx.Error().Msgf( + "WatchOutTx: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, ob.chain.ChainId, tracker.Nonce, result) } } } @@ -1134,12 +1169,12 @@ func (ob *BTCChainClient) observeOutTx() { ob.setIncludedTx(tracker.Nonce, txResult) } else if txCount > 1 { ob.removeIncludedTx(tracker.Nonce) // we can't tell which txHash is true, so we remove all (if any) to be safe - ob.logger.ObserveOutTx.Error().Msgf("observeOutTx: included multiple (%d) outTx for chain %d nonce %d", txCount, ob.chain.ChainId, tracker.Nonce) + ob.logger.OutTx.Error().Msgf("WatchOutTx: included multiple (%d) outTx for chain %d nonce %d", txCount, ob.chain.ChainId, tracker.Nonce) } } - ticker.UpdateInterval(ob.GetChainParams().OutTxTicker, ob.logger.ObserveOutTx) + ticker.UpdateInterval(ob.GetChainParams().OutTxTicker, ob.logger.OutTx) case <-ob.stop: - ob.logger.ObserveOutTx.Info().Msg("observeOutTx stopped") + ob.logger.OutTx.Info().Msgf("WatchOutTx stopped for chain %d", ob.chain.ChainId) return } } @@ -1149,19 +1184,19 @@ func (ob *BTCChainClient) observeOutTx() { // Note: if txResult is nil, then inMempool flag should be ignored. func (ob *BTCChainClient) checkIncludedTx(cctx *types.CrossChainTx, txHash string) (*btcjson.GetTransactionResult, bool) { outTxID := ob.GetTxID(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) - hash, getTxResult, err := ob.GetTxResultByHash(txHash) + hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) if err != nil { - ob.logger.ObserveOutTx.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) + ob.logger.OutTx.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false } if txHash != getTxResult.TxID { // just in case, we'll use getTxResult.TxID later - ob.logger.ObserveOutTx.Error().Msgf("checkIncludedTx: inconsistent txHash %s and getTxResult.TxID %s", txHash, getTxResult.TxID) + ob.logger.OutTx.Error().Msgf("checkIncludedTx: inconsistent txHash %s and getTxResult.TxID %s", txHash, getTxResult.TxID) return nil, false } if getTxResult.Confirmations >= 0 { // check included tx only err = ob.checkTssOutTxResult(cctx, hash, getTxResult) if err != nil { - ob.logger.ObserveOutTx.Error().Err(err).Msgf("checkIncludedTx: error verify bitcoin outTx %s outTxID %s", txHash, outTxID) + ob.logger.OutTx.Error().Err(err).Msgf("checkIncludedTx: error verify bitcoin outTx %s outTxID %s", txHash, outTxID) return nil, false } return getTxResult, false // included @@ -1184,16 +1219,16 @@ func (ob *BTCChainClient) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTr if nonce >= ob.pendingNonce { // try increasing pending nonce on every newly included outTx ob.pendingNonce = nonce + 1 } - ob.logger.ObserveOutTx.Info().Msgf("setIncludedTx: included new bitcoin outTx %s outTxID %s pending nonce %d", txHash, outTxID, ob.pendingNonce) + ob.logger.OutTx.Info().Msgf("setIncludedTx: included new bitcoin outTx %s outTxID %s pending nonce %d", txHash, outTxID, ob.pendingNonce) } else if txHash == res.TxID { // found same hash. ob.includedTxResults[outTxID] = getTxResult // update tx result as confirmations may increase if getTxResult.Confirmations > res.Confirmations { - ob.logger.ObserveOutTx.Info().Msgf("setIncludedTx: bitcoin outTx %s got confirmations %d", txHash, getTxResult.Confirmations) + ob.logger.OutTx.Info().Msgf("setIncludedTx: bitcoin outTx %s got confirmations %d", txHash, getTxResult.Confirmations) } } else { // found other hash. // be alert for duplicate payment!!! As we got a new hash paying same cctx (for whatever reason). delete(ob.includedTxResults, outTxID) // we can't tell which txHash is true, so we remove all to be safe - ob.logger.ObserveOutTx.Error().Msgf("setIncludedTx: duplicate payment by bitcoin outTx %s outTxID %s, prior outTx %s", txHash, outTxID, res.TxID) + ob.logger.OutTx.Error().Msgf("setIncludedTx: duplicate payment by bitcoin outTx %s outTxID %s, prior outTx %s", txHash, outTxID, res.TxID) } } @@ -1223,7 +1258,7 @@ func (ob *BTCChainClient) removeIncludedTx(nonce uint64) { func (ob *BTCChainClient) checkTssOutTxResult(cctx *types.CrossChainTx, hash *chainhash.Hash, res *btcjson.GetTransactionResult) error { params := cctx.GetCurrentOutTxParam() nonce := params.OutboundTxTssNonce - rawResult, err := ob.getRawTxResult(hash, res) + rawResult, err := GetRawTxResult(ob.rpcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutTxResult: error GetRawTxResultByHash %s", hash.String()) } @@ -1247,23 +1282,25 @@ func (ob *BTCChainClient) checkTssOutTxResult(cctx *types.CrossChainTx, hash *ch return nil } -func (ob *BTCChainClient) GetTxResultByHash(txID string) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { +// GetTxResultByHash gets the transaction result by hash +func GetTxResultByHash(rpcClient interfaces.BTCRPCClient, txID string) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { hash, err := chainhash.NewHashFromStr(txID) if err != nil { return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error NewHashFromStr: %s", txID) } // The Bitcoin node has to be configured to watch TSS address - txResult, err := ob.rpcClient.GetTransaction(hash) + txResult, err := rpcClient.GetTransaction(hash) if err != nil { return nil, nil, errors.Wrapf(err, "GetOutTxByTxHash: error GetTransaction %s", hash.String()) } return hash, txResult, nil } -func (ob *BTCChainClient) getRawTxResult(hash *chainhash.Hash, res *btcjson.GetTransactionResult) (btcjson.TxRawResult, error) { +// GetRawTxResult gets the raw tx result +func GetRawTxResult(rpcClient interfaces.BTCRPCClient, hash *chainhash.Hash, res *btcjson.GetTransactionResult) (btcjson.TxRawResult, error) { if res.Confirmations == 0 { // for pending tx, we query the raw tx directly - rawResult, err := ob.rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx + rawResult, err := rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx if err != nil { return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetRawTransactionVerbose %s", res.TxID) } @@ -1273,7 +1310,7 @@ func (ob *BTCChainClient) getRawTxResult(hash *chainhash.Hash, res *btcjson.GetT if err != nil { return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error NewHashFromStr for block hash %s", res.BlockHash) } - block, err := ob.rpcClient.GetBlockVerboseTx(blkHash) + block, err := rpcClient.GetBlockVerboseTx(blkHash) if err != nil { return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetBlockVerboseTx %s", res.BlockHash) } @@ -1332,33 +1369,35 @@ func (ob *BTCChainClient) checkTSSVout(params *types.OutboundTxParams, vouts []b nonce := params.OutboundTxTssNonce tssAddress := ob.Tss.BTCAddress() for _, vout := range vouts { - recvAddress, amount, err := DecodeP2WPKHVout(vout, ob.chain) + // decode receiver and amount from vout + receiverExpected := tssAddress + if vout.N == 1 { + // the 2nd output is the payment to recipient + receiverExpected = params.Receiver + } + receiverVout, amount, err := DecodeTSSVout(vout, receiverExpected, ob.chain) if err != nil { - return errors.Wrap(err, "checkTSSVout: error decoding P2WPKH vout") + return err } - // 1st vout: nonce-mark - if vout.N == 0 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVout: nonce-mark address %s not match TSS address %s", recvAddress, tssAddress) + switch vout.N { + case 0: // 1st vout: nonce-mark + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVout: nonce-mark address %s not match TSS address %s", receiverVout, tssAddress) } if amount != chains.NonceMarkAmount(nonce) { return fmt.Errorf("checkTSSVout: nonce-mark amount %d not match nonce-mark amount %d", amount, chains.NonceMarkAmount(nonce)) } - } - // 2nd vout: payment to recipient - if vout.N == 1 { - if recvAddress != params.Receiver { - return fmt.Errorf("checkTSSVout: output address %s not match params receiver %s", recvAddress, params.Receiver) + case 1: // 2nd vout: payment to recipient + if receiverVout != params.Receiver { + return fmt.Errorf("checkTSSVout: output address %s not match params receiver %s", receiverVout, params.Receiver) } // #nosec G701 always positive if uint64(amount) != params.Amount.Uint64() { return fmt.Errorf("checkTSSVout: output amount %d not match params amount %d", amount, params.Amount) } - } - // 3rd vout: change to TSS (optional) - if vout.N == 2 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVout: change address %s not match TSS address %s", recvAddress, tssAddress) + case 2: // 3rd vout: change to TSS (optional) + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVout: change address %s not match TSS address %s", receiverVout, tssAddress) } } } @@ -1377,23 +1416,22 @@ func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, nonce := params.OutboundTxTssNonce tssAddress := ob.Tss.BTCAddress() for _, vout := range vouts { - recvAddress, amount, err := DecodeP2WPKHVout(vout, ob.chain) + // decode receiver and amount from vout + receiverVout, amount, err := DecodeTSSVout(vout, tssAddress, ob.chain) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } - // 1st vout: nonce-mark - if vout.N == 0 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVoutCancelled: nonce-mark address %s not match TSS address %s", recvAddress, tssAddress) + switch vout.N { + case 0: // 1st vout: nonce-mark + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVoutCancelled: nonce-mark address %s not match TSS address %s", receiverVout, tssAddress) } if amount != chains.NonceMarkAmount(nonce) { return fmt.Errorf("checkTSSVoutCancelled: nonce-mark amount %d not match nonce-mark amount %d", amount, chains.NonceMarkAmount(nonce)) } - } - // 2nd vout: change to TSS (optional) - if vout.N == 2 { - if recvAddress != tssAddress { - return fmt.Errorf("checkTSSVoutCancelled: change address %s not match TSS address %s", recvAddress, tssAddress) + case 1: // 2nd vout: change to TSS (optional) + if receiverVout != tssAddress { + return fmt.Errorf("checkTSSVoutCancelled: change address %s not match TSS address %s", receiverVout, tssAddress) } } } @@ -1403,7 +1441,7 @@ func (ob *BTCChainClient) checkTSSVoutCancelled(params *types.OutboundTxParams, func (ob *BTCChainClient) BuildBroadcastedTxMap() error { var broadcastedTransactions []clienttypes.OutTxHashSQLType if err := ob.db.Find(&broadcastedTransactions).Error; err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("error iterating over db") + ob.logger.Chain.Error().Err(err).Msg("error iterating over db") return err } for _, entry := range broadcastedTransactions { @@ -1421,7 +1459,7 @@ func (ob *BTCChainClient) LoadLastBlock() error { //Load persisted block number var lastBlockNum clienttypes.LastBlockSQLType if err := ob.db.First(&lastBlockNum, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.ChainLogger.Info().Msg("LastBlockNum not found in DB, scan from latest") + ob.logger.Chain.Info().Msg("LastBlockNum not found in DB, scan from latest") ob.SetLastBlockHeightScanned(bn) } else { // #nosec G701 always in range @@ -1430,7 +1468,7 @@ func (ob *BTCChainClient) LoadLastBlock() error { //If persisted block number is too low, use the latest height if (bn - lastBN) > maxHeightDiff { - ob.logger.ChainLogger.Info().Msgf("LastBlockNum too low: %d, scan from latest", lastBlockNum.Num) + ob.logger.Chain.Info().Msgf("LastBlockNum too low: %d, scan from latest", lastBlockNum.Num) ob.SetLastBlockHeightScanned(bn) } } @@ -1438,7 +1476,7 @@ func (ob *BTCChainClient) LoadLastBlock() error { if ob.chain.ChainId == 18444 { // bitcoin regtest: start from block 100 ob.SetLastBlockHeightScanned(100) } - ob.logger.ChainLogger.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) + ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) return nil } diff --git a/zetaclient/bitcoin/bitcoin_client_rpc_test.go b/zetaclient/bitcoin/bitcoin_client_live_test.go similarity index 76% rename from zetaclient/bitcoin/bitcoin_client_rpc_test.go rename to zetaclient/bitcoin/bitcoin_client_live_test.go index d4f65ed84a..522b604d19 100644 --- a/zetaclient/bitcoin/bitcoin_client_rpc_test.go +++ b/zetaclient/bitcoin/bitcoin_client_live_test.go @@ -9,13 +9,6 @@ import ( "testing" "time" - "github.com/zeta-chain/zetacore/pkg/chains" - appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - "github.com/zeta-chain/zetacore/zetaclient/config" - corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" - "github.com/zeta-chain/zetacore/zetaclient/interfaces" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -25,12 +18,18 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/zeta-chain/zetacore/pkg/chains" + appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" + clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" + corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" "github.com/zeta-chain/zetacore/zetaclient/testutils" ) type BitcoinClientTestSuite struct { suite.Suite - BitcoinChainClient *BTCChainClient + rpcClient *rpcclient.Client } func (suite *BitcoinClientTestSuite) SetupTest() { @@ -50,7 +49,8 @@ func (suite *BitcoinClientTestSuite) SetupTest() { client, err := NewBitcoinClient(appContext, chains.BtcRegtestChain(), nil, tss, tempSQLiteDbPath, clientcommon.DefaultLoggers(), config.BTCConfig{}, nil) suite.Require().NoError(err) - suite.BitcoinChainClient = client + suite.rpcClient, err = getRPCClient(18332) + suite.Require().NoError(err) skBytes, err := hex.DecodeString(skHex) suite.Require().NoError(err) suite.T().Logf("skBytes: %d", len(skBytes)) @@ -127,10 +127,10 @@ func getFeeRate(client *rpcclient.Client, confTarget int64, estimateMode *btcjso // All methods that begin with "Test" are run as tests within a // suite. func (suite *BitcoinClientTestSuite) Test1() { - feeResult, err := suite.BitcoinChainClient.rpcClient.EstimateSmartFee(1, nil) + feeResult, err := suite.rpcClient.EstimateSmartFee(1, nil) suite.Require().NoError(err) suite.T().Logf("fee result: %f", *feeResult.FeeRate) - bn, err := suite.BitcoinChainClient.rpcClient.GetBlockCount() + bn, err := suite.rpcClient.GetBlockCount() suite.Require().NoError(err) suite.T().Logf("block %d", bn) @@ -139,21 +139,21 @@ func (suite *BitcoinClientTestSuite) Test1() { err = chainhash.Decode(&hash, hashStr) suite.Require().NoError(err) - //:= suite.BitcoinChainClient.rpcClient.GetBlock(&hash) - block, err := suite.BitcoinChainClient.rpcClient.GetBlockVerboseTx(&hash) + block, err := suite.rpcClient.GetBlockVerboseTx(&hash) suite.Require().NoError(err) suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block txs len %d", len(block.Tx)) - inTxs := FilterAndParseIncomingTx( + inTxs, err := FilterAndParseIncomingTx( + suite.rpcClient, block.Tx, uint64(block.Height), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - &log.Logger, + log.Logger, &chaincfg.TestNet3Params, 0.0, ) - + suite.Require().NoError(err) suite.Require().Equal(1, len(inTxs)) suite.Require().Equal(inTxs[0].Value, 0.0001) suite.Require().Equal(inTxs[0].ToAddress, "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2") @@ -175,27 +175,27 @@ func (suite *BitcoinClientTestSuite) Test2() { err := chainhash.Decode(&hash, hashStr) suite.Require().NoError(err) - //:= suite.BitcoinChainClient.rpcClient.GetBlock(&hash) - block, err := suite.BitcoinChainClient.rpcClient.GetBlockVerboseTx(&hash) + block, err := suite.rpcClient.GetBlockVerboseTx(&hash) suite.Require().NoError(err) suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block height %d", block.Height) suite.T().Logf("block txs len %d", len(block.Tx)) - inTxs := FilterAndParseIncomingTx( + inTxs, err := FilterAndParseIncomingTx( + suite.rpcClient, block.Tx, uint64(block.Height), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - &log.Logger, + log.Logger, &chaincfg.TestNet3Params, 0.0, ) - + suite.Require().NoError(err) suite.Require().Equal(0, len(inTxs)) } func (suite *BitcoinClientTestSuite) Test3() { - client := suite.BitcoinChainClient.rpcClient + client := suite.rpcClient res, err := client.EstimateSmartFee(1, &btcjson.EstimateModeConservative) suite.Require().NoError(err) suite.T().Logf("fee: %f", *res.FeeRate) @@ -210,11 +210,18 @@ func (suite *BitcoinClientTestSuite) Test3() { suite.T().Logf("block number %d", bn) } -// func TestBitcoinChainClient(t *testing.T) { -// suite.Run(t, new(BitcoinClientTestSuite)) -// } +// TestBitcoinClientLive is a phony test to run each live test individually +func TestBitcoinClientLive(t *testing.T) { + // suite.Run(t, new(BitcoinClientTestSuite)) -// Remove prefix "Live" to run this live test + // LiveTestBitcoinFeeRate(t) + // LiveTestAvgFeeRateMainnetMempoolSpace(t) + // LiveTestAvgFeeRateTestnetMempoolSpace(t) + // LiveTestGetSenderByVin(t) +} + +// LiveTestBitcoinFeeRate query Bitcoin mainnet fee rate every 5 minutes +// and compares Conservative and Economical fee rates for different block targets (1 and 2) func LiveTestBitcoinFeeRate(t *testing.T) { // setup Bitcoin client client, err := getRPCClient(8332) @@ -319,7 +326,7 @@ func compareAvgFeeRate(t *testing.T, client *rpcclient.Client, startBlock int, e } } -// Remove prefix "Live" to run this live test +// LiveTestAvgFeeRateMainnetMempoolSpace compares calculated fee rate with mempool.space fee rate for mainnet func LiveTestAvgFeeRateMainnetMempoolSpace(t *testing.T) { // setup Bitcoin client client, err := getRPCClient(8332) @@ -333,7 +340,7 @@ func LiveTestAvgFeeRateMainnetMempoolSpace(t *testing.T) { compareAvgFeeRate(t, client, startBlock, endBlock, false) } -// Remove prefix "Live" to run this live test +// LiveTestAvgFeeRateTestnetMempoolSpace compares calculated fee rate with mempool.space fee rate for testnet func LiveTestAvgFeeRateTestnetMempoolSpace(t *testing.T) { // setup Bitcoin client client, err := getRPCClient(18332) @@ -346,3 +353,76 @@ func LiveTestAvgFeeRateTestnetMempoolSpace(t *testing.T) { compareAvgFeeRate(t, client, startBlock, endBlock, true) } + +// LiveTestGetSenderByVin gets sender address for each vin and compares with mempool.space sender address +func LiveTestGetSenderByVin(t *testing.T) { + // setup Bitcoin client + chainID := int64(8332) + client, err := getRPCClient(chainID) + require.NoError(t, err) + + // net params + net, err := chains.GetBTCChainParams(chainID) + require.NoError(t, err) + testnet := false + if chainID == chains.BtcTestNetChain().ChainId { + testnet = true + } + + // calculates block range to test + startBlock, err := client.GetBlockCount() + require.NoError(t, err) + endBlock := startBlock - 5000 + + // loop through mempool.space blocks in descending order +BLOCKLOOP: + for bn := startBlock; bn >= endBlock; { + // get block hash + blkHash, err := client.GetBlockHash(int64(bn)) + if err != nil { + fmt.Printf("error GetBlockHash for block %d: %s\n", bn, err) + time.Sleep(3 * time.Second) + continue + } + + // get mempool.space txs for the block + mempoolTxs, err := testutils.GetBlockTxs(context.Background(), blkHash.String(), testnet) + if err != nil { + fmt.Printf("error GetBlockTxs %d: %s\n", bn, err) + time.Sleep(10 * time.Second) + continue + } + + // loop through each tx in the block + for i, mptx := range mempoolTxs { + // sample 10 txs per block + if i >= 10 { + break + } + for _, mpvin := range mptx.Vin { + // skip coinbase tx + if mpvin.IsCoinbase { + continue + } + // get sender address for each vin + vin := btcjson.Vin{ + Txid: mpvin.TxID, + Vout: mpvin.Vout, + } + senderAddr, err := GetSenderAddressByVin(client, vin, net) + if err != nil { + fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) + time.Sleep(3 * time.Second) + continue BLOCKLOOP // retry the block + } + if senderAddr != mpvin.Prevout.ScriptpubkeyAddress { + panic(fmt.Sprintf("block %d, tx %s, vout %d: want %s, got %s\n", bn, vin.Txid, vin.Vout, mpvin.Prevout.ScriptpubkeyAddress, senderAddr)) + } else { + fmt.Printf("block: %d sender address type: %s\n", bn, mpvin.Prevout.ScriptpubkeyType) + } + } + } + bn-- + time.Sleep(500 * time.Millisecond) + } +} diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index fa7a84b02f..58377cdc65 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -6,12 +6,14 @@ import ( "math" "math/big" "path" + "strings" "sync" "testing" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" @@ -39,6 +41,22 @@ func MockBTCClientMainnet() *BTCChainClient { } } +// createRPCClientAndLoadTx is a helper function to load raw tx and feed it to mock rpc client +func createRPCClientAndLoadTx(t *testing.T, chainId int64, txHash string) *stub.MockBTCRPCClient { + // file name for the archived MsgTx + nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chainId, txHash)) + + // load archived MsgTx + var msgTx wire.MsgTx + testutils.LoadObjectFromJSONFile(t, &msgTx, nameMsgTx) + tx := btcutil.NewTx(&msgTx) + + // feed tx to mock rpc client + rpcClient := stub.NewMockBTCRPCClient() + rpcClient.WithRawTransaction(tx) + return rpcClient +} + func TestNewBitcoinClient(t *testing.T) { t.Run("should return error because zetacore doesn't update core context", func(t *testing.T) { cfg := config.NewConfig() @@ -46,7 +64,7 @@ func TestNewBitcoinClient(t *testing.T) { appContext := appcontext.NewAppContext(coreContext, cfg) chain := chains.BtcMainnetChain() bridge := stub.NewMockZetaCoreBridge() - tss := stub.NewMockTSS(sample.EthAddress().String(), "") + tss := stub.NewMockTSS(chains.BtcTestNetChain(), sample.EthAddress().String(), "") loggers := clientcommon.ClientLogger{} btcCfg := cfg.BitcoinConfig ts := metrics.NewTelemetryServer() @@ -78,13 +96,11 @@ func TestConfirmationThreshold(t *testing.T) { func TestAvgFeeRateBlock828440(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - err := testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(t, &blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) // https://mempool.space/block/000000000000000000025ca01d2c1094b8fd3bacc5468cc3193ced6a14618c27 var blockMb testutils.MempoolBlock - err = testutils.LoadObjectFromJSONFile(&blockMb, path.Join("../", testutils.TestDataPathBTC, "block_mempool.space_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(t, &blockMb, path.Join("../", testutils.TestDataPathBTC, "block_mempool.space_8332_828440.json")) gasRate, err := CalcBlockAvgFeeRate(&blockVb, &chaincfg.MainNetParams) require.NoError(t, err) @@ -94,8 +110,7 @@ func TestAvgFeeRateBlock828440(t *testing.T) { func TestAvgFeeRateBlock828440Errors(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - err := testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(t, &blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) t.Run("block has no transactions", func(t *testing.T) { emptyVb := btcjson.GetBlockVerboseTxResult{Tx: []btcjson.TxRawResult{}} @@ -181,8 +196,7 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { func TestCalcDepositorFee828440(t *testing.T) { // load archived block 828440 var blockVb btcjson.GetBlockVerboseTxResult - err := testutils.LoadObjectFromJSONFile(&blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(t, &blockVb, path.Join("../", testutils.TestDataPathBTC, "block_trimmed_8332_828440.json")) avgGasRate := float64(32.0) // #nosec G701 test - always in range gasRate := int64(avgGasRate * clientcommon.BTCOuttxGasPriceMultiplier) @@ -212,7 +226,8 @@ func TestCalcDepositorFee828440(t *testing.T) { func TestCheckTSSVout(t *testing.T) { // the archived outtx raw result file and cctx file // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chainID := int64(8332) + chain := chains.BtcMainnetChain() + chainID := chain.ChainId nonce := uint64(148) // create mainnet mock client @@ -234,6 +249,15 @@ func TestCheckTSSVout(t *testing.T) { err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) + t.Run("should fail on invalid TSS vout", func(t *testing.T) { + rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) + params := cctx.GetCurrentOutTxParam() + + // invalid TSS vout + rawResult.Vout[0].ScriptPubKey.Hex = "invalid script" + err := btcClient.checkTSSVout(params, rawResult.Vout) + require.Error(t, err) + }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) params := cctx.GetCurrentOutTxParam() @@ -284,7 +308,8 @@ func TestCheckTSSVout(t *testing.T) { func TestCheckTSSVoutCancelled(t *testing.T) { // the archived outtx raw result file and cctx file // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chainID := int64(8332) + chain := chains.BtcMainnetChain() + chainID := chain.ChainId nonce := uint64(148) // create mainnet mock client @@ -338,6 +363,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // remove change vout to simulate cancelled tx rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, chainID, nonce) rawResult.Vout[1] = rawResult.Vout[2] + rawResult.Vout[1].N = 1 // swap vout index rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutTxParam() @@ -348,6 +374,345 @@ func TestCheckTSSVoutCancelled(t *testing.T) { }) } +func TestGetSenderAddressByVin(t *testing.T) { + chain := chains.BtcMainnetChain() + net := &chaincfg.MainNetParams + + t.Run("should get sender address from P2TR tx", func(t *testing.T) { + // vin from the archived P2TR tx + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", sender) + }) + t.Run("should get sender address from P2WSH tx", func(t *testing.T) { + // vin from the archived P2WSH tx + // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 + txHash := "d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016" + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 0} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", sender) + }) + t.Run("should get sender address from P2WPKH tx", func(t *testing.T) { + // vin from the archived P2WPKH tx + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + txHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) + }) + t.Run("should get sender address from P2SH tx", func(t *testing.T) { + // vin from the archived P2SH tx + // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a + txHash := "211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a" + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 0} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", sender) + }) + t.Run("should get sender address from P2PKH tx", func(t *testing.T) { + // vin from the archived P2PKH tx + // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 + txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 1} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Equal(t, "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", sender) + }) + t.Run("should get empty sender address on unknown script", func(t *testing.T) { + // vin from the archived P2PKH tx + // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 + txHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" + nameMsgTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCMsgTx(chain.ChainId, txHash)) + var msgTx wire.MsgTx + testutils.LoadObjectFromJSONFile(t, &msgTx, nameMsgTx) + + // modify script to unknown script + msgTx.TxOut[1].PkScript = []byte{0x00, 0x01, 0x02, 0x03} // can be any invalid script bytes + tx := btcutil.NewTx(&msgTx) + + // feed tx to mock rpc client + rpcClient := stub.NewMockBTCRPCClient() + rpcClient.WithRawTransaction(tx) + + // get sender address + txVin := btcjson.Vin{Txid: txHash, Vout: 1} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.NoError(t, err) + require.Empty(t, sender) + }) +} + +func TestGetSenderAddressByVinErrors(t *testing.T) { + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" + chain := chains.BtcMainnetChain() + net := &chaincfg.MainNetParams + + t.Run("should get sender address from P2TR tx", func(t *testing.T) { + rpcClient := stub.NewMockBTCRPCClient() + // use invalid tx hash + txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.Error(t, err) + require.Empty(t, sender) + }) + t.Run("should return error when RPC client fails to get raw tx", func(t *testing.T) { + // create mock rpc client without preloaded tx + rpcClient := stub.NewMockBTCRPCClient() + txVin := btcjson.Vin{Txid: txHash, Vout: 2} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.ErrorContains(t, err, "error getting raw transaction") + require.Empty(t, sender) + }) + t.Run("should return error on invalid output index", func(t *testing.T) { + // create mock rpc client with preloaded tx + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) + // invalid output index + txVin := btcjson.Vin{Txid: txHash, Vout: 3} + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + require.ErrorContains(t, err, "out of range") + require.Empty(t, sender) + }) +} + +func TestGetBtcEvent(t *testing.T) { + // load archived intx P2WPKH raw result + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + chain := chains.BtcMainnetChain() + + // GetBtcEvent arguments + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tssAddress := testutils.TSSAddressBTCMainnet + blockNumber := uint64(835640) + net := &chaincfg.MainNetParams + // 2.992e-05, see avgFeeRate https://mempool.space/api/v1/blocks/835640 + depositorFee := DepositorFee(22 * clientcommon.BTCOuttxGasPriceMultiplier) + + // expected result + memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) + require.NoError(t, err) + eventExpected := &BTCInTxEvnet{ + FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, + } + + t.Run("should get BTC intx event from P2WPKH sender", func(t *testing.T) { + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 2 + eventExpected.FromAddress = "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2TR sender", func(t *testing.T) { + // replace vin with a P2TR vin, so the sender address will change + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 + preHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 2 + eventExpected.FromAddress = "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2WSH sender", func(t *testing.T) { + // replace vin with a P2WSH vin, so the sender address will change + // https://mempool.space/tx/d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016 + preHash := "d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 0 + eventExpected.FromAddress = "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2SH sender", func(t *testing.T) { + // replace vin with a P2SH vin, so the sender address will change + // https://mempool.space/tx/211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a + preHash := "211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 0 + eventExpected.FromAddress = "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should get BTC intx event from P2PKH sender", func(t *testing.T) { + // replace vin with a P2PKH vin, so the sender address will change + // https://mempool.space/tx/781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7 + preHash := "781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 1 + eventExpected.FromAddress = "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV" + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + t.Run("should skip tx if len(tx.Vout) < 2", func(t *testing.T) { + // load tx and modify the tx to have only 1 vout + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tx.Vout = tx.Vout[:1] + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if Vout[0] is not a P2WPKH output", func(t *testing.T) { + // load tx + rpcClient := stub.NewMockBTCRPCClient() + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + + // modify the tx to have Vout[0] a P2SH output + tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + + // append 1 byte to script to make it longer than 22 bytes + tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" + event, err = GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if receiver address is not TSS address", func(t *testing.T) { + // load tx and modify receiver address to any non-tss address: bc1qw8wrek2m7nlqldll66ajnwr9mh64syvkt67zlu + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if amount is less than depositor fee", func(t *testing.T) { + // load tx and modify amount to less than depositor fee + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if 2nd vout is not OP_RETURN", func(t *testing.T) { + // load tx and modify memo OP_RETURN to OP_1 + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a", "51", 1) + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) + t.Run("should skip tx if memo decoding fails", func(t *testing.T) { + // load tx and modify memo length to be 1 byte less than actual + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a14", "6a13", 1) + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.NoError(t, err) + require.Nil(t, event) + }) +} + +func TestGetBtcEventErrors(t *testing.T) { + // load archived intx P2WPKH raw result + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + chain := chains.BtcMainnetChain() + net := &chaincfg.MainNetParams + tssAddress := testutils.TSSAddressBTCMainnet + blockNumber := uint64(835640) + depositorFee := DepositorFee(22 * clientcommon.BTCOuttxGasPriceMultiplier) + + t.Run("should return error on invalid Vout[0] script", func(t *testing.T) { + // load tx and modify Vout[0] script to invalid script + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tx.Vout[0].ScriptPubKey.Hex = "0014invalid000000000000000000000000000000000" + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.Error(t, err) + require.Nil(t, event) + }) + t.Run("should return error if len(tx.Vin) < 1", func(t *testing.T) { + // load tx and remove vin + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + tx.Vin = nil + + // get BTC event + rpcClient := stub.NewMockBTCRPCClient() + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.Error(t, err) + require.Nil(t, event) + }) + t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { + // load tx and leave rpc client without preloaded tx + tx := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + rpcClient := stub.NewMockBTCRPCClient() + + // get BTC event + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + require.Error(t, err) + require.Nil(t, event) + }) +} + func TestBTCChainClient_ObserveInTx(t *testing.T) { t.Run("should return error", func(t *testing.T) { // create mainnet mock client diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index df5502b4ae..a5ff22fa6a 100644 --- a/zetaclient/bitcoin/bitcoin_signer.go +++ b/zetaclient/bitcoin/bitcoin_signer.go @@ -8,35 +8,34 @@ import ( "math/rand" "time" - corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" - - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/coin" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - "github.com/zeta-chain/zetacore/zetaclient/compliance" - "github.com/zeta-chain/zetacore/zetaclient/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/metrics" - "github.com/zeta-chain/zetacore/zetaclient/outtxprocessor" - "github.com/zeta-chain/zetacore/zetaclient/tss" - "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" + corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/outtxprocessor" + "github.com/zeta-chain/zetacore/zetaclient/tss" ) const ( + // the maximum number of inputs per outtx maxNoOfInputsPerTx = 20 - consolidationRank = 10 // the rank below (or equal to) which we consolidate UTXOs - outTxBytesMin = uint64(239) // 239vB == EstimateSegWitTxSize(2, 3) - outTxBytesMax = uint64(1531) // 1531v == EstimateSegWitTxSize(21, 3) + + // the rank below (or equal to) which we consolidate UTXOs + consolidationRank = 10 ) // BTCSigner deals with signing BTC transactions and implements the ChainSigner interface @@ -98,9 +97,73 @@ func (signer *BTCSigner) GetERC20CustodyAddress() ethcommon.Address { return ethcommon.Address{} } +// AddWithdrawTxOutputs adds the 3 outputs to the withdraw tx +// 1st output: the nonce-mark btc to TSS itself +// 2nd output: the payment to the recipient +// 3rd output: the remaining btc to TSS itself +func (signer *BTCSigner) AddWithdrawTxOutputs( + tx *wire.MsgTx, + to btcutil.Address, + total float64, + amount float64, + nonceMark int64, + fees *big.Int, + cancelTx bool, +) error { + // convert withdraw amount to satoshis + amountSatoshis, err := GetSatoshis(amount) + if err != nil { + return err + } + + // calculate remaining btc (the change) to TSS self + remaining := total - amount + remainingSats, err := GetSatoshis(remaining) + if err != nil { + return err + } + remainingSats -= fees.Int64() + remainingSats -= nonceMark + if remainingSats < 0 { + return fmt.Errorf("remainder value is negative: %d", remainingSats) + } else if remainingSats == nonceMark { + signer.logger.Info().Msgf("adjust remainder value to avoid duplicate nonce-mark: %d", remainingSats) + remainingSats-- + } + + // 1st output: the nonce-mark btc to TSS self + tssAddrP2WPKH := signer.tssSigner.BTCAddressWitnessPubkeyHash() + payToSelfScript, err := PayToAddrScript(tssAddrP2WPKH) + if err != nil { + return err + } + txOut1 := wire.NewTxOut(nonceMark, payToSelfScript) + tx.AddTxOut(txOut1) + + // 2nd output: the payment to the recipient + if !cancelTx { + pkScript, err := PayToAddrScript(to) + if err != nil { + return err + } + txOut2 := wire.NewTxOut(amountSatoshis, pkScript) + tx.AddTxOut(txOut2) + } else { + // send the amount to TSS self if tx is cancelled + remainingSats += amountSatoshis + } + + // 3rd output: the remaining btc to TSS self + if remainingSats > 0 { + txOut3 := wire.NewTxOut(remainingSats, payToSelfScript) + tx.AddTxOut(txOut3) + } + return nil +} + // SignWithdrawTx receives utxos sorted by value, amount in BTC, feeRate in BTC per Kb func (signer *BTCSigner) SignWithdrawTx( - to *btcutil.AddressWitnessPubKeyHash, + to btcutil.Address, amount float64, gasPrice *big.Int, sizeLimit uint64, @@ -137,14 +200,12 @@ func (signer *BTCSigner) SignWithdrawTx( tx.AddTxIn(txIn) } - amountSatoshis, err := GetSatoshis(amount) + // size checking + // #nosec G701 always positive + txSize, err := EstimateOuttxSize(uint64(len(prevOuts)), []btcutil.Address{to}) if err != nil { return nil, err } - - // size checking - // #nosec G701 always positive - txSize := EstimateSegWitTxSize(uint64(len(prevOuts)), 3) if sizeLimit < BtcOutTxBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user signer.logger.Info().Msgf("sizeLimit %d is less than BtcOutTxBytesWithdrawer %d for nonce %d", sizeLimit, txSize, nonce) } @@ -163,45 +224,11 @@ func (signer *BTCSigner) SignWithdrawTx( signer.logger.Info().Msgf("bitcoin outTx nonce %d gasPrice %s size %d fees %s consolidated %d utxos of value %v", nonce, gasPrice.String(), txSize, fees.String(), consolidatedUtxo, consolidatedValue) - // calculate remaining btc to TSS self - tssAddrWPKH := signer.tssSigner.BTCAddressWitnessPubkeyHash() - payToSelf, err := PayToWitnessPubKeyHashScript(tssAddrWPKH.WitnessProgram()) + // add tx outputs + err = signer.AddWithdrawTxOutputs(tx, to, total, amount, nonceMark, fees, cancelTx) if err != nil { return nil, err } - remaining := total - amount - remainingSats, err := GetSatoshis(remaining) - if err != nil { - return nil, err - } - remainingSats -= fees.Int64() - remainingSats -= nonceMark - if remainingSats < 0 { - return nil, fmt.Errorf("remainder value is negative: %d", remainingSats) - } else if remainingSats == nonceMark { - signer.logger.Info().Msgf("SignWithdrawTx: adjust remainder value to avoid duplicate nonce-mark: %d", remainingSats) - remainingSats-- - } - - // 1st output: the nonce-mark btc to TSS self - txOut1 := wire.NewTxOut(nonceMark, payToSelf) - tx.AddTxOut(txOut1) - - // 2nd output: the payment to the recipient - if !cancelTx { - pkScript, err := PayToWitnessPubKeyHashScript(to.WitnessProgram()) - if err != nil { - return nil, err - } - txOut2 := wire.NewTxOut(amountSatoshis, pkScript) - tx.AddTxOut(txOut2) - } - - // 3rd output: the remaining btc to TSS self - if remainingSats > 0 { - txOut3 := wire.NewTxOut(remainingSats, payToSelf) - tx.AddTxOut(txOut3) - } // sign the tx sigHashes := txscript.NewTxSigHashes(tx) @@ -314,27 +341,13 @@ func (signer *BTCSigner) TryProcessOutTx( } // Check receiver P2WPKH address - bitcoinNetParams, err := chains.BitcoinNetParamsFromChainID(params.ReceiverChainId) - if err != nil { - logger.Error().Err(err).Msgf("cannot get bitcoin net params%v", err) - return - } - addr, err := chains.DecodeBtcAddress(params.Receiver, params.ReceiverChainId) + to, err := chains.DecodeBtcAddress(params.Receiver, params.ReceiverChainId) if err != nil { logger.Error().Err(err).Msgf("cannot decode address %s ", params.Receiver) return } - if !addr.IsForNet(bitcoinNetParams) { - logger.Error().Msgf( - "address %s is not for network %s", - params.Receiver, - bitcoinNetParams.Name, - ) - return - } - to, ok := addr.(*btcutil.AddressWitnessPubKeyHash) - if err != nil || !ok { - logger.Error().Err(err).Msgf("cannot convert address %s to P2WPKH address", params.Receiver) + if !chains.IsBtcAddressSupported(to) { + logger.Error().Msgf("unsupported address %s", params.Receiver) return } amount := float64(params.Amount.Uint64()) / 1e8 @@ -356,7 +369,7 @@ func (signer *BTCSigner) TryProcessOutTx( amount = 0.0 // zero out the amount to cancel the tx } - logger.Info().Msgf("SignWithdrawTx: to %s, value %d sats", addr.EncodeAddress(), params.Amount.Uint64()) + logger.Info().Msgf("SignWithdrawTx: to %s, value %d sats", to.EncodeAddress(), params.Amount.Uint64()) logger.Info().Msgf("using utxos: %v", btcClient.utxos) tx, err := signer.SignWithdrawTx( diff --git a/zetaclient/bitcoin/bitcoin_signer_test.go b/zetaclient/bitcoin/bitcoin_signer_test.go index 26e79ed8d6..96dc48cdff 100644 --- a/zetaclient/bitcoin/bitcoin_signer_test.go +++ b/zetaclient/bitcoin/bitcoin_signer_test.go @@ -4,12 +4,12 @@ import ( "encoding/hex" "fmt" "math" - "math/rand" + "math/big" + "reflect" "sort" "sync" "testing" - "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" @@ -25,6 +25,7 @@ import ( corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" "github.com/zeta-chain/zetacore/zetaclient/interfaces" "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" . "gopkg.in/check.v1" ) @@ -34,31 +35,6 @@ type BTCSignerSuite struct { var _ = Suite(&BTCSignerSuite{}) -// 21 example UTXO txids to use in the test. -var exampleTxids = []string{ - "c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e", - "54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa", - "b18a55a34319cfbedebfcfe1a80fef2b92ad8894d06caf8293a0344824c2cfbc", - "969fb309a4df7c299972700da788b5d601c0c04bab4ab46fff79d0335a7d75de", - "6c71913061246ffc20e268c1b0e65895055c36bfbf1f8faf92dcad6f8242121e", - "ba6d6e88cb5a97556684a1232719a3ffe409c5c9501061e1f59741bc412b3585", - "69b56c3c8c5d1851f9eaec256cd49f290b477a5d43e2aef42ef25d3c1d9f4b33", - "b87effd4cb46fe1a575b5b1ba0289313dc9b4bc9e615a3c6cbc0a14186921fdf", - "3135433054523f5e220621c9e3d48efbbb34a6a2df65635c2a3e7d462d3e1cda", - "8495c22a9ce6359ab53aa048c13b41c64fdf5fe141f516ba2573cc3f9313f06e", - "f31583544b475370d7b9187c9a01b92e44fb31ac5fcfa7fc55565ac64043aa9a", - "c03d55f9f717c1df978623e2e6b397b720999242f9ead7db9b5988fee3fb3933", - "ee55688439b47a5410cdc05bac46be0094f3af54d307456fdfe6ba8caf336e0b", - "61895f86c70f0bc3eef55d9a00347b509fa90f7a344606a9774be98a3ee9e02a", - "ffabb401a19d04327bd4a076671d48467dbcde95459beeab23df21686fd01525", - "b7e1c03b9b73e4e90fc06da893072c5604203c49e66699acbb2f61485d822981", - "185614d21973990138e478ce10e0a4014352df58044276d4e4c0093aa140f482", - "4a2800f13d15dc0c82308761d6fe8f6d13b65e42d7ca96a42a3a7048830e8c55", - "fb98f52e91db500735b185797cebb5848afbfe1289922d87e03b98c3da5b85ef", - "7901c5e36d9e8456ac61b29b82048650672a889596cbd30a9f8910a589ffc5b3", - "6bcd0850fd2fa1404290ed04d78d4ae718414f16d4fbfd344951add8dcf60326", -} - func (s *BTCSignerSuite) SetUpTest(c *C) { // test private key with EVM address //// EVM: 0x236C7f53a90493Bb423411fe4117Cb4c2De71DfB @@ -104,7 +80,7 @@ func (s *BTCSignerSuite) TestP2PH(c *C) { prevOut := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0}, nil) originTx.AddTxIn(txIn) - pkScript, err := txscript.PayToAddrScript(addr) + pkScript, err := PayToAddrScript(addr) c.Assert(err, IsNil) @@ -176,7 +152,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { prevOut := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0}, nil) originTx.AddTxIn(txIn) - pkScript, err := txscript.PayToAddrScript(addr) + pkScript, err := PayToAddrScript(addr) c.Assert(err, IsNil) txOut := wire.NewTxOut(100000000, pkScript) originTx.AddTxOut(txOut) @@ -197,7 +173,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { txOut = wire.NewTxOut(0, nil) redeemTx.AddTxOut(txOut) txSigHashes := txscript.NewTxSigHashes(redeemTx) - pkScript, err = PayToWitnessPubKeyHashScript(addr.WitnessProgram()) + pkScript, err = PayToAddrScript(addr) c.Assert(err, IsNil) { @@ -239,193 +215,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { fmt.Println("Transaction successfully signed") } -func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, []byte) { - privateKey, err := btcec.NewPrivateKey(btcec.S256()) - require.Nil(t, err) - pubKeyHash := btcutil.Hash160(privateKey.PubKey().SerializeCompressed()) - addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) - require.Nil(t, err) - //fmt.Printf("New address: %s\n", addr.EncodeAddress()) - pkScript, err := PayToWitnessPubKeyHashScript(addr.WitnessProgram()) - require.Nil(t, err) - return privateKey, pkScript -} - -func addTxInputs(t *testing.T, tx *wire.MsgTx, txids []string) { - preTxSize := tx.SerializeSize() - for _, txid := range txids { - hash, err := chainhash.NewHashFromStr(txid) - require.Nil(t, err) - outpoint := wire.NewOutPoint(hash, uint32(rand.Intn(100))) - txIn := wire.NewTxIn(outpoint, nil, nil) - tx.AddTxIn(txIn) - require.Equal(t, bytesPerInput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, input %d size: %d\n", tx.SerializeSize(), i, tx.SerializeSize()-preTxSize) - preTxSize = tx.SerializeSize() - } -} - -func addTxOutputs(t *testing.T, tx *wire.MsgTx, payerScript, payeeScript []byte) { - preTxSize := tx.SerializeSize() - - // 1st output to payer - value1 := int64(1 + rand.Intn(100000000)) - txOut1 := wire.NewTxOut(value1, payerScript) - tx.AddTxOut(txOut1) - require.Equal(t, bytesPerOutput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, output 1: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) - preTxSize = tx.SerializeSize() - - // 2nd output to payee - value2 := int64(1 + rand.Intn(100000000)) - txOut2 := wire.NewTxOut(value2, payeeScript) - tx.AddTxOut(txOut2) - require.Equal(t, bytesPerOutput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, output 2: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) - preTxSize = tx.SerializeSize() - - // 3rd output to payee - value3 := int64(1 + rand.Intn(100000000)) - txOut3 := wire.NewTxOut(value3, payeeScript) - tx.AddTxOut(txOut3) - require.Equal(t, bytesPerOutput, tx.SerializeSize()-preTxSize) - //fmt.Printf("tx size: %d, output 3: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) -} - -func signTx(t *testing.T, tx *wire.MsgTx, payerScript []byte, privateKey *btcec.PrivateKey) { - preTxSize := tx.SerializeSize() - sigHashes := txscript.NewTxSigHashes(tx) - for ix := range tx.TxIn { - amount := int64(1 + rand.Intn(100000000)) - witnessHash, err := txscript.CalcWitnessSigHash(payerScript, sigHashes, txscript.SigHashAll, tx, ix, amount) - require.Nil(t, err) - sig, err := privateKey.Sign(witnessHash) - require.Nil(t, err) - - pkCompressed := privateKey.PubKey().SerializeCompressed() - txWitness := wire.TxWitness{append(sig.Serialize(), byte(txscript.SigHashAll)), pkCompressed} - tx.TxIn[ix].Witness = txWitness - - //fmt.Printf("tx size: %d, witness %d: %d\n", tx.SerializeSize(), ix+1, tx.SerializeSize()-preTxSize) - if ix == 0 { - bytesIncur := bytes1stWitness + len(tx.TxIn) - 1 // e.g., 130 bytes for a 21-input tx - require.True(t, tx.SerializeSize()-preTxSize >= bytesIncur-5) - require.True(t, tx.SerializeSize()-preTxSize <= bytesIncur+5) - } else { - require.True(t, tx.SerializeSize()-preTxSize >= bytesPerWitness-5) - require.True(t, tx.SerializeSize()-preTxSize <= bytesPerWitness+5) - } - preTxSize = tx.SerializeSize() - } -} - -func TestP2WPHSize2In3Out(t *testing.T) { - // Generate payer/payee private keys and P2WPKH addresss - privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) - _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) - - // 2 example UTXO txids to use in the test. - utxosTxids := []string{ - "c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e", - "54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa", - } - - // Create a new transaction and add inputs - tx := wire.NewMsgTx(wire.TxVersion) - addTxInputs(t, tx, utxosTxids) - - // Add P2WPKH outputs - addTxOutputs(t, tx, payerScript, payeeScript) - - // Payer sign the redeeming transaction. - signTx(t, tx, payerScript, privateKey) - - // Estimate the tx size in vByte - // #nosec G701 always positive - vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3) - require.Equal(t, vBytes, vBytesEstimated) - require.Equal(t, vBytes, outTxBytesMin) -} - -func TestP2WPHSize21In3Out(t *testing.T) { - // Generate payer/payee private keys and P2WPKH addresss - privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) - _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) - - // Create a new transaction and add inputs - tx := wire.NewMsgTx(wire.TxVersion) - addTxInputs(t, tx, exampleTxids) - - // Add P2WPKH outputs - addTxOutputs(t, tx, payerScript, payeeScript) - - // Payer sign the redeeming transaction. - signTx(t, tx, payerScript, privateKey) - - // Estimate the tx size in vByte - // #nosec G701 always positive - vError := uint64(21 / 4) // 5 vBytes error tolerance - vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateSegWitTxSize(uint64(len(exampleTxids)), 3) - require.Equal(t, vBytesEstimated, outTxBytesMax) - if vBytes > vBytesEstimated { - require.True(t, vBytes-vBytesEstimated <= vError) - } else { - require.True(t, vBytesEstimated-vBytes <= vError) - } -} - -func TestP2WPHSizeXIn3Out(t *testing.T) { - // Generate payer/payee private keys and P2WPKH addresss - privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) - _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) - - // Create new transactions with X (2 <= X <= 21) inputs and 3 outputs respectively - for x := 2; x <= 21; x++ { - tx := wire.NewMsgTx(wire.TxVersion) - addTxInputs(t, tx, exampleTxids[:x]) - - // Add P2WPKH outputs - addTxOutputs(t, tx, payerScript, payeeScript) - - // Payer sign the redeeming transaction. - signTx(t, tx, payerScript, privateKey) - - // Estimate the tx size - // #nosec G701 always positive - vError := uint64(0.25 + float64(x)/4) // 1st witness incur 0.25 vByte error, other witness incur 1/4 vByte error tolerance, - vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) - vBytesEstimated := EstimateSegWitTxSize(uint64(len(exampleTxids[:x])), 3) - if vBytes > vBytesEstimated { - require.True(t, vBytes-vBytesEstimated <= vError) - //fmt.Printf("%d error percentage: %.2f%%\n", float64(vBytes-vBytesEstimated)/float64(vBytes)*100) - } else { - require.True(t, vBytesEstimated-vBytes <= vError) - //fmt.Printf("error percentage: %.2f%%\n", float64(vBytesEstimated-vBytes)/float64(vBytes)*100) - } - } -} - -func TestP2WPHSizeBreakdown(t *testing.T) { - txSize2In3Out := EstimateSegWitTxSize(2, 3) - require.Equal(t, outTxBytesMin, txSize2In3Out) - - sz := EstimateSegWitTxSize(1, 1) - fmt.Printf("1 input, 1 output: %d\n", sz) - - txSizeDepositor := SegWitTxSizeDepositor() - require.Equal(t, uint64(68), txSizeDepositor) - - txSizeWithdrawer := SegWitTxSizeWithdrawer() - require.Equal(t, uint64(171), txSizeWithdrawer) - require.Equal(t, txSize2In3Out, txSizeDepositor+txSizeWithdrawer) // 239 = 68 + 171 - - depositFee := DepositorFee(defaultDepositorFeeRate) - require.Equal(t, depositFee, 0.00001360) -} - -// helper function to create a new BitcoinChainClient +// helper function to create a test BitcoinChainClient func createTestClient(t *testing.T) *BTCChainClient { skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8" privateKey, err := crypto.HexToECDSA(skHex) @@ -433,14 +223,18 @@ func createTestClient(t *testing.T) *BTCChainClient { tss := interfaces.TestSigner{ PrivKey: privateKey, } - tssAddress := tss.BTCAddressWitnessPubkeyHash().EncodeAddress() - - // Create BitcoinChainClient - client := &BTCChainClient{ + return &BTCChainClient{ Tss: tss, Mu: &sync.Mutex{}, includedTxResults: make(map[string]*btcjson.GetTransactionResult), } +} + +// helper function to create a test BitcoinChainClient with UTXOs +func createTestClientWithUTXOs(t *testing.T) *BTCChainClient { + // Create BitcoinChainClient + client := createTestClient(t) + tssAddress := client.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() // Create 10 dummy UTXOs (22.44 BTC in total) client.utxos = make([]btcjson.ListUnspentResult, 0, 10) @@ -451,6 +245,153 @@ func createTestClient(t *testing.T) *BTCChainClient { return client } +func TestAddWithdrawTxOutputs(t *testing.T) { + // Create test signer and receiver address + signer, err := NewBTCSigner(config.BTCConfig{}, stub.NewTSSMainnet(), clientcommon.DefaultLoggers(), &metrics.TelemetryServer{}, nil) + require.NoError(t, err) + + // tss address and script + tssAddr := signer.tssSigner.BTCAddressWitnessPubkeyHash() + tssScript, err := PayToAddrScript(tssAddr) + require.NoError(t, err) + fmt.Printf("tss address: %s", tssAddr.EncodeAddress()) + + // receiver addresses + receiver := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" + to, err := chains.DecodeBtcAddress(receiver, chains.BtcMainnetChain().ChainId) + require.NoError(t, err) + toScript, err := PayToAddrScript(to) + require.NoError(t, err) + + // test cases + tests := []struct { + name string + tx *wire.MsgTx + to btcutil.Address + total float64 + amount float64 + nonce int64 + fees *big.Int + cancelTx bool + fail bool + message string + txout []*wire.TxOut + }{ + { + name: "should add outputs successfully", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 1.00012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 20000000, PkScript: toScript}, + {Value: 80000000, PkScript: tssScript}, + }, + }, + { + name: "should add outputs without change successfully", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.20012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 20000000, PkScript: toScript}, + }, + }, + { + name: "should cancel tx successfully", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 1.00012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + cancelTx: true, + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 100000000, PkScript: tssScript}, + }, + }, + { + name: "should fail on invalid amount", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 1.00012000, + amount: -0.5, + fail: true, + }, + { + name: "should fail when total < amount", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.00012000, + amount: 0.2, + fail: true, + }, + { + name: "should fail when total < fees + amount + nonce", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.20011000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: true, + message: "remainder value is negative", + }, + { + name: "should not produce duplicate nonce mark", + tx: wire.NewMsgTx(wire.TxVersion), + to: to, + total: 0.20022000, // 0.2 + fee + nonceMark * 2 + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: false, + txout: []*wire.TxOut{ + {Value: 10000, PkScript: tssScript}, + {Value: 20000000, PkScript: toScript}, + {Value: 9999, PkScript: tssScript}, // nonceMark - 1 + }, + }, + { + name: "should fail on invalid to address", + tx: wire.NewMsgTx(wire.TxVersion), + to: nil, + total: 1.00012000, + amount: 0.2, + nonce: 10000, + fees: big.NewInt(2000), + fail: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := signer.AddWithdrawTxOutputs(tt.tx, tt.to, tt.total, tt.amount, tt.nonce, tt.fees, tt.cancelTx) + if tt.fail { + require.Error(t, err) + if tt.message != "" { + require.Contains(t, err.Error(), tt.message) + } + return + } else { + require.NoError(t, err) + require.True(t, reflect.DeepEqual(tt.txout, tt.tx.TxOut)) + } + }) + } +} + func mineTxNSetNonceMark(ob *BTCChainClient, nonce uint64, txid string, preMarkIndex int) { // Mine transaction outTxID := ob.GetTxID(nonce) @@ -471,7 +412,7 @@ func mineTxNSetNonceMark(ob *BTCChainClient, nonce uint64, txid string, preMarkI } func TestSelectUTXOs(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) dummyTxID := "6e6f71d281146c1fc5c755b35908ee449f26786c84e2ae18f98b268de40b7ec4" // Case1: nonce = 0, bootstrap @@ -563,7 +504,7 @@ func TestUTXOConsolidation(t *testing.T) { dummyTxID := "6e6f71d281146c1fc5c755b35908ee449f26786c84e2ae18f98b268de40b7ec4" t.Run("should not consolidate", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 10, amount = 0.01, nonce = 1, rank = 10 @@ -577,7 +518,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate 1 utxo", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 9, amount = 0.01, nonce = 1, rank = 9 @@ -591,7 +532,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate 3 utxos", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 5, amount = 0.01, nonce = 0, rank = 5 @@ -610,7 +551,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate all utxos using rank 1", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 12, amount = 0.01, nonce = 0, rank = 1 @@ -629,7 +570,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate 3 utxos sparse", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 24105431, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 24105431 // input: utxoCap = 5, amount = 0.13, nonce = 24105432, rank = 5 @@ -647,7 +588,7 @@ func TestUTXOConsolidation(t *testing.T) { }) t.Run("should consolidate all utxos sparse", func(t *testing.T) { - ob := createTestClient(t) + ob := createTestClientWithUTXOs(t) mineTxNSetNonceMark(ob, 24105431, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 24105431 // input: utxoCap = 12, amount = 0.13, nonce = 24105432, rank = 1 @@ -685,5 +626,6 @@ func TestNewBTCSigner(t *testing.T) { clientcommon.DefaultLoggers(), &metrics.TelemetryServer{}, corecontext.NewZetaCoreContext(cfg)) + require.NoError(t, err) require.NotNil(t, btcSigner) } diff --git a/zetaclient/bitcoin/bitcoin_test.go b/zetaclient/bitcoin/bitcoin_test.go index 12e9e29136..4dae7f8590 100644 --- a/zetaclient/bitcoin/bitcoin_test.go +++ b/zetaclient/bitcoin/bitcoin_test.go @@ -96,7 +96,6 @@ func buildTX() (*wire.MsgTx, *txscript.TxSigHashes, int, int64, []byte, *btcec.P if err != nil { return nil, nil, 0, 0, nil, nil, false, err } - fmt.Printf("addr %v\n", addr.EncodeAddress()) hash, err := chainhash.NewHashFromStr(prevOut) if err != nil { @@ -109,7 +108,7 @@ func buildTX() (*wire.MsgTx, *txscript.TxSigHashes, int, int64, []byte, *btcec.P txIn := wire.NewTxIn(outpoint, nil, nil) tx.AddTxIn(txIn) - pkScript, err := PayToWitnessPubKeyHashScript(addr.WitnessProgram()) + pkScript, err := PayToAddrScript(addr) if err != nil { return nil, nil, 0, 0, nil, nil, false, err } diff --git a/zetaclient/bitcoin/fee.go b/zetaclient/bitcoin/fee.go new file mode 100644 index 0000000000..ab19240264 --- /dev/null +++ b/zetaclient/bitcoin/fee.go @@ -0,0 +1,226 @@ +package bitcoin + +import ( + "encoding/hex" + "fmt" + "math" + "math/big" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/pkg/chains" + clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" +) + +const ( + bytesPerKB = 1000 + bytesPerInput = 41 // each input is 41 bytes + bytesPerOutputP2TR = 43 // each P2TR output is 43 bytes + bytesPerOutputP2WSH = 43 // each P2WSH output is 43 bytes + bytesPerOutputP2WPKH = 31 // each P2WPKH output is 31 bytes + bytesPerOutputP2SH = 32 // each P2SH output is 32 bytes + bytesPerOutputP2PKH = 34 // each P2PKH output is 34 bytes + bytesPerOutputAvg = 37 // average size of all above types of outputs (36.6 bytes) + bytes1stWitness = 110 // the 1st witness incurs about 110 bytes and it may vary + bytesPerWitness = 108 // each additional witness incurs about 108 bytes and it may vary + defaultDepositorFeeRate = 20 // 20 sat/byte is the default depositor fee rate + + outTxBytesMin = uint64(239) // 239vB == EstimateSegWitTxSize(2, 2, toP2WPKH) + outTxBytesMax = uint64(1543) // 1543v == EstimateSegWitTxSize(21, 2, toP2TR) + outTxBytesAvg = uint64(245) // 245vB is a suggested gas limit for zeta core +) + +var ( + // The outtx size incurred by the depositor: 68vB + BtcOutTxBytesDepositor = OuttxSizeDepositor() + + // The outtx size incurred by the withdrawer: 177vB + BtcOutTxBytesWithdrawer = OuttxSizeWithdrawer() + + // The default depositor fee is 0.00001360 BTC (20 * 68vB / 100000000) + // default depositor fee calculation is based on a fixed fee rate of 20 sat/byte just for simplicity. + DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) +) + +// FeeRateToSatPerByte converts a fee rate in BTC/KB to sat/byte. +func FeeRateToSatPerByte(rate float64) *big.Int { + // #nosec G701 always in range + satPerKB := new(big.Int).SetInt64(int64(rate * btcutil.SatoshiPerBitcoin)) + return new(big.Int).Div(satPerKB, big.NewInt(bytesPerKB)) +} + +// WiredTxSize calculates the wired tx size in bytes +func WiredTxSize(numInputs uint64, numOutputs uint64) uint64 { + // Version 4 bytes + LockTime 4 bytes + Serialized varint size for the + // number of transaction inputs and outputs. + // #nosec G701 always positive + return uint64(8 + wire.VarIntSerializeSize(numInputs) + wire.VarIntSerializeSize(numOutputs)) +} + +// EstimateOuttxSize estimates the size of a outtx in vBytes +func EstimateOuttxSize(numInputs uint64, payees []btcutil.Address) (uint64, error) { + if numInputs == 0 { + return 0, nil + } + // #nosec G701 always positive + numOutputs := 2 + uint64(len(payees)) + bytesWiredTx := WiredTxSize(numInputs, numOutputs) + bytesInput := numInputs * bytesPerInput + bytesOutput := uint64(2) * bytesPerOutputP2WPKH // new nonce mark, change + + // calculate the size of the outputs to payees + bytesToPayees := uint64(0) + for _, to := range payees { + sizeOutput, err := GetOutputSizeByAddress(to) + if err != nil { + return 0, err + } + bytesToPayees += sizeOutput + } + // calculate the size of the witness + bytesWitness := bytes1stWitness + (numInputs-1)*bytesPerWitness + // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations + // Calculation for signed SegWit tx: blockchain.GetTransactionWeight(tx) / 4 + return bytesWiredTx + bytesInput + bytesOutput + bytesToPayees + bytesWitness/blockchain.WitnessScaleFactor, nil +} + +// GetOutputSizeByAddress returns the size of a tx output in bytes by the given address +func GetOutputSizeByAddress(to btcutil.Address) (uint64, error) { + switch addr := to.(type) { + case *chains.AddressTaproot: + if addr == nil { + return 0, nil + } + return bytesPerOutputP2TR, nil + case *btcutil.AddressWitnessScriptHash: + if addr == nil { + return 0, nil + } + return bytesPerOutputP2WSH, nil + case *btcutil.AddressWitnessPubKeyHash: + if addr == nil { + return 0, nil + } + return bytesPerOutputP2WPKH, nil + case *btcutil.AddressScriptHash: + if addr == nil { + return 0, nil + } + return bytesPerOutputP2SH, nil + case *btcutil.AddressPubKeyHash: + if addr == nil { + return 0, nil + } + return bytesPerOutputP2PKH, nil + default: + return 0, fmt.Errorf("cannot get output size for address type %T", to) + } +} + +// OuttxSizeDepositor returns outtx size (68vB) incurred by the depositor +func OuttxSizeDepositor() uint64 { + return bytesPerInput + bytesPerWitness/blockchain.WitnessScaleFactor +} + +// OuttxSizeWithdrawer returns outtx size (177vB) incurred by the withdrawer (1 input, 3 outputs) +func OuttxSizeWithdrawer() uint64 { + bytesWiredTx := WiredTxSize(1, 3) + bytesInput := uint64(1) * bytesPerInput // nonce mark + bytesOutput := uint64(2) * bytesPerOutputP2WPKH // 2 P2WPKH outputs: new nonce mark, change + bytesOutput += bytesPerOutputAvg // 1 output to withdrawer's address + return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor +} + +// DepositorFee calculates the depositor fee in BTC for a given sat/byte fee rate +// Note: the depositor fee is charged in order to cover the cost of spending the deposited UTXO in the future +func DepositorFee(satPerByte int64) float64 { + return float64(satPerByte) * float64(BtcOutTxBytesDepositor) / btcutil.SatoshiPerBitcoin +} + +// CalcBlockAvgFeeRate calculates the average gas rate (in sat/vByte) for a given block +func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *chaincfg.Params) (int64, error) { + // sanity check + if len(blockVb.Tx) == 0 { + return 0, errors.New("block has no transactions") + } + if len(blockVb.Tx) == 1 { + return 0, nil // only coinbase tx, it happens + } + txCoinbase := &blockVb.Tx[0] + if blockVb.Weight < blockchain.WitnessScaleFactor { + return 0, fmt.Errorf("block weight %d too small", blockVb.Weight) + } + if blockVb.Weight < txCoinbase.Weight { + return 0, fmt.Errorf("block weight %d less than coinbase tx weight %d", blockVb.Weight, txCoinbase.Weight) + } + if blockVb.Height <= 0 || blockVb.Height > math.MaxInt32 { + return 0, fmt.Errorf("invalid block height %d", blockVb.Height) + } + + // make sure the first tx is coinbase tx + txBytes, err := hex.DecodeString(txCoinbase.Hex) + if err != nil { + return 0, fmt.Errorf("failed to decode coinbase tx %s", txCoinbase.Txid) + } + tx, err := btcutil.NewTxFromBytes(txBytes) + if err != nil { + return 0, fmt.Errorf("failed to parse coinbase tx %s", txCoinbase.Txid) + } + if !blockchain.IsCoinBaseTx(tx.MsgTx()) { + return 0, fmt.Errorf("first tx %s is not coinbase tx", txCoinbase.Txid) + } + + // calculate fees earned by the miner + btcEarned := int64(0) + for _, out := range tx.MsgTx().TxOut { + if out.Value > 0 { + btcEarned += out.Value + } + } + // #nosec G701 checked above + subsidy := blockchain.CalcBlockSubsidy(int32(blockVb.Height), netParams) + if btcEarned < subsidy { + return 0, fmt.Errorf("miner earned %d, less than subsidy %d", btcEarned, subsidy) + } + txsFees := btcEarned - subsidy + + // sum up weight of all txs (<= 4 MWU) + txsWeight := int32(0) + for i, tx := range blockVb.Tx { + // coinbase doesn't pay fees, so we exclude it + if i > 0 && tx.Weight > 0 { + txsWeight += tx.Weight + } + } + + // calculate average fee rate. + vBytes := txsWeight / blockchain.WitnessScaleFactor + return txsFees / int64(vBytes), nil +} + +// CalcDepositorFee calculates the depositor fee for a given block +func CalcDepositorFee(blockVb *btcjson.GetBlockVerboseTxResult, chainID int64, netParams *chaincfg.Params, logger zerolog.Logger) float64 { + // use default fee for regnet + if chains.IsBitcoinRegnet(chainID) { + return DefaultDepositorFee + } + // mainnet dynamic fee takes effect only after a planned upgrade height + if chains.IsBitcoinMainnet(chainID) && blockVb.Height < DynamicDepositorFeeHeight { + return DefaultDepositorFee + } + + // calculate deposit fee rate + feeRate, err := CalcBlockAvgFeeRate(blockVb, netParams) + if err != nil { + feeRate = defaultDepositorFeeRate // use default fee rate if calculation fails, should not happen + logger.Error().Err(err).Msgf("cannot calculate fee rate for block %d", blockVb.Height) + } + // #nosec G701 always in range + feeRate = int64(float64(feeRate) * clientcommon.BTCOuttxGasPriceMultiplier) + return DepositorFee(feeRate) +} diff --git a/zetaclient/bitcoin/fee_test.go b/zetaclient/bitcoin/fee_test.go new file mode 100644 index 0000000000..c9276911cf --- /dev/null +++ b/zetaclient/bitcoin/fee_test.go @@ -0,0 +1,463 @@ +package bitcoin + +import ( + "math/rand" + "testing" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" +) + +const ( + // btc address script types + ScriptTypeP2TR = "witness_v1_taproot" + ScriptTypeP2WSH = "witness_v0_scripthash" + ScriptTypeP2WPKH = "witness_v0_keyhash" + ScriptTypeP2SH = "scripthash" + ScriptTypeP2PKH = "pubkeyhash" +) + +var testAddressMap = map[string]string{ + ScriptTypeP2TR: "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", + ScriptTypeP2WSH: "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", + ScriptTypeP2WPKH: "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y", + ScriptTypeP2SH: "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", + ScriptTypeP2PKH: "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", +} + +// 21 example UTXO txids to use in the test. +var exampleTxids = []string{ + "c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e", + "54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa", + "b18a55a34319cfbedebfcfe1a80fef2b92ad8894d06caf8293a0344824c2cfbc", + "969fb309a4df7c299972700da788b5d601c0c04bab4ab46fff79d0335a7d75de", + "6c71913061246ffc20e268c1b0e65895055c36bfbf1f8faf92dcad6f8242121e", + "ba6d6e88cb5a97556684a1232719a3ffe409c5c9501061e1f59741bc412b3585", + "69b56c3c8c5d1851f9eaec256cd49f290b477a5d43e2aef42ef25d3c1d9f4b33", + "b87effd4cb46fe1a575b5b1ba0289313dc9b4bc9e615a3c6cbc0a14186921fdf", + "3135433054523f5e220621c9e3d48efbbb34a6a2df65635c2a3e7d462d3e1cda", + "8495c22a9ce6359ab53aa048c13b41c64fdf5fe141f516ba2573cc3f9313f06e", + "f31583544b475370d7b9187c9a01b92e44fb31ac5fcfa7fc55565ac64043aa9a", + "c03d55f9f717c1df978623e2e6b397b720999242f9ead7db9b5988fee3fb3933", + "ee55688439b47a5410cdc05bac46be0094f3af54d307456fdfe6ba8caf336e0b", + "61895f86c70f0bc3eef55d9a00347b509fa90f7a344606a9774be98a3ee9e02a", + "ffabb401a19d04327bd4a076671d48467dbcde95459beeab23df21686fd01525", + "b7e1c03b9b73e4e90fc06da893072c5604203c49e66699acbb2f61485d822981", + "185614d21973990138e478ce10e0a4014352df58044276d4e4c0093aa140f482", + "4a2800f13d15dc0c82308761d6fe8f6d13b65e42d7ca96a42a3a7048830e8c55", + "fb98f52e91db500735b185797cebb5848afbfe1289922d87e03b98c3da5b85ef", + "7901c5e36d9e8456ac61b29b82048650672a889596cbd30a9f8910a589ffc5b3", + "6bcd0850fd2fa1404290ed04d78d4ae718414f16d4fbfd344951add8dcf60326", +} + +func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, btcutil.Address, []byte) { + privateKey, err := btcec.NewPrivateKey(btcec.S256()) + require.Nil(t, err) + pubKeyHash := btcutil.Hash160(privateKey.PubKey().SerializeCompressed()) + addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) + require.Nil(t, err) + //fmt.Printf("New address: %s\n", addr.EncodeAddress()) + pkScript, err := PayToAddrScript(addr) + require.Nil(t, err) + return privateKey, addr, pkScript +} + +// getTestAddrScript returns hard coded test address scripts by script type +func getTestAddrScript(t *testing.T, scriptType string) btcutil.Address { + chain := chains.BtcMainnetChain() + if inputAddress, ok := testAddressMap[scriptType]; ok { + address, err := chains.DecodeBtcAddress(inputAddress, chain.ChainId) + require.NoError(t, err) + return address + } else { + panic("unknown script type") + } +} + +// createPkScripts creates 10 random amount of scripts to the given address 'to' +func createPkScripts(t *testing.T, to btcutil.Address, repeat int) ([]btcutil.Address, [][]byte) { + pkScript, err := PayToAddrScript(to) + require.NoError(t, err) + + addrs := []btcutil.Address{} + pkScripts := [][]byte{} + for i := 0; i < repeat; i++ { + addrs = append(addrs, to) + pkScripts = append(pkScripts, pkScript) + } + return addrs, pkScripts +} + +func addTxInputs(t *testing.T, tx *wire.MsgTx, txids []string) { + preTxSize := tx.SerializeSize() + for _, txid := range txids { + hash, err := chainhash.NewHashFromStr(txid) + require.Nil(t, err) + outpoint := wire.NewOutPoint(hash, uint32(rand.Intn(100))) + txIn := wire.NewTxIn(outpoint, nil, nil) + tx.AddTxIn(txIn) + require.Equal(t, bytesPerInput, tx.SerializeSize()-preTxSize) + //fmt.Printf("tx size: %d, input %d size: %d\n", tx.SerializeSize(), i, tx.SerializeSize()-preTxSize) + preTxSize = tx.SerializeSize() + } +} + +func addTxOutputs(t *testing.T, tx *wire.MsgTx, payerScript []byte, payeeScripts [][]byte) { + preTxSize := tx.SerializeSize() + + // 1st output to payer + value1 := int64(1 + rand.Intn(100000000)) + txOut1 := wire.NewTxOut(value1, payerScript) + tx.AddTxOut(txOut1) + require.Equal(t, bytesPerOutputP2WPKH, tx.SerializeSize()-preTxSize) + //fmt.Printf("tx size: %d, output 1: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) + preTxSize = tx.SerializeSize() + + // output to payee list + for _, payeeScript := range payeeScripts { + value := int64(1 + rand.Intn(100000000)) + txOut := wire.NewTxOut(value, payeeScript) + tx.AddTxOut(txOut) + //fmt.Printf("tx size: %d, output %d: %d\n", tx.SerializeSize(), i+1, tx.SerializeSize()-preTxSize) + preTxSize = tx.SerializeSize() + } + + // 3rd output to payee + value3 := int64(1 + rand.Intn(100000000)) + txOut3 := wire.NewTxOut(value3, payerScript) + tx.AddTxOut(txOut3) + require.Equal(t, bytesPerOutputP2WPKH, tx.SerializeSize()-preTxSize) + //fmt.Printf("tx size: %d, last output: %d\n", tx.SerializeSize(), tx.SerializeSize()-preTxSize) +} + +func addTxInputsOutputsAndSignTx( + t *testing.T, tx *wire.MsgTx, + privateKey *btcec.PrivateKey, + payerScript []byte, + txids []string, + payeeScripts [][]byte) { + // Add inputs + addTxInputs(t, tx, txids) + + // Add outputs + addTxOutputs(t, tx, payerScript, payeeScripts) + + // Payer sign the redeeming transaction. + signTx(t, tx, payerScript, privateKey) +} + +func signTx(t *testing.T, tx *wire.MsgTx, payerScript []byte, privateKey *btcec.PrivateKey) { + preTxSize := tx.SerializeSize() + sigHashes := txscript.NewTxSigHashes(tx) + for ix := range tx.TxIn { + amount := int64(1 + rand.Intn(100000000)) + witnessHash, err := txscript.CalcWitnessSigHash(payerScript, sigHashes, txscript.SigHashAll, tx, ix, amount) + require.Nil(t, err) + sig, err := privateKey.Sign(witnessHash) + require.Nil(t, err) + + pkCompressed := privateKey.PubKey().SerializeCompressed() + txWitness := wire.TxWitness{append(sig.Serialize(), byte(txscript.SigHashAll)), pkCompressed} + tx.TxIn[ix].Witness = txWitness + + //fmt.Printf("tx size: %d, witness %d: %d\n", tx.SerializeSize(), ix+1, tx.SerializeSize()-preTxSize) + if ix == 0 { + bytesIncur := bytes1stWitness + len(tx.TxIn) - 1 // e.g., 130 bytes for a 21-input tx + require.True(t, tx.SerializeSize()-preTxSize >= bytesIncur-5) + require.True(t, tx.SerializeSize()-preTxSize <= bytesIncur+5) + } else { + require.True(t, tx.SerializeSize()-preTxSize >= bytesPerWitness-5) + require.True(t, tx.SerializeSize()-preTxSize <= bytesPerWitness+5) + } + preTxSize = tx.SerializeSize() + } +} + +func TestOutTxSize2In3Out(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + _, payee, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) + + // 2 example UTXO txids to use in the test. + utxosTxids := exampleTxids[:2] + + // Create a new transaction + tx := wire.NewMsgTx(wire.TxVersion) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, utxosTxids, [][]byte{payeeScript}) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vError := uint64(1) // 1 vByte error tolerance + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated, err := EstimateOuttxSize(uint64(len(utxosTxids)), []btcutil.Address{payee}) + require.NoError(t, err) + if vBytes > vBytesEstimated { + require.True(t, vBytes-vBytesEstimated <= vError) + } else { + require.True(t, vBytesEstimated-vBytes <= vError) + } +} + +func TestOutTxSize21In3Out(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + _, payee, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) + + // Create a new transaction + tx := wire.NewMsgTx(wire.TxVersion) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids, [][]byte{payeeScript}) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vError := uint64(21 / 4) // 5 vBytes error tolerance + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated, err := EstimateOuttxSize(uint64(len(exampleTxids)), []btcutil.Address{payee}) + require.NoError(t, err) + if vBytes > vBytesEstimated { + require.True(t, vBytes-vBytesEstimated <= vError) + } else { + require.True(t, vBytesEstimated-vBytes <= vError) + } +} + +func TestOutTxSizeXIn3Out(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + _, payee, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) + + // Create new transactions with X (2 <= X <= 21) inputs and 3 outputs respectively + for x := 2; x <= 21; x++ { + // Create transaction. Add inputs and outputs and sign the transaction + tx := wire.NewMsgTx(wire.TxVersion) + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:x], [][]byte{payeeScript}) + + // Estimate the tx size + // #nosec G701 always positive + vError := uint64(0.25 + float64(x)/4) // 1st witness incurs 0.25 more vByte error than others (which incurs 1/4 vByte per witness) + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated, err := EstimateOuttxSize(uint64(len(exampleTxids[:x])), []btcutil.Address{payee}) + require.NoError(t, err) + if vBytes > vBytesEstimated { + require.True(t, vBytes-vBytesEstimated <= vError) + //fmt.Printf("%d error percentage: %.2f%%\n", float64(vBytes-vBytesEstimated)/float64(vBytes)*100) + } else { + require.True(t, vBytesEstimated-vBytes <= vError) + //fmt.Printf("error percentage: %.2f%%\n", float64(vBytesEstimated-vBytes)/float64(vBytes)*100) + } + } +} + +func TestGetOutputSizeByAddress(t *testing.T) { + // test nil P2TR address and non-nil P2TR address + nilP2TR := (*chains.AddressTaproot)(nil) + sizeNilP2TR, err := GetOutputSizeByAddress(nilP2TR) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2TR) + + addrP2TR := getTestAddrScript(t, ScriptTypeP2TR) + sizeP2TR, err := GetOutputSizeByAddress(addrP2TR) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2TR), sizeP2TR) + + // test nil P2WSH address and non-nil P2WSH address + nilP2WSH := (*btcutil.AddressWitnessScriptHash)(nil) + sizeNilP2WSH, err := GetOutputSizeByAddress(nilP2WSH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2WSH) + + addrP2WSH := getTestAddrScript(t, ScriptTypeP2WSH) + sizeP2WSH, err := GetOutputSizeByAddress(addrP2WSH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2WSH), sizeP2WSH) + + // test nil P2WPKH address and non-nil P2WPKH address + nilP2WPKH := (*btcutil.AddressWitnessPubKeyHash)(nil) + sizeNilP2WPKH, err := GetOutputSizeByAddress(nilP2WPKH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2WPKH) + + addrP2WPKH := getTestAddrScript(t, ScriptTypeP2WPKH) + sizeP2WPKH, err := GetOutputSizeByAddress(addrP2WPKH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2WPKH), sizeP2WPKH) + + // test nil P2SH address and non-nil P2SH address + nilP2SH := (*btcutil.AddressScriptHash)(nil) + sizeNilP2SH, err := GetOutputSizeByAddress(nilP2SH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2SH) + + addrP2SH := getTestAddrScript(t, ScriptTypeP2SH) + sizeP2SH, err := GetOutputSizeByAddress(addrP2SH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2SH), sizeP2SH) + + // test nil P2PKH address and non-nil P2PKH address + nilP2PKH := (*btcutil.AddressPubKeyHash)(nil) + sizeNilP2PKH, err := GetOutputSizeByAddress(nilP2PKH) + require.NoError(t, err) + require.Equal(t, uint64(0), sizeNilP2PKH) + + addrP2PKH := getTestAddrScript(t, ScriptTypeP2PKH) + sizeP2PKH, err := GetOutputSizeByAddress(addrP2PKH) + require.NoError(t, err) + require.Equal(t, uint64(bytesPerOutputP2PKH), sizeP2PKH) + + // test unsupported address type + nilP2PK := (*btcutil.AddressPubKey)(nil) + sizeP2PK, err := GetOutputSizeByAddress(nilP2PK) + require.ErrorContains(t, err, "cannot get output size for address type") + require.Equal(t, uint64(0), sizeP2PK) +} + +func TestOutputSizeP2TR(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2TR) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOutputSizeP2WSH(t *testing.T) { + // Generate payer/payee private keys and P2WPKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2WSH) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOutputSizeP2SH(t *testing.T) { + // Generate payer/payee private keys and P2SH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2SH) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOutputSizeP2PKH(t *testing.T) { + // Generate payer/payee private keys and P2PKH addresss + privateKey, _, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) + payee := getTestAddrScript(t, ScriptTypeP2PKH) + + // Create a new transaction and 10 random amount of payee scripts + tx := wire.NewMsgTx(wire.TxVersion) + payees, payeeScripts := createPkScripts(t, payee, 10) + + // Add inputs and outputs and sign the transaction + addTxInputsOutputsAndSignTx(t, tx, privateKey, payerScript, exampleTxids[:2], payeeScripts) + + // Estimate the tx size in vByte + // #nosec G701 always positive + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated, err := EstimateOuttxSize(2, payees) + require.NoError(t, err) + require.Equal(t, vBytes, vBytesEstimated) +} + +func TestOuttxSizeBreakdown(t *testing.T) { + // a list of all types of addresses + payees := []btcutil.Address{ + getTestAddrScript(t, ScriptTypeP2TR), + getTestAddrScript(t, ScriptTypeP2WSH), + getTestAddrScript(t, ScriptTypeP2WPKH), + getTestAddrScript(t, ScriptTypeP2SH), + getTestAddrScript(t, ScriptTypeP2PKH), + } + + // add all outtx sizes paying to each address + txSizeTotal := uint64(0) + for _, payee := range payees { + sizeOutput, err := EstimateOuttxSize(2, []btcutil.Address{payee}) + require.NoError(t, err) + txSizeTotal += sizeOutput + } + + // calculate the average outtx size + // #nosec G701 always in range + txSizeAverage := uint64((float64(txSizeTotal))/float64(len(payees)) + 0.5) + + // get deposit fee + txSizeDepositor := OuttxSizeDepositor() + require.Equal(t, uint64(68), txSizeDepositor) + + // get withdrawer fee + txSizeWithdrawer := OuttxSizeWithdrawer() + require.Equal(t, uint64(177), txSizeWithdrawer) + + // total outtx size == (deposit fee + withdrawer fee), 245 = 68 + 177 + require.Equal(t, outTxBytesAvg, txSizeAverage) + require.Equal(t, txSizeAverage, txSizeDepositor+txSizeWithdrawer) + + // check default depositor fee + depositFee := DepositorFee(defaultDepositorFeeRate) + require.Equal(t, depositFee, 0.00001360) +} + +func TestOuttxSizeMinMaxError(t *testing.T) { + // P2TR output is the largest in size; P2WPKH is the smallest + toP2TR := getTestAddrScript(t, ScriptTypeP2TR) + toP2WPKH := getTestAddrScript(t, ScriptTypeP2WPKH) + + // Estimate the largest outtx size in vByte + sizeMax, err := EstimateOuttxSize(21, []btcutil.Address{toP2TR}) + require.NoError(t, err) + require.Equal(t, outTxBytesMax, sizeMax) + + // Estimate the smallest outtx size in vByte + sizeMin, err := EstimateOuttxSize(2, []btcutil.Address{toP2WPKH}) + require.NoError(t, err) + require.Equal(t, outTxBytesMin, sizeMin) + + // Estimate unknown address type + nilP2PK := (*btcutil.AddressPubKey)(nil) + size, err := EstimateOuttxSize(1, []btcutil.Address{nilP2PK}) + require.Error(t, err) + require.Equal(t, uint64(0), size) +} diff --git a/zetaclient/bitcoin/inbound_tracker.go b/zetaclient/bitcoin/inbound_tracker.go index 2a6b80c124..b948a6c979 100644 --- a/zetaclient/bitcoin/inbound_tracker.go +++ b/zetaclient/bitcoin/inbound_tracker.go @@ -10,10 +10,11 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetabridge" ) -func (ob *BTCChainClient) ExternalChainWatcherForNewInboundTrackerSuggestions() { - ticker, err := types.NewDynamicTicker("Bitcoin_WatchInTx_InboundTrackerSuggestions", ob.GetChainParams().InTxTicker) +// WatchIntxTracker watches zetacore for bitcoin intx trackers +func (ob *BTCChainClient) WatchIntxTracker() { + ticker, err := types.NewDynamicTicker("Bitcoin_WatchIntxTracker", ob.GetChainParams().InTxTicker) if err != nil { - ob.logger.WatchInTx.Err(err).Msg("error creating ticker") + ob.logger.InTx.Err(err).Msg("error creating ticker") return } @@ -21,13 +22,16 @@ func (ob *BTCChainClient) ExternalChainWatcherForNewInboundTrackerSuggestions() for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } err := ob.ObserveTrackerSuggestions() if err != nil { - ob.logger.WatchInTx.Error().Err(err).Msg("error observing in tx") + ob.logger.InTx.Error().Err(err).Msgf("error observing intx tracker for chain %d", ob.chain.ChainId) } - ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.WatchInTx) + ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.InTx) case <-ob.stop: - ob.logger.WatchInTx.Info().Msg("ExternalChainWatcher for BTC inboundTrackerSuggestions stopped") + ob.logger.InTx.Info().Msgf("WatchIntxTracker stopped for chain %d", ob.chain.ChainId) return } } @@ -39,12 +43,12 @@ func (ob *BTCChainClient) ObserveTrackerSuggestions() error { return err } for _, tracker := range trackers { - ob.logger.WatchInTx.Info().Msgf("checking tracker with hash :%s and coin-type :%s ", tracker.TxHash, tracker.CoinType) + ob.logger.InTx.Info().Msgf("checking tracker with hash :%s and coin-type :%s ", tracker.TxHash, tracker.CoinType) ballotIdentifier, err := ob.CheckReceiptForBtcTxHash(tracker.TxHash, true) if err != nil { return err } - ob.logger.WatchInTx.Info().Msgf("Vote submitted for inbound Tracker,Chain : %s,Ballot Identifier : %s, coin-type %s", ob.chain.ChainName, ballotIdentifier, coin.CoinType_Gas.String()) + ob.logger.InTx.Info().Msgf("Vote submitted for inbound Tracker,Chain : %s,Ballot Identifier : %s, coin-type %s", ob.chain.ChainName, ballotIdentifier, coin.CoinType_Gas.String()) } return nil } @@ -69,13 +73,13 @@ func (ob *BTCChainClient) CheckReceiptForBtcTxHash(txHash string, vote bool) (st if len(blockVb.Tx) <= 1 { return "", fmt.Errorf("block %d has no transactions", blockVb.Height) } - depositorFee := CalcDepositorFee(blockVb, ob.chain.ChainId, ob.netParams, ob.logger.WatchInTx) + depositorFee := CalcDepositorFee(blockVb, ob.chain.ChainId, ob.netParams, ob.logger.InTx) tss, err := ob.zetaClient.GetBtcTssAddress(ob.chain.ChainId) if err != nil { return "", err } // #nosec G701 always positive - event, err := GetBtcEvent(*tx, tss, uint64(blockVb.Height), &ob.logger.WatchInTx, ob.netParams, depositorFee) + event, err := GetBtcEvent(ob.rpcClient, *tx, tss, uint64(blockVb.Height), ob.logger.InTx, ob.netParams, depositorFee) if err != nil { return "", err } @@ -91,10 +95,10 @@ func (ob *BTCChainClient) CheckReceiptForBtcTxHash(txHash string, vote bool) (st } zetaHash, ballot, err := ob.zetaClient.PostVoteInbound(zetabridge.PostVoteInboundGasLimit, zetabridge.PostVoteInboundExecutionGasLimit, msg) if err != nil { - ob.logger.WatchInTx.Error().Err(err).Msg("error posting to zeta core") + ob.logger.InTx.Error().Err(err).Msg("error posting to zeta core") return "", err } else if zetaHash != "" { - ob.logger.WatchInTx.Info().Msgf("BTC deposit detected and reported: PostVoteInbound zeta tx hash: %s inTx %s ballot %s fee %v", + ob.logger.InTx.Info().Msgf("BTC deposit detected and reported: PostVoteInbound zeta tx hash: %s inTx %s ballot %s fee %v", zetaHash, txHash, ballot, depositorFee) } return msg.Digest(), nil diff --git a/zetaclient/bitcoin/tx_script.go b/zetaclient/bitcoin/tx_script.go new file mode 100644 index 0000000000..08046b0171 --- /dev/null +++ b/zetaclient/bitcoin/tx_script.go @@ -0,0 +1,229 @@ +package bitcoin + +import ( + "bytes" + "encoding/hex" + "fmt" + "strconv" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/cosmos/btcutil/base58" + "github.com/pkg/errors" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/constant" + "golang.org/x/crypto/ripemd160" +) + +const ( + // Lenth of P2TR script [OP_1 0x20 <32-byte-hash>] + LengthScriptP2TR = 34 + + // Length of P2WSH script [OP_0 0x20 <32-byte-hash>] + LengthScriptP2WSH = 34 + + // Length of P2WPKH script [OP_0 0x14 <20-byte-hash>] + LengthScriptP2WPKH = 22 + + // Length of P2SH script [OP_HASH160 0x14 <20-byte-hash> OP_EQUAL] + LengthScriptP2SH = 23 + + // Length of P2PKH script [OP_DUP OP_HASH160 0x14 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG] + LengthScriptP2PKH = 25 +) + +// PayToAddrScript creates a new script to pay a transaction output to a the +// specified address. +func PayToAddrScript(addr btcutil.Address) ([]byte, error) { + switch addr := addr.(type) { + case *chains.AddressTaproot: + return chains.PayToWitnessTaprootScript(addr.ScriptAddress()) + default: + return txscript.PayToAddrScript(addr) + } +} + +// IsPkScriptP2TR checks if the given script is a P2TR script +func IsPkScriptP2TR(script []byte) bool { + return len(script) == LengthScriptP2TR && script[0] == txscript.OP_1 && script[1] == 0x20 +} + +// IsPkScriptP2WSH checks if the given script is a P2WSH script +func IsPkScriptP2WSH(script []byte) bool { + return len(script) == LengthScriptP2WSH && script[0] == txscript.OP_0 && script[1] == 0x20 +} + +// IsPkScriptP2WPKH checks if the given script is a P2WPKH script +func IsPkScriptP2WPKH(script []byte) bool { + return len(script) == LengthScriptP2WPKH && script[0] == txscript.OP_0 && script[1] == 0x14 +} + +// IsPkScriptP2SH checks if the given script is a P2SH script +func IsPkScriptP2SH(script []byte) bool { + return len(script) == LengthScriptP2SH && + script[0] == txscript.OP_HASH160 && + script[1] == 0x14 && + script[22] == txscript.OP_EQUAL +} + +// IsPkScriptP2PKH checks if the given script is a P2PKH script +func IsPkScriptP2PKH(script []byte) bool { + return len(script) == LengthScriptP2PKH && + script[0] == txscript.OP_DUP && + script[1] == txscript.OP_HASH160 && + script[2] == 0x14 && + script[23] == txscript.OP_EQUALVERIFY && + script[24] == txscript.OP_CHECKSIG +} + +// DecodeScriptP2TR decodes address from P2TR script +func DecodeScriptP2TR(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) + if err != nil { + return "", errors.Wrapf(err, "error decoding script %s", scriptHex) + } + if !IsPkScriptP2TR(script) { + return "", fmt.Errorf("invalid P2TR script: %s", scriptHex) + } + witnessProg := script[2:] + receiverAddress, err := chains.NewAddressTaproot(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting address from script %s", scriptHex) + } + return receiverAddress.EncodeAddress(), nil +} + +// DecodeScriptP2WSH decodes address from P2WSH script +func DecodeScriptP2WSH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) + if err != nil { + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) + } + if !IsPkScriptP2WSH(script) { + return "", fmt.Errorf("invalid P2WSH script: %s", scriptHex) + } + witnessProg := script[2:] + receiverAddress, err := btcutil.NewAddressWitnessScriptHash(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) + } + return receiverAddress.EncodeAddress(), nil +} + +// DecodeScriptP2WPKH decodes address from P2WPKH script +func DecodeScriptP2WPKH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) + if err != nil { + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) + } + if !IsPkScriptP2WPKH(script) { + return "", fmt.Errorf("invalid P2WPKH script: %s", scriptHex) + } + witnessProg := script[2:] + receiverAddress, err := btcutil.NewAddressWitnessPubKeyHash(witnessProg, net) + if err != nil { // should never happen + return "", errors.Wrapf(err, "error getting receiver from script: %s", scriptHex) + } + return receiverAddress.EncodeAddress(), nil +} + +// DecodeScriptP2SH decodes address from P2SH script +func DecodeScriptP2SH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) + if err != nil { + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) + } + if !IsPkScriptP2SH(script) { + return "", fmt.Errorf("invalid P2SH script: %s", scriptHex) + } + scriptHash := script[2:22] + return EncodeAddress(scriptHash, net.ScriptHashAddrID), nil +} + +// DecodeScriptP2PKH decodes address from P2PKH script +func DecodeScriptP2PKH(scriptHex string, net *chaincfg.Params) (string, error) { + script, err := hex.DecodeString(scriptHex) + if err != nil { + return "", errors.Wrapf(err, "error decoding script: %s", scriptHex) + } + if !IsPkScriptP2PKH(script) { + return "", fmt.Errorf("invalid P2PKH script: %s", scriptHex) + } + pubKeyHash := script[3:23] + return EncodeAddress(pubKeyHash, net.PubKeyHashAddrID), nil +} + +// DecodeOpReturnMemo decodes memo from OP_RETURN script +// returns (memo, found, error) +func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { + if len(scriptHex) >= 4 && scriptHex[:2] == "6a" { // OP_RETURN + memoSize, err := strconv.ParseInt(scriptHex[2:4], 16, 32) + if err != nil { + return nil, false, errors.Wrapf(err, "error decoding memo size: %s", scriptHex) + } + if int(memoSize) != (len(scriptHex)-4)/2 { + return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(scriptHex)-4)/2) + } + memoBytes, err := hex.DecodeString(scriptHex[4:]) + if err != nil { + return nil, false, errors.Wrapf(err, "error hex decoding memo: %s", scriptHex) + } + if bytes.Equal(memoBytes, []byte(constant.DonationMessage)) { + return nil, false, fmt.Errorf("donation tx: %s", txid) + } + return memoBytes, true, nil + } + return nil, false, nil +} + +// EncodeAddress returns a human-readable payment address given a ripemd160 hash +// and netID which encodes the bitcoin network and address type. It is used +// in both pay-to-pubkey-hash (P2PKH) and pay-to-script-hash (P2SH) address +// encoding. +// Note: this function is a copy of the function in btcutil/address.go +func EncodeAddress(hash160 []byte, netID byte) string { + // Format is 1 byte for a network and address class (i.e. P2PKH vs + // P2SH), 20 bytes for a RIPEMD160 hash, and 4 bytes of checksum. + return base58.CheckEncode(hash160[:ripemd160.Size], netID) +} + +// DecodeTSSVout decodes receiver and amount from a given TSS vout +func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chain) (string, int64, error) { + // parse amount + amount, err := GetSatoshis(vout.Value) + if err != nil { + return "", 0, errors.Wrap(err, "error getting satoshis") + } + // get btc chain params + chainParams, err := chains.GetBTCChainParams(chain.ChainId) + if err != nil { + return "", 0, errors.Wrapf(err, "error GetBTCChainParams for chain %d", chain.ChainId) + } + // decode cctx receiver address + addr, err := chains.DecodeBtcAddress(receiverExpected, chain.ChainId) + if err != nil { + return "", 0, errors.Wrapf(err, "error decoding receiver %s", receiverExpected) + } + // parse receiver address from vout + var receiverVout string + switch addr.(type) { + case *chains.AddressTaproot: + receiverVout, err = DecodeScriptP2TR(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressWitnessScriptHash: + receiverVout, err = DecodeScriptP2WSH(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressWitnessPubKeyHash: + receiverVout, err = DecodeScriptP2WPKH(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressScriptHash: + receiverVout, err = DecodeScriptP2SH(vout.ScriptPubKey.Hex, chainParams) + case *btcutil.AddressPubKeyHash: + receiverVout, err = DecodeScriptP2PKH(vout.ScriptPubKey.Hex, chainParams) + default: + return "", 0, fmt.Errorf("unsupported receiver address type: %T", addr) + } + if err != nil { + return "", 0, errors.Wrap(err, "error decoding TSS vout") + } + return receiverVout, amount, nil +} diff --git a/zetaclient/bitcoin/tx_script_test.go b/zetaclient/bitcoin/tx_script_test.go new file mode 100644 index 0000000000..c29e22c556 --- /dev/null +++ b/zetaclient/bitcoin/tx_script_test.go @@ -0,0 +1,521 @@ +package bitcoin + +import ( + "encoding/hex" + "path" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/constant" + "github.com/zeta-chain/zetacore/zetaclient/testutils" +) + +func TestDecodeVoutP2TR(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + chain := chains.BtcMainnetChain() + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + require.Len(t, rawResult.Vout, 2) + + // decode vout 0, P2TR + receiver, err := DecodeScriptP2TR(rawResult.Vout[0].ScriptPubKey.Hex, net) + require.NoError(t, err) + require.Equal(t, "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9", receiver) +} + +func TestDecodeVoutP2TRErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + chain := chains.BtcMainnetChain() + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := 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 := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2TR script") + }) + t.Run("should return error on invalid OP_1", func(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 := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2TR script") + }) + t.Run("should return error on wrong hash length", func(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 := DecodeScriptP2TR(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2TR script") + }) +} + +func TestDecodeVoutP2WSH(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + chain := chains.BtcMainnetChain() + txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + require.Len(t, rawResult.Vout, 1) + + // decode vout 0, P2WSH + receiver, err := DecodeScriptP2WSH(rawResult.Vout[0].ScriptPubKey.Hex, net) + require.NoError(t, err) + require.Equal(t, "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc", receiver) +} + +func TestDecodeVoutP2WSHErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + chain := chains.BtcMainnetChain() + txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := 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 := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WSH script") + }) + t.Run("should return error on invalid OP_0", func(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 := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WSH script") + }) + t.Run("should return error on wrong hash length", func(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 := DecodeScriptP2WSH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WSH script") + }) +} + +func TestDecodeP2WPKHVout(t *testing.T) { + // load archived outtx raw result + // https://mempool.space/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 + chain := chains.BtcMainnetChain() + nonce := uint64(148) + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + require.Len(t, rawResult.Vout, 3) + + // decode vout 0, nonce mark 148 + receiver, err := 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 = 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 = DecodeScriptP2WPKH(rawResult.Vout[2].ScriptPubKey.Hex, net) + require.NoError(t, err) + require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) +} + +func TestDecodeP2WPKHVoutErrors(t *testing.T) { + // load archived outtx raw result + // https://mempool.space/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 + chain := chains.BtcMainnetChain() + nonce := uint64(148) + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := 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 := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WPKH script") + }) + t.Run("should return error on wrong hash length", func(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 := DecodeScriptP2WPKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2WPKH script") + }) +} + +func TestDecodeVoutP2SH(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + chain := chains.BtcMainnetChain() + txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + require.Len(t, rawResult.Vout, 2) + + // decode vout 0, P2SH + receiver, err := DecodeScriptP2SH(rawResult.Vout[0].ScriptPubKey.Hex, net) + require.NoError(t, err) + require.Equal(t, "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE", receiver) +} + +func TestDecodeVoutP2SHErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + chain := chains.BtcMainnetChain() + txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := 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 := 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 := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2SH script") + }) + t.Run("should return error on wrong data length", func(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 := 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 := DecodeScriptP2SH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2SH script") + }) +} + +func TestDecodeVoutP2PKH(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + chain := chains.BtcMainnetChain() + txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + require.Len(t, rawResult.Vout, 2) + + // decode vout 0, P2PKH + receiver, err := DecodeScriptP2PKH(rawResult.Vout[0].ScriptPubKey.Hex, net) + require.NoError(t, err) + require.Equal(t, "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte", receiver) +} + +func TestDecodeVoutP2PKHErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + chain := chains.BtcMainnetChain() + txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" + net := &chaincfg.MainNetParams + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + t.Run("should return error on invalid script", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + _, err := 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 := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") + }) + t.Run("should return error on invalid OP_DUP", func(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 := 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 := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") + }) + t.Run("should return error on wrong data length", func(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 := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") + }) + t.Run("should return error on invalid OP_EQUALVERIFY", func(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 := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") + }) + t.Run("should return error on invalid OP_CHECKSIG", func(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 := DecodeScriptP2PKH(invalidVout.ScriptPubKey.Hex, net) + require.ErrorContains(t, err, "invalid P2PKH script") + }) +} + +func TestDecodeOpReturnMemo(t *testing.T) { + // load archived intx raw result + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + chain := chains.BtcMainnetChain() + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + scriptHex := "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf" + rawResult := testutils.LoadBTCIntxRawResult(t, chain.ChainId, txHash, false) + require.True(t, len(rawResult.Vout) >= 2) + require.Equal(t, scriptHex, rawResult.Vout[1].ScriptPubKey.Hex) + + t.Run("should decode memo from OP_RETURN output", func(t *testing.T) { + memo, found, err := DecodeOpReturnMemo(rawResult.Vout[1].ScriptPubKey.Hex, txHash) + require.NoError(t, err) + require.True(t, found) + // [OP_RETURN, 0x14,<20-byte-hash>] + require.Equal(t, scriptHex[4:], hex.EncodeToString(memo)) + }) + t.Run("should return nil memo non-OP_RETURN output", func(t *testing.T) { + // modify the OP_RETURN to OP_1 + scriptInvalid := strings.Replace(scriptHex, "6a", "51", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.NoError(t, err) + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return nil memo on invalid script", func(t *testing.T) { + // use known short script + scriptInvalid := "00" + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.NoError(t, err) + require.False(t, found) + require.Nil(t, memo) + }) +} + +func TestDecodeOpReturnMemoErrors(t *testing.T) { + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + scriptHex := "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf" + + t.Run("should return error on invalid memo size", func(t *testing.T) { + // use invalid memo size + scriptInvalid := strings.Replace(scriptHex, "6a14", "6axy", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.ErrorContains(t, err, "error decoding memo size") + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return error on memo size mismatch", func(t *testing.T) { + // use wrong memo size + scriptInvalid := strings.Replace(scriptHex, "6a14", "6a13", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.ErrorContains(t, err, "memo size mismatch") + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return error on invalid hex", func(t *testing.T) { + // use invalid hex + scriptInvalid := strings.Replace(scriptHex, "6a1467", "6a14xy", 1) + memo, found, err := DecodeOpReturnMemo(scriptInvalid, txHash) + require.ErrorContains(t, err, "error hex decoding memo") + require.False(t, found) + require.Nil(t, memo) + }) + t.Run("should return nil memo on donation tx", func(t *testing.T) { + // use donation sctipt "6a0a4920616d207269636821" + scriptDonation := "6a0a" + hex.EncodeToString([]byte(constant.DonationMessage)) + memo, found, err := DecodeOpReturnMemo(scriptDonation, txHash) + require.ErrorContains(t, err, "donation tx") + require.False(t, found) + require.Nil(t, memo) + }) +} + +func TestDecodeTSSVout(t *testing.T) { + chain := chains.BtcMainnetChain() + + t.Run("should decode P2TR vout", func(t *testing.T) { + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(45000), amount) + }) + t.Run("should decode P2WSH vout", func(t *testing.T) { + // https://mempool.space/tx/791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53 + txHash := "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WSH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + receiverExpected := "bc1qqv6pwn470vu0tssdfha4zdk89v3c8ch5lsnyy855k9hcrcv3evequdmjmc" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(36557203), amount) + }) + t.Run("should decode P2WPKH vout", func(t *testing.T) { + // https://mempool.space/tx/5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b + txHash := "5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2WPKH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + receiverExpected := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(79938), amount) + }) + t.Run("should decode P2SH vout", func(t *testing.T) { + // https://mempool.space/tx/fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21 + txHash := "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2SH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + receiverExpected := "327z4GyFM8Y8DiYfasGKQWhRK4MvyMSEgE" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(1003881), amount) + }) + t.Run("should decode P2PKH vout", func(t *testing.T) { + // https://mempool.space/tx/9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca + txHash := "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2PKH", txHash)) + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + + receiverExpected := "1FueivsE338W2LgifJ25HhTcVJ7CRT8kte" + receiver, amount, err := DecodeTSSVout(rawResult.Vout[0], receiverExpected, chain) + require.NoError(t, err) + require.Equal(t, receiverExpected, receiver) + require.Equal(t, int64(1140000), amount) + }) +} + +func TestDecodeTSSVoutErrors(t *testing.T) { + // load archived tx raw result + // https://mempool.space/tx/259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7 + chain := chains.BtcMainnetChain() + txHash := "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7" + nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCTxByType(chain.ChainId, "P2TR", txHash)) + + var rawResult btcjson.TxRawResult + testutils.LoadObjectFromJSONFile(t, &rawResult, nameTx) + receiverExpected := "bc1p4scddlkkuw9486579autxumxmkvuphm5pz4jvf7f6pdh50p2uzqstawjt9" + + 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 := DecodeTSSVout(invalidVout, receiverExpected, chain) + require.ErrorContains(t, err, "error getting satoshis") + require.Empty(t, receiver) + require.Zero(t, amount) + }) + t.Run("should return error on invalid btc chain", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // use invalid chain + invalidChain := chains.Chain{ChainId: 123} + receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, invalidChain) + require.ErrorContains(t, err, "error GetBTCChainParams") + require.Empty(t, receiver) + require.Zero(t, amount) + }) + t.Run("should return error when invalid receiver passed", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + // use testnet params to decode mainnet receiver + wrongChain := chains.BtcTestNetChain() + receiver, amount, err := DecodeTSSVout(invalidVout, "bc1qulmx8ej27cj0xe20953cztr2excnmsqvuh0s5c", wrongChain) + require.ErrorContains(t, err, "error decoding receiver") + require.Empty(t, receiver) + require.Zero(t, amount) + }) + t.Run("should return error on decoding failure", func(t *testing.T) { + invalidVout := rawResult.Vout[0] + invalidVout.ScriptPubKey.Hex = "invalid script" + receiver, amount, err := DecodeTSSVout(invalidVout, receiverExpected, chain) + require.ErrorContains(t, err, "error decoding TSS vout") + require.Empty(t, receiver) + require.Zero(t, amount) + }) +} diff --git a/zetaclient/bitcoin/utils.go b/zetaclient/bitcoin/utils.go index 12d24a5880..fe2bb2481b 100644 --- a/zetaclient/bitcoin/utils.go +++ b/zetaclient/bitcoin/utils.go @@ -1,50 +1,13 @@ package bitcoin import ( - "encoding/hex" "encoding/json" - "fmt" "math" - "math/big" - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil" - "github.com/rs/zerolog" - "github.com/zeta-chain/zetacore/pkg/chains" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" ) -const ( - bytesPerKB = 1000 - bytesEmptyTx = 10 // an empty tx is about 10 bytes - bytesPerInput = 41 // each input is about 41 bytes - bytesPerOutput = 31 // each output is about 31 bytes - bytes1stWitness = 110 // the 1st witness incurs about 110 bytes and it may vary - bytesPerWitness = 108 // each additional witness incurs about 108 bytes and it may vary - defaultDepositorFeeRate = 20 // 20 sat/byte is the default depositor fee rate -) - -var ( - BtcOutTxBytesDepositor uint64 - BtcOutTxBytesWithdrawer uint64 - DefaultDepositorFee float64 -) - -func init() { - BtcOutTxBytesDepositor = SegWitTxSizeDepositor() // 68vB, the outtx size incurred by the depositor - BtcOutTxBytesWithdrawer = SegWitTxSizeWithdrawer() // 171vB, the outtx size incurred by the withdrawer - - // default depositor fee calculation is based on a fixed fee rate of 20 sat/byte just for simplicity. - // In reality, the fee rate on UTXO deposit is different from the fee rate when the UTXO is spent. - DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) // 0.00001360 (20 * 68vB / 100000000) -} - func PrettyPrintStruct(val interface{}) (string, error) { prettyStruct, err := json.MarshalIndent( val, @@ -57,143 +20,6 @@ func PrettyPrintStruct(val interface{}) (string, error) { return string(prettyStruct), nil } -// FeeRateToSatPerByte converts a fee rate in BTC/KB to sat/byte. -func FeeRateToSatPerByte(rate float64) *big.Int { - // #nosec G701 always in range - satPerKB := new(big.Int).SetInt64(int64(rate * btcutil.SatoshiPerBitcoin)) - return new(big.Int).Div(satPerKB, big.NewInt(bytesPerKB)) -} - -// WiredTxSize calculates the wired tx size in bytes -func WiredTxSize(numInputs uint64, numOutputs uint64) uint64 { - // Version 4 bytes + LockTime 4 bytes + Serialized varint size for the - // number of transaction inputs and outputs. - // #nosec G701 always positive - return uint64(8 + wire.VarIntSerializeSize(numInputs) + wire.VarIntSerializeSize(numOutputs)) -} - -// EstimateSegWitTxSize estimates SegWit tx size -func EstimateSegWitTxSize(numInputs uint64, numOutputs uint64) uint64 { - if numInputs == 0 { - return 0 - } - bytesWiredTx := WiredTxSize(numInputs, numOutputs) - bytesInput := numInputs * bytesPerInput - bytesOutput := numOutputs * bytesPerOutput - bytesWitness := bytes1stWitness + (numInputs-1)*bytesPerWitness - // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations - // Calculation for signed SegWit tx: blockchain.GetTransactionWeight(tx) / 4 - return bytesWiredTx + bytesInput + bytesOutput + bytesWitness/blockchain.WitnessScaleFactor -} - -// SegWitTxSizeDepositor returns SegWit tx size (68vB) incurred by the depositor -func SegWitTxSizeDepositor() uint64 { - return bytesPerInput + bytesPerWitness/blockchain.WitnessScaleFactor -} - -// SegWitTxSizeWithdrawer returns SegWit tx size (171vB) incurred by the withdrawer (1 input, 3 outputs) -func SegWitTxSizeWithdrawer() uint64 { - bytesWiredTx := WiredTxSize(1, 3) - bytesInput := uint64(1) * bytesPerInput // nonce mark - bytesOutput := uint64(3) * bytesPerOutput // 3 outputs: new nonce mark, payment, change - return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor -} - -// DepositorFee calculates the depositor fee in BTC for a given sat/byte fee rate -// Note: the depositor fee is charged in order to cover the cost of spending the deposited UTXO in the future -func DepositorFee(satPerByte int64) float64 { - return float64(satPerByte) * float64(BtcOutTxBytesDepositor) / btcutil.SatoshiPerBitcoin -} - -// CalcBlockAvgFeeRate calculates the average gas rate (in sat/vByte) for a given block -func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *chaincfg.Params) (int64, error) { - // sanity check - if len(blockVb.Tx) == 0 { - return 0, errors.New("block has no transactions") - } - if len(blockVb.Tx) == 1 { - return 0, nil // only coinbase tx, it happens - } - txCoinbase := &blockVb.Tx[0] - if blockVb.Weight < blockchain.WitnessScaleFactor { - return 0, fmt.Errorf("block weight %d too small", blockVb.Weight) - } - if blockVb.Weight < txCoinbase.Weight { - return 0, fmt.Errorf("block weight %d less than coinbase tx weight %d", blockVb.Weight, txCoinbase.Weight) - } - if blockVb.Height <= 0 || blockVb.Height > math.MaxInt32 { - return 0, fmt.Errorf("invalid block height %d", blockVb.Height) - } - - // make sure the first tx is coinbase tx - txBytes, err := hex.DecodeString(txCoinbase.Hex) - if err != nil { - return 0, fmt.Errorf("failed to decode coinbase tx %s", txCoinbase.Txid) - } - tx, err := btcutil.NewTxFromBytes(txBytes) - if err != nil { - return 0, fmt.Errorf("failed to parse coinbase tx %s", txCoinbase.Txid) - } - if !blockchain.IsCoinBaseTx(tx.MsgTx()) { - return 0, fmt.Errorf("first tx %s is not coinbase tx", txCoinbase.Txid) - } - - // calculate fees earned by the miner - btcEarned := int64(0) - for _, out := range tx.MsgTx().TxOut { - if out.Value > 0 { - btcEarned += out.Value - } - } - // #nosec G701 checked above - subsidy := blockchain.CalcBlockSubsidy(int32(blockVb.Height), netParams) - if btcEarned < subsidy { - return 0, fmt.Errorf("miner earned %d, less than subsidy %d", btcEarned, subsidy) - } - txsFees := btcEarned - subsidy - - // sum up weight of all txs (<= 4 MWU) - txsWeight := int32(0) - for i, tx := range blockVb.Tx { - // coinbase doesn't pay fees, so we exclude it - if i > 0 && tx.Weight > 0 { - txsWeight += tx.Weight - } - } - - // calculate average fee rate. - vBytes := txsWeight / blockchain.WitnessScaleFactor - return txsFees / int64(vBytes), nil -} - -// CalcDepositorFee calculates the depositor fee for a given block -func CalcDepositorFee(blockVb *btcjson.GetBlockVerboseTxResult, chainID int64, netParams *chaincfg.Params, logger zerolog.Logger) float64 { - // use dynamic fee or default - dynamicFee := true - - // use default fee for regnet - if chains.IsBitcoinRegnet(chainID) { - dynamicFee = false - } - // mainnet dynamic fee takes effect only after a planned upgrade height - if chains.IsBitcoinMainnet(chainID) && blockVb.Height < DynamicDepositorFeeHeight { - dynamicFee = false - } - if !dynamicFee { - return DefaultDepositorFee - } - - // calculate deposit fee rate - feeRate, err := CalcBlockAvgFeeRate(blockVb, netParams) - if err != nil { - feeRate = defaultDepositorFeeRate // use default fee rate if calculation fails, should not happen - logger.Error().Err(err).Msgf("cannot calculate fee rate for block %d", blockVb.Height) - } - // #nosec G701 always in range - feeRate = int64(float64(feeRate) * clientcommon.BTCOuttxGasPriceMultiplier) - return DepositorFee(feeRate) -} - func GetSatoshis(btc float64) (int64, error) { // The amount is only considered invalid if it cannot be represented // as an integer type. This may happen if f is NaN or +-Infinity. @@ -221,34 +47,3 @@ func round(f float64) int64 { // #nosec G701 always in range return int64(f + 0.5) } - -func PayToWitnessPubKeyHashScript(pubKeyHash []byte) ([]byte, error) { - return txscript.NewScriptBuilder().AddOp(txscript.OP_0).AddData(pubKeyHash).Script() -} - -// DecodeP2WPKHVout decodes receiver and amount from P2WPKH output -func DecodeP2WPKHVout(vout btcjson.Vout, chain chains.Chain) (string, int64, error) { - amount, err := GetSatoshis(vout.Value) - if err != nil { - return "", 0, errors.Wrap(err, "error getting satoshis") - } - // decode P2WPKH scriptPubKey - scriptPubKey := vout.ScriptPubKey.Hex - decodedScriptPubKey, err := hex.DecodeString(scriptPubKey) - if err != nil { - return "", 0, errors.Wrapf(err, "error decoding scriptPubKey %s", scriptPubKey) - } - if len(decodedScriptPubKey) != 22 { // P2WPKH script - return "", 0, fmt.Errorf("unsupported scriptPubKey: %s", scriptPubKey) - } - witnessVersion := decodedScriptPubKey[0] - witnessProgram := decodedScriptPubKey[2:] - if witnessVersion != 0 { - return "", 0, fmt.Errorf("unsupported witness in scriptPubKey %s", scriptPubKey) - } - recvAddress, err := chain.BTCAddressFromWitnessProgram(witnessProgram) - if err != nil { - return "", 0, errors.Wrapf(err, "error getting receiver from witness program %s", witnessProgram) - } - return recvAddress, amount, nil -} diff --git a/zetaclient/bitcoin/utils_test.go b/zetaclient/bitcoin/utils_test.go deleted file mode 100644 index 7196460cd8..0000000000 --- a/zetaclient/bitcoin/utils_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package bitcoin - -import ( - "path" - "testing" - - "github.com/btcsuite/btcd/btcjson" - "github.com/stretchr/testify/require" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/zetaclient/testutils" -) - -func TestDecodeP2WPKHVout(t *testing.T) { - // load archived outtx raw result - // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chain := chains.BtcMainnetChain() - nonce := uint64(148) - nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) - - var rawResult btcjson.TxRawResult - err := testutils.LoadObjectFromJSONFile(&rawResult, nameTx) - require.NoError(t, err) - require.Len(t, rawResult.Vout, 3) - - // decode vout 0, nonce mark 148 - receiver, amount, err := DecodeP2WPKHVout(rawResult.Vout[0], chain) - require.NoError(t, err) - require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) - require.Equal(t, chains.NonceMarkAmount(nonce), amount) - - // decode vout 1, payment 0.00012000 BTC - receiver, amount, err = DecodeP2WPKHVout(rawResult.Vout[1], chain) - require.NoError(t, err) - require.Equal(t, "bc1qpsdlklfcmlcfgm77c43x65ddtrt7n0z57hsyjp", receiver) - require.Equal(t, int64(12000), amount) - - // decode vout 2, change 0.39041489 BTC - receiver, amount, err = DecodeP2WPKHVout(rawResult.Vout[2], chain) - require.NoError(t, err) - require.Equal(t, testutils.TSSAddressBTCMainnet, receiver) - require.Equal(t, int64(39041489), amount) -} - -func TestDecodeP2WPKHVoutErrors(t *testing.T) { - // load archived outtx raw result - // https://blockstream.info/tx/030cd813443f7b70cc6d8a544d320c6d8465e4528fc0f3410b599dc0b26753a0 - chain := chains.BtcMainnetChain() - nonce := uint64(148) - nameTx := path.Join("../", testutils.TestDataPathBTC, testutils.FileNameBTCOuttx(chain.ChainId, nonce)) - - var rawResult btcjson.TxRawResult - err := testutils.LoadObjectFromJSONFile(&rawResult, nameTx) - require.NoError(t, err) - - t.Run("should return error on invalid amount", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - invalidVout.Value = -0.5 // negative amount, should not happen - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "error getting satoshis") - }) - t.Run("should return error on invalid script", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - invalidVout.ScriptPubKey.Hex = "invalid script" - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "error decoding scriptPubKey") - }) - t.Run("should return error on unsupported script", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - // can use any invalid script, https://blockstream.info/tx/e95c6ff206103716129c8e3aa8def1427782af3490589d1ea35ccf0122adbc25 (P2SH) - invalidVout.ScriptPubKey.Hex = "a91413b2388e6532653a4b369b7e4ed130f7b81626cc87" - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "unsupported scriptPubKey") - }) - t.Run("should return error on unsupported witness version", func(t *testing.T) { - invalidVout := rawResult.Vout[0] - // use a fake witness version 1, even if version 0 is the only witness version defined in BIP141 - invalidVout.ScriptPubKey.Hex = "01140c1bfb7d38dff0946fdec5626d51ad58d7e9bc54" - _, _, err := DecodeP2WPKHVout(invalidVout, chain) - require.Error(t, err) - require.ErrorContains(t, err, "unsupported witness in scriptPubKey") - }) -} diff --git a/zetaclient/compliance/compliance_test.go b/zetaclient/compliance/compliance_test.go index 638e6c8ba0..001e5d9e0d 100644 --- a/zetaclient/compliance/compliance_test.go +++ b/zetaclient/compliance/compliance_test.go @@ -14,8 +14,7 @@ import ( func TestCctxRestricted(t *testing.T) { // load archived cctx var cctx crosschaintypes.CrossChainTx - err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_1_6270.json")) - require.NoError(t, err) + testutils.LoadObjectFromJSONFile(t, &cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_1_6270.json")) // create config cfg := config.Config{ diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index 869d9111ea..f211f1030d 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -54,6 +54,10 @@ var evmChainsConfigs = map[int64]EVMConfig{ Chain: chains.GoerliChain(), Endpoint: "", }, + chains.SepoliaChain().ChainId: { + Chain: chains.SepoliaChain(), + Endpoint: "", + }, chains.BscTestnetChain().ChainId: { Chain: chains.BscTestnetChain(), Endpoint: "", diff --git a/zetaclient/evm/evm_client.go b/zetaclient/evm/evm_client.go index 84fbd19e83..f94536be25 100644 --- a/zetaclient/evm/evm_client.go +++ b/zetaclient/evm/evm_client.go @@ -57,11 +57,11 @@ type OutTx struct { Nonce int64 } type Log struct { - ChainLogger zerolog.Logger // Parent logger - ExternalChainWatcher zerolog.Logger // Observes external Chains for incoming trasnactions - WatchGasPrice zerolog.Logger // Observes external Chains for Gas prices and posts to core - ObserveOutTx zerolog.Logger // Observes external Chains for outgoing transactions - Compliance zerolog.Logger // Compliance logger + Chain zerolog.Logger // The parent logger for the chain + InTx zerolog.Logger // Logger for incoming trasnactions + OutTx zerolog.Logger // Logger for outgoing transactions + GasPrice zerolog.Logger // Logger for gas prices + Compliance zerolog.Logger // Logger for compliance checks } var _ interfaces.ChainClient = &ChainClient{} @@ -108,11 +108,11 @@ func NewEVMChainClient( } chainLogger := loggers.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() ob.logger = Log{ - ChainLogger: chainLogger, - ExternalChainWatcher: chainLogger.With().Str("module", "ExternalChainWatcher").Logger(), - WatchGasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - ObserveOutTx: chainLogger.With().Str("module", "ObserveOutTx").Logger(), - Compliance: loggers.Compliance, + Chain: chainLogger, + InTx: chainLogger.With().Str("module", "WatchInTx").Logger(), + OutTx: chainLogger.With().Str("module", "WatchOutTx").Logger(), + GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), + Compliance: loggers.Compliance, } ob.coreContext = appContext.ZetaCoreContext() chainParams, found := ob.coreContext.GetEVMChainParams(evmCfg.Chain.ChainId) @@ -130,10 +130,10 @@ func NewEVMChainClient( ob.outTXConfirmedReceipts = make(map[string]*ethtypes.Receipt) ob.outTXConfirmedTransactions = make(map[string]*ethtypes.Transaction) - ob.logger.ChainLogger.Info().Msgf("Chain %s endpoint %s", ob.chain.ChainName.String(), evmCfg.Endpoint) + ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.ChainName.String(), evmCfg.Endpoint) client, err := ethclient.Dial(evmCfg.Endpoint) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("eth Client Dial") + ob.logger.Chain.Error().Err(err).Msg("eth Client Dial") return nil, err } ob.evmClient = client @@ -142,12 +142,12 @@ func NewEVMChainClient( // create block header and block caches ob.blockCache, err = lru.New(1000) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("failed to create block cache") + ob.logger.Chain.Error().Err(err).Msg("failed to create block cache") return nil, err } ob.headerCache, err = lru.New(1000) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("failed to create header cache") + ob.logger.Chain.Error().Err(err).Msg("failed to create header cache") return nil, err } @@ -156,7 +156,7 @@ func NewEVMChainClient( return nil, err } - ob.logger.ChainLogger.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) + ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) return &ob, nil } @@ -169,10 +169,10 @@ func (ob *ChainClient) WithLogger(logger zerolog.Logger) { ob.Mu.Lock() defer ob.Mu.Unlock() ob.logger = Log{ - ChainLogger: logger, - ExternalChainWatcher: logger.With().Str("module", "ExternalChainWatcher").Logger(), - WatchGasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), - ObserveOutTx: logger.With().Str("module", "ObserveOutTx").Logger(), + Chain: logger, + InTx: logger.With().Str("module", "WatchInTx").Logger(), + OutTx: logger.With().Str("module", "WatchOutTx").Logger(), + GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), } } @@ -252,43 +252,48 @@ func FetchERC20CustodyContract(addr ethcommon.Address, client interfaces.EVMRPCC return erc20custody.NewERC20Custody(addr, client) } +// Start all observation routines for the evm chain func (ob *ChainClient) Start() { - go ob.ExternalChainWatcherForNewInboundTrackerSuggestions() - go ob.ExternalChainWatcher() // Observes external Chains for incoming trasnactions - go ob.WatchGasPrice() // Observes external Chains for Gas prices and posts to core - go ob.observeOutTx() // Populates receipts and confirmed outbound transactions - go ob.ExternalChainRPCStatus() + go ob.WatchInTx() // watch evm chain for incoming txs and post votes to zetacore + go ob.WatchOutTx() // watch evm chain for outgoing txs status + go ob.WatchGasPrice() // watch evm chain for gas prices and post to zetacore + go ob.WatchIntxTracker() // watch zetacore for intx trackers + go ob.WatchRPCStatus() // watch the RPC status of the evm chain } -func (ob *ChainClient) ExternalChainRPCStatus() { - ob.logger.ChainLogger.Info().Msgf("Starting RPC status check for chain %s", ob.chain.String()) +// WatchRPCStatus watches the RPC status of the evm chain +func (ob *ChainClient) WatchRPCStatus() { + ob.logger.Chain.Info().Msgf("Starting RPC status check for chain %s", ob.chain.String()) ticker := time.NewTicker(60 * time.Second) for { select { case <-ticker.C: + if !ob.GetChainParams().IsSupported { + continue + } bn, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } gasPrice, err := ob.evmClient.SuggestGasPrice(context.Background()) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(bn)) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } // #nosec G701 always in range blockTime := time.Unix(int64(header.Time), 0).UTC() elapsedSeconds := time.Since(blockTime).Seconds() if elapsedSeconds > 100 { - ob.logger.ChainLogger.Warn().Msgf("RPC Status Check warning: RPC stale or chain stuck (check explorer)? Latest block %d timestamp is %.0fs ago", bn, elapsedSeconds) + ob.logger.Chain.Warn().Msgf("RPC Status Check warning: RPC stale or chain stuck (check explorer)? Latest block %d timestamp is %.0fs ago", bn, elapsedSeconds) continue } - ob.logger.ChainLogger.Info().Msgf("[OK] RPC status: latest block num %d, timestamp %s ( %.0fs ago), suggested gas price %d", header.Number, blockTime.String(), elapsedSeconds, gasPrice.Uint64()) + ob.logger.Chain.Info().Msgf("[OK] RPC status: latest block num %d, timestamp %s ( %.0fs ago), suggested gas price %d", header.Number, blockTime.String(), elapsedSeconds, gasPrice.Uint64()) case <-ob.stop: return } @@ -296,20 +301,20 @@ func (ob *ChainClient) ExternalChainRPCStatus() { } func (ob *ChainClient) Stop() { - ob.logger.ChainLogger.Info().Msgf("ob %s is stopping", ob.chain.String()) + ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) close(ob.stop) // this notifies all goroutines to stop - ob.logger.ChainLogger.Info().Msg("closing ob.db") + ob.logger.Chain.Info().Msg("closing ob.db") dbInst, err := ob.db.DB() if err != nil { - ob.logger.ChainLogger.Info().Msg("error getting database instance") + ob.logger.Chain.Info().Msg("error getting database instance") } err = dbInst.Close() if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("error closing database") + ob.logger.Chain.Error().Err(err).Msg("error closing database") } - ob.logger.ChainLogger.Info().Msgf("%s observer stopped", ob.chain.String()) + ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) } // returns: isIncluded, isConfirmed, Error @@ -606,19 +611,18 @@ func (ob *ChainClient) IsSendOutTxProcessed(cctx *crosschaintypes.CrossChainTx, return false, false, nil } -// FIXME: there's a chance that a txhash in OutTxChan may not deliver when Stop() is called -// observeOutTx periodically checks all the txhash in potential outbound txs -func (ob *ChainClient) observeOutTx() { +// WatchOutTx watches evm chain for outgoing txs status +func (ob *ChainClient) WatchOutTx() { // read env variables if set timeoutNonce, err := strconv.Atoi(os.Getenv("OS_TIMEOUT_NONCE")) if err != nil || timeoutNonce <= 0 { timeoutNonce = 100 * 3 // process up to 100 hashes } - ob.logger.ObserveOutTx.Info().Msgf("observeOutTx: using timeoutNonce %d seconds", timeoutNonce) + ob.logger.OutTx.Info().Msgf("WatchOutTx: using timeoutNonce %d seconds", timeoutNonce) - ticker, err := clienttypes.NewDynamicTicker(fmt.Sprintf("EVM_observeOutTx_%d", ob.chain.ChainId), ob.GetChainParams().OutTxTicker) + ticker, err := clienttypes.NewDynamicTicker(fmt.Sprintf("EVM_WatchOutTx_%d", ob.chain.ChainId), ob.GetChainParams().OutTxTicker) if err != nil { - ob.logger.ObserveOutTx.Error().Err(err).Msg("failed to create ticker") + ob.logger.OutTx.Error().Err(err).Msg("error creating ticker") return } @@ -626,6 +630,9 @@ func (ob *ChainClient) observeOutTx() { for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } trackers, err := ob.zetaClient.GetAllOutTxTrackerByChain(ob.chain.ChainId, interfaces.Ascending) if err != nil { continue @@ -644,17 +651,17 @@ func (ob *ChainClient) observeOutTx() { for _, txHash := range tracker.HashList { select { case <-outTimeout: - ob.logger.ObserveOutTx.Warn().Msgf("observeOutTx: timeout on chain %d nonce %d", ob.chain.ChainId, nonceInt) + ob.logger.OutTx.Warn().Msgf("WatchOutTx: timeout on chain %d nonce %d", ob.chain.ChainId, nonceInt) break TRACKERLOOP default: if recpt, tx, ok := ob.checkConfirmedTx(txHash.TxHash, nonceInt); ok { txCount++ receipt = recpt transaction = tx - ob.logger.ObserveOutTx.Info().Msgf("observeOutTx: confirmed outTx %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, nonceInt) + ob.logger.OutTx.Info().Msgf("WatchOutTx: confirmed outTx %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, nonceInt) if txCount > 1 { - ob.logger.ObserveOutTx.Error().Msgf( - "observeOutTx: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.chain.ChainId, nonceInt, receipt, transaction) + ob.logger.OutTx.Error().Msgf( + "WatchOutTx: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.chain.ChainId, nonceInt, receipt, transaction) } } } @@ -662,12 +669,12 @@ func (ob *ChainClient) observeOutTx() { if txCount == 1 { // should be only one txHash confirmed for each nonce. ob.SetTxNReceipt(nonceInt, receipt, transaction) } else if txCount > 1 { // should not happen. We can't tell which txHash is true. It might happen (e.g. glitchy/hacked endpoint) - ob.logger.ObserveOutTx.Error().Msgf("observeOutTx: confirmed multiple (%d) outTx for chain %d nonce %d", txCount, ob.chain.ChainId, nonceInt) + ob.logger.OutTx.Error().Msgf("WatchOutTx: confirmed multiple (%d) outTx for chain %d nonce %d", txCount, ob.chain.ChainId, nonceInt) } } - ticker.UpdateInterval(ob.GetChainParams().OutTxTicker, ob.logger.ObserveOutTx) + ticker.UpdateInterval(ob.GetChainParams().OutTxTicker, ob.logger.OutTx) case <-ob.stop: - ob.logger.ObserveOutTx.Info().Msg("observeOutTx: stopped") + ob.logger.OutTx.Info().Msg("WatchOutTx: stopped") return } } @@ -833,26 +840,31 @@ func (ob *ChainClient) GetLastBlockHeight() uint64 { return height } -func (ob *ChainClient) ExternalChainWatcher() { - ticker, err := clienttypes.NewDynamicTicker(fmt.Sprintf("EVM_ExternalChainWatcher_%d", ob.chain.ChainId), ob.GetChainParams().InTxTicker) +// WatchInTx watches evm chain for incoming txs and post votes to zetacore +func (ob *ChainClient) WatchInTx() { + ticker, err := clienttypes.NewDynamicTicker(fmt.Sprintf("EVM_WatchInTx_%d", ob.chain.ChainId), ob.GetChainParams().InTxTicker) if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msg("NewDynamicTicker error") + ob.logger.InTx.Error().Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.ExternalChainWatcher.Info().Msg("ExternalChainWatcher started") - sampledLogger := ob.logger.ExternalChainWatcher.Sample(&zerolog.BasicSampler{N: 10}) + ob.logger.InTx.Info().Msgf("WatchInTx started for chain %d", ob.chain.ChainId) + sampledLogger := ob.logger.InTx.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + sampledLogger.Info().Msgf("WatchInTx: chain %d is not supported", ob.chain.ChainId) + continue + } err := ob.observeInTX(sampledLogger) if err != nil { - ob.logger.ExternalChainWatcher.Err(err).Msg("observeInTX error") + ob.logger.InTx.Err(err).Msg("WatchInTx: observeInTX error") } - ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.ExternalChainWatcher) + ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.InTx) case <-ob.stop: - ob.logger.ExternalChainWatcher.Info().Msg("ExternalChainWatcher stopped") + ob.logger.InTx.Info().Msgf("WatchInTx stopped for chain %d", ob.chain.ChainId) return } } @@ -883,12 +895,12 @@ func (ob *ChainClient) postBlockHeader(tip uint64) error { header, err := ob.GetBlockHeaderCached(bn) if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) + ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) return err } headerRLP, err := rlp.EncodeToBytes(header) if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) + ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) return err } @@ -899,7 +911,7 @@ func (ob *ChainClient) postBlockHeader(tip uint64) error { proofs.NewEthereumHeader(headerRLP), ) if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) + ob.logger.InTx.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) return err } return nil @@ -966,7 +978,7 @@ func (ob *ChainClient) observeInTX(sampledLogger zerolog.Logger) error { ob.chain.ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) ob.SetLastBlockHeightScanned(lastScannedLowest) if err := ob.db.Save(clienttypes.ToLastBlockSQLType(lastScannedLowest)).Error; err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("observeInTX: error writing lastScannedLowest %d to db", lastScannedLowest) + ob.logger.InTx.Error().Err(err).Msgf("observeInTX: error writing lastScannedLowest %d to db", lastScannedLowest) } } return nil @@ -978,7 +990,7 @@ func (ob *ChainClient) ObserveZetaSent(startBlock, toBlock uint64) uint64 { // filter ZetaSent logs addrConnector, connector, err := ob.GetConnectorContract() if err != nil { - ob.logger.ChainLogger.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") + ob.logger.Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") return startBlock - 1 // lastScanned } iter, err := connector.FilterZetaSent(&bind.FilterOpts{ @@ -987,7 +999,7 @@ func (ob *ChainClient) ObserveZetaSent(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}, []*big.Int{}) if err != nil { - ob.logger.ChainLogger.Warn().Err(err).Msgf( + ob.logger.Chain.Warn().Err(err).Msgf( "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) return startBlock - 1 // lastScanned } @@ -1001,7 +1013,7 @@ func (ob *ChainClient) ObserveZetaSent(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.logger.ExternalChainWatcher.Warn().Err(err).Msgf("ObserveZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d", + ob.logger.InTx.Warn().Err(err).Msgf("ObserveZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d", iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { @@ -1027,7 +1039,7 @@ func (ob *ChainClient) ObserveZetaSent(startBlock, toBlock uint64) uint64 { } // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.ExternalChainWatcher.Warn().Msgf("ObserveZetaSent: multiple remote call events detected in tx %s", event.Raw.TxHash) + ob.logger.InTx.Warn().Msgf("ObserveZetaSent: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } guard[event.Raw.TxHash.Hex()] = true @@ -1050,7 +1062,7 @@ func (ob *ChainClient) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 // filter ERC20CustodyDeposited logs addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() if err != nil { - ob.logger.ExternalChainWatcher.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") + ob.logger.InTx.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") return startBlock - 1 // lastScanned } iter, err := erc20custodyContract.FilterDeposited(&bind.FilterOpts{ @@ -1059,7 +1071,7 @@ func (ob *ChainClient) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 Context: context.TODO(), }, []ethcommon.Address{}) if err != nil { - ob.logger.ExternalChainWatcher.Warn().Err(err).Msgf( + ob.logger.InTx.Warn().Err(err).Msgf( "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) return startBlock - 1 // lastScanned } @@ -1073,7 +1085,7 @@ func (ob *ChainClient) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 events = append(events, iter.Event) continue } - ob.logger.ExternalChainWatcher.Warn().Err(err).Msgf("ObserveERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d", + ob.logger.InTx.Warn().Err(err).Msgf("ObserveERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d", iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { @@ -1099,7 +1111,7 @@ func (ob *ChainClient) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 } tx, _, err := ob.TransactionByHash(event.Raw.TxHash.Hex()) if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf( + ob.logger.InTx.Error().Err(err).Msgf( "ObserveERC20Deposited: error getting transaction for intx %s chain %d", event.Raw.TxHash, ob.chain.ChainId) return beingScanned - 1 // we have to re-scan from this block next time } @@ -1107,7 +1119,7 @@ func (ob *ChainClient) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.ExternalChainWatcher.Warn().Msgf("ObserveERC20Deposited: multiple remote call events detected in tx %s", event.Raw.TxHash) + ob.logger.InTx.Warn().Msgf("ObserveERC20Deposited: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } guard[event.Raw.TxHash.Hex()] = true @@ -1127,10 +1139,6 @@ func (ob *ChainClient) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 // ObserverTSSReceive queries the incoming gas asset to TSS address and posts to zetabridge // returns the last block successfully scanned func (ob *ChainClient) ObserverTSSReceive(startBlock, toBlock uint64, flags observertypes.CrosschainFlags) uint64 { - if !ob.GetChainParams().IsSupported { - return startBlock - 1 // lastScanned - } - // query incoming gas asset for bn := startBlock; bn <= toBlock; bn++ { // post new block header (if any) to zetabridge and ignore error @@ -1140,14 +1148,14 @@ func (ob *ChainClient) ObserverTSSReceive(startBlock, toBlock uint64, flags obse chains.IsHeaderSupportedEvmChain(ob.chain.ChainId) { // post block header for supported chains err := ob.postBlockHeader(toBlock) if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msg("error posting block header") + ob.logger.InTx.Error().Err(err).Msg("error posting block header") } } // observe TSS received gas token in block 'bn' err := ob.ObserveTSSReceiveInBlock(bn) if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.chain.ChainId) + ob.logger.InTx.Error().Err(err).Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.chain.ChainId) return bn - 1 // we have to re-scan from this block next time } } @@ -1155,41 +1163,45 @@ func (ob *ChainClient) ObserverTSSReceive(startBlock, toBlock uint64, flags obse return toBlock } +// WatchGasPrice watches evm chain for gas prices and post to zetacore func (ob *ChainClient) WatchGasPrice() { - ob.logger.WatchGasPrice.Info().Msg("WatchGasPrice starting...") + ob.logger.GasPrice.Info().Msg("WatchGasPrice starting...") err := ob.PostGasPrice() if err != nil { height, err := ob.zetaClient.GetBlockHeight() if err != nil { - ob.logger.WatchGasPrice.Error().Err(err).Msg("GetBlockHeight error") + ob.logger.GasPrice.Error().Err(err).Msg("GetBlockHeight error") } else { - ob.logger.WatchGasPrice.Error().Err(err).Msgf("PostGasPrice error at zeta block : %d ", height) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error at zeta block : %d ", height) } } ticker, err := clienttypes.NewDynamicTicker(fmt.Sprintf("EVM_WatchGasPrice_%d", ob.chain.ChainId), ob.GetChainParams().GasPriceTicker) if err != nil { - ob.logger.WatchGasPrice.Error().Err(err).Msg("NewDynamicTicker error") + ob.logger.GasPrice.Error().Err(err).Msg("NewDynamicTicker error") return } - ob.logger.WatchGasPrice.Info().Msgf("WatchGasPrice started with interval %d", ob.GetChainParams().GasPriceTicker) + ob.logger.GasPrice.Info().Msgf("WatchGasPrice started with interval %d", ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } err = ob.PostGasPrice() if err != nil { height, err := ob.zetaClient.GetBlockHeight() if err != nil { - ob.logger.WatchGasPrice.Error().Err(err).Msg("GetBlockHeight error") + ob.logger.GasPrice.Error().Err(err).Msg("GetBlockHeight error") } else { - ob.logger.WatchGasPrice.Error().Err(err).Msgf("PostGasPrice error at zeta block : %d ", height) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error at zeta block : %d ", height) } } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.WatchGasPrice) + ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) case <-ob.stop: - ob.logger.WatchGasPrice.Info().Msg("WatchGasPrice stopped") + ob.logger.GasPrice.Info().Msg("WatchGasPrice stopped") return } } @@ -1200,12 +1212,12 @@ func (ob *ChainClient) PostGasPrice() error { // GAS PRICE gasPrice, err := ob.evmClient.SuggestGasPrice(context.TODO()) if err != nil { - ob.logger.WatchGasPrice.Err(err).Msg("Err SuggestGasPrice:") + ob.logger.GasPrice.Err(err).Msg("Err SuggestGasPrice:") return err } blockNum, err := ob.evmClient.BlockNumber(context.TODO()) if err != nil { - ob.logger.WatchGasPrice.Err(err).Msg("Err Fetching Most recent Block : ") + ob.logger.GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") return err } @@ -1214,7 +1226,7 @@ func (ob *ChainClient) PostGasPrice() error { zetaHash, err := ob.zetaClient.PostGasPrice(ob.chain, gasPrice.Uint64(), supply, blockNum) if err != nil { - ob.logger.WatchGasPrice.Err(err).Msg("PostGasPrice to zetabridge failed") + ob.logger.GasPrice.Err(err).Msg("PostGasPrice to zetabridge failed") return err } _ = zetaHash @@ -1223,7 +1235,7 @@ func (ob *ChainClient) PostGasPrice() error { } func (ob *ChainClient) BuildLastBlock() error { - logger := ob.logger.ChainLogger.With().Str("module", "BuildBlockIndex").Logger() + logger := ob.logger.Chain.With().Str("module", "BuildBlockIndex").Logger() envvar := ob.chain.ChainName.String() + "_SCAN_FROM" scanFromBlock := os.Getenv(envvar) if scanFromBlock != "" { @@ -1264,7 +1276,7 @@ func (ob *ChainClient) BuildReceiptsMap() error { logger := ob.logger var receipts []clienttypes.ReceiptSQLType if err := ob.db.Find(&receipts).Error; err != nil { - logger.ChainLogger.Error().Err(err).Msg("error iterating over db") + logger.Chain.Error().Err(err).Msg("error iterating over db") return err } for _, receipt := range receipts { @@ -1297,7 +1309,7 @@ func (ob *ChainClient) LoadDB(dbPath string, chain chains.Chain) error { &clienttypes.TransactionSQLType{}, &clienttypes.LastBlockSQLType{}) if err != nil { - ob.logger.ChainLogger.Error().Err(err).Msg("error migrating db") + ob.logger.Chain.Error().Err(err).Msg("error migrating db") return err } diff --git a/zetaclient/evm/evm_signer.go b/zetaclient/evm/evm_signer.go index 8301a5d267..e077d19fe2 100644 --- a/zetaclient/evm/evm_signer.go +++ b/zetaclient/evm/evm_signer.go @@ -141,7 +141,11 @@ func (signer *Signer) Sign( height uint64, ) (*ethtypes.Transaction, []byte, []byte, error) { log.Debug().Msgf("TSS SIGNER: %s", signer.tssSigner.Pubkey()) + + // TODO: use EIP-1559 transaction type + // https://github.com/zeta-chain/node/issues/1952 tx := ethtypes.NewTransaction(nonce, to, big.NewInt(0), gasLimit, gasPrice, data) + hashBytes := signer.ethSigner.Hash(tx).Bytes() sig, err := signer.tssSigner.Sign(hashBytes, height, nonce, signer.chain, "") @@ -249,7 +253,10 @@ func (signer *Signer) SignRevertTx(txData *OutBoundTransactionData) (*ethtypes.T // SignCancelTx signs a transaction from TSS address to itself with a zero amount in order to increment the nonce func (signer *Signer) SignCancelTx(nonce uint64, gasPrice *big.Int, height uint64) (*ethtypes.Transaction, error) { + // TODO: use EIP-1559 transaction type + // https://github.com/zeta-chain/node/issues/1952 tx := ethtypes.NewTransaction(nonce, signer.tssSigner.EVMAddress(), big.NewInt(0), 21000, gasPrice, nil) + hashBytes := signer.ethSigner.Hash(tx).Bytes() sig, err := signer.tssSigner.Sign(hashBytes, height, nonce, signer.chain, "") if err != nil { @@ -271,7 +278,10 @@ func (signer *Signer) SignCancelTx(nonce uint64, gasPrice *big.Int, height uint6 // SignWithdrawTx signs a withdrawal transaction sent from the TSS address to the destination func (signer *Signer) SignWithdrawTx(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { + // TODO: use EIP-1559 transaction type + // https://github.com/zeta-chain/node/issues/1952 tx := ethtypes.NewTransaction(txData.nonce, txData.to, txData.amount, 21000, txData.gasPrice, nil) + hashBytes := signer.ethSigner.Hash(tx).Bytes() sig, err := signer.tssSigner.Sign(hashBytes, txData.height, txData.nonce, signer.chain, "") if err != nil { @@ -506,6 +516,7 @@ func (signer *Signer) reportToOutTxTracker(zetaBridge interfaces.ZetaCoreBridger for { // give up after 10 minutes of monitoring time.Sleep(10 * time.Second) + if time.Since(tStart) > OutTxInclusionTimeout { // if tx is still pending after timeout, report to outTxTracker anyway as we cannot monitor forever if isPending { diff --git a/zetaclient/evm/evm_signer_test.go b/zetaclient/evm/evm_signer_test.go index b78a3d1c26..eace105ff2 100644 --- a/zetaclient/evm/evm_signer_test.go +++ b/zetaclient/evm/evm_signer_test.go @@ -11,7 +11,6 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/constant" "github.com/zeta-chain/zetacore/testutil/sample" - "github.com/zeta-chain/zetacore/x/crosschain/types" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" "github.com/zeta-chain/zetacore/zetaclient/common" @@ -67,16 +66,16 @@ func getNewOutTxProcessor() *outtxprocessor.Processor { return outtxprocessor.NewOutTxProcessorManager(logger) } -func getCCTX() (*types.CrossChainTx, error) { +func getCCTX(t *testing.T) *crosschaintypes.CrossChainTx { var cctx crosschaintypes.CrossChainTx - err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) - return &cctx, err + testutils.LoadObjectFromJSONFile(t, &cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) + return &cctx } -func getInvalidCCTX() (*types.CrossChainTx, error) { +func getInvalidCCTX(t *testing.T) *crosschaintypes.CrossChainTx { var cctx crosschaintypes.CrossChainTx - err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) - return &cctx, err + testutils.LoadObjectFromJSONFile(t, &cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) + return &cctx } func TestSigner_SetGetConnectorAddress(t *testing.T) { @@ -106,8 +105,7 @@ func TestSigner_SetGetERC20CustodyAddress(t *testing.T) { func TestSigner_TryProcessOutTx(t *testing.T) { evmSigner, err := getNewEvmSigner() require.NoError(t, err) - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) processorManager := getNewOutTxProcessor() mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) @@ -126,8 +124,7 @@ func TestSigner_SignOutboundTx(t *testing.T) { // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -156,8 +153,7 @@ func TestSigner_SignRevertTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -186,8 +182,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -216,8 +211,7 @@ func TestSigner_SignCommandTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -264,8 +258,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -294,8 +287,7 @@ func TestSigner_BroadcastOutTx(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) @@ -325,8 +317,7 @@ func TestSigner_getEVMRPC(t *testing.T) { } func TestSigner_SignerErrorMsg(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) msg := SignerErrorMsg(cctx) require.Contains(t, msg, "nonce 68270 chain 56") @@ -338,8 +329,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { require.NoError(t, err) // Setup txData struct - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) mockChainClient, err := getNewEvmChainClient() require.NoError(t, err) txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) diff --git a/zetaclient/evm/inbounds.go b/zetaclient/evm/inbounds.go index 2daa4fc0c1..d6f6fcb223 100644 --- a/zetaclient/evm/inbounds.go +++ b/zetaclient/evm/inbounds.go @@ -26,30 +26,33 @@ import ( "golang.org/x/net/context" ) -// ExternalChainWatcherForNewInboundTrackerSuggestions At each tick, gets a list of Inbound tracker suggestions from zeta-core and tries to check if the in-tx was confirmed. +// WatchIntxTracker gets a list of Inbound tracker suggestions from zeta-core at each tick and tries to check if the in-tx was confirmed. // If it was, it tries to broadcast the confirmation vote. If this zeta client has previously broadcast the vote, the tx would be rejected -func (ob *ChainClient) ExternalChainWatcherForNewInboundTrackerSuggestions() { +func (ob *ChainClient) WatchIntxTracker() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_ExternalChainWatcher_InboundTrackerSuggestions_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchIntxTracker_%d", ob.chain.ChainId), ob.GetChainParams().InTxTicker, ) if err != nil { - ob.logger.ExternalChainWatcher.Err(err).Msg("error creating ticker") + ob.logger.InTx.Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.ExternalChainWatcher.Info().Msg("ExternalChainWatcher for inboundTrackerSuggestions started") + ob.logger.InTx.Info().Msgf("Intx tracker watcher started for chain %d", ob.chain.ChainId) for { select { case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } err := ob.ObserveIntxTrackers() if err != nil { - ob.logger.ExternalChainWatcher.Err(err).Msg("ObserveTrackerSuggestions error") + ob.logger.InTx.Err(err).Msg("ObserveTrackerSuggestions error") } - ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.ExternalChainWatcher) + ticker.UpdateInterval(ob.GetChainParams().InTxTicker, ob.logger.InTx) case <-ob.stop: - ob.logger.ExternalChainWatcher.Info().Msg("ExternalChainWatcher for inboundTrackerSuggestions stopped") + ob.logger.InTx.Info().Msg("ExternalChainWatcher for inboundTrackerSuggestions stopped") return } } @@ -71,7 +74,7 @@ func (ob *ChainClient) ObserveIntxTrackers() error { if err != nil { return errors.Wrapf(err, "error getting receipt for intx %s chain %d", tracker.TxHash, ob.chain.ChainId) } - ob.logger.ExternalChainWatcher.Info().Msgf("checking tracker for intx %s chain %d", tracker.TxHash, ob.chain.ChainId) + ob.logger.InTx.Info().Msgf("checking tracker for intx %s chain %d", tracker.TxHash, ob.chain.ChainId) // check and vote on inbound tx switch tracker.CoinType { @@ -113,7 +116,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenZeta(tx *ethrpc.Transaction, rece if err == nil { msg = ob.BuildInboundVoteMsgForZetaSentEvent(event) } else { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("CheckEvmTxLog error on intx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.logger.InTx.Error().Err(err).Msgf("CheckEvmTxLog error on intx %s chain %d", tx.Hash, ob.chain.ChainId) return "", err } break // only one event is allowed per tx @@ -121,7 +124,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenZeta(tx *ethrpc.Transaction, rece } if msg == nil { // no event, restricted tx, etc. - ob.logger.ExternalChainWatcher.Info().Msgf("no ZetaSent event found for intx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.logger.InTx.Info().Msgf("no ZetaSent event found for intx %s chain %d", tx.Hash, ob.chain.ChainId) return "", nil } if vote { @@ -154,7 +157,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenERC20(tx *ethrpc.Transaction, rec if err == nil { msg = ob.BuildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) } else { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("CheckEvmTxLog error on intx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.logger.InTx.Error().Err(err).Msgf("CheckEvmTxLog error on intx %s chain %d", tx.Hash, ob.chain.ChainId) return "", err } break // only one event is allowed per tx @@ -162,7 +165,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenERC20(tx *ethrpc.Transaction, rec } if msg == nil { // no event, donation, restricted tx, etc. - ob.logger.ExternalChainWatcher.Info().Msgf("no Deposited event found for intx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.logger.InTx.Info().Msgf("no Deposited event found for intx %s chain %d", tx.Hash, ob.chain.ChainId) return "", nil } if vote { @@ -190,7 +193,7 @@ func (ob *ChainClient) CheckAndVoteInboundTokenGas(tx *ethrpc.Transaction, recei msg := ob.BuildInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) if msg == nil { // donation, restricted tx, etc. - ob.logger.ExternalChainWatcher.Info().Msgf("no vote message built for intx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.logger.InTx.Info().Msgf("no vote message built for intx %s chain %d", tx.Hash, ob.chain.ChainId) return "", nil } if vote { @@ -205,12 +208,12 @@ func (ob *ChainClient) PostVoteInbound(msg *types.MsgVoteOnObservedInboundTx, co chainID := ob.chain.ChainId zetaHash, ballot, err := ob.zetaClient.PostVoteInbound(zetabridge.PostVoteInboundGasLimit, retryGasLimit, msg) if err != nil { - ob.logger.ExternalChainWatcher.Err(err).Msgf("intx detected: error posting vote for chain %d token %s intx %s", chainID, coinType, txHash) + ob.logger.InTx.Err(err).Msgf("intx detected: error posting vote for chain %d token %s intx %s", chainID, coinType, txHash) return "", err } else if zetaHash != "" { - ob.logger.ExternalChainWatcher.Info().Msgf("intx detected: chain %d token %s intx %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) + ob.logger.InTx.Info().Msgf("intx detected: chain %d token %s intx %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) } else { - ob.logger.ExternalChainWatcher.Info().Msgf("intx detected: chain %d token %s intx %s already voted on ballot %s", chainID, coinType, txHash, ballot) + ob.logger.InTx.Info().Msgf("intx detected: chain %d token %s intx %s already voted on ballot %s", chainID, coinType, txHash, ballot) } return ballot, err } @@ -230,18 +233,18 @@ func (ob *ChainClient) BuildInboundVoteMsgForDepositedEvent(event *erc20custody. maybeReceiver = parsedAddress.Hex() } if config.ContainRestrictedAddress(sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), maybeReceiver) { - compliance.PrintComplianceLog(ob.logger.ExternalChainWatcher, ob.logger.Compliance, + compliance.PrintComplianceLog(ob.logger.InTx, ob.logger.Compliance, false, ob.chain.ChainId, event.Raw.TxHash.Hex(), sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), "ERC20") return nil } // donation check if bytes.Equal(event.Message, []byte(constant.DonationMessage)) { - ob.logger.ExternalChainWatcher.Info().Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.chain.ChainId) + ob.logger.InTx.Info().Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.chain.ChainId) return nil } message := hex.EncodeToString(event.Message) - ob.logger.ExternalChainWatcher.Info().Msgf("ERC20CustodyDeposited inTx detected on chain %d tx %s block %d from %s value %s message %s", + ob.logger.InTx.Info().Msgf("ERC20CustodyDeposited inTx detected on chain %d tx %s block %d from %s value %s message %s", ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) return zetabridge.GetInBoundVoteMessage( @@ -266,7 +269,7 @@ func (ob *ChainClient) BuildInboundVoteMsgForDepositedEvent(event *erc20custody. func (ob *ChainClient) BuildInboundVoteMsgForZetaSentEvent(event *zetaconnector.ZetaConnectorNonEthZetaSent) *types.MsgVoteOnObservedInboundTx { destChain := chains.GetChainFromChainID(event.DestinationChainId.Int64()) if destChain == nil { - ob.logger.ExternalChainWatcher.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) + ob.logger.InTx.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) return nil } destAddr := clienttypes.BytesToEthHex(event.DestinationAddress) @@ -274,7 +277,7 @@ func (ob *ChainClient) BuildInboundVoteMsgForZetaSentEvent(event *zetaconnector. // compliance check sender := event.ZetaTxSenderAddress.Hex() if config.ContainRestrictedAddress(sender, destAddr, event.SourceTxOriginAddress.Hex()) { - compliance.PrintComplianceLog(ob.logger.ExternalChainWatcher, ob.logger.Compliance, + compliance.PrintComplianceLog(ob.logger.InTx, ob.logger.Compliance, false, ob.chain.ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") return nil } @@ -282,17 +285,17 @@ func (ob *ChainClient) BuildInboundVoteMsgForZetaSentEvent(event *zetaconnector. if !destChain.IsZetaChain() { paramsDest, found := ob.coreContext.GetEVMChainParams(destChain.ChainId) if !found { - ob.logger.ExternalChainWatcher.Warn().Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) + ob.logger.InTx.Warn().Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) return nil } if strings.EqualFold(destAddr, paramsDest.ZetaTokenContractAddress) { - ob.logger.ExternalChainWatcher.Warn().Msgf("potential attack attempt: %s destination address is ZETA token contract address %s", destChain, destAddr) + ob.logger.InTx.Warn().Msgf("potential attack attempt: %s destination address is ZETA token contract address %s", destChain, destAddr) return nil } } message := base64.StdEncoding.EncodeToString(event.Message) - ob.logger.ExternalChainWatcher.Info().Msgf("ZetaSent inTx detected on chain %d tx %s block %d from %s value %s message %s", + ob.logger.InTx.Info().Msgf("ZetaSent inTx detected on chain %d tx %s block %d from %s value %s message %s", ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) return zetabridge.GetInBoundVoteMessage( @@ -324,7 +327,7 @@ func (ob *ChainClient) BuildInboundVoteMsgForTokenSentToTSS(tx *ethrpc.Transacti maybeReceiver = parsedAddress.Hex() } if config.ContainRestrictedAddress(sender.Hex(), maybeReceiver) { - compliance.PrintComplianceLog(ob.logger.ExternalChainWatcher, ob.logger.Compliance, + compliance.PrintComplianceLog(ob.logger.InTx, ob.logger.Compliance, false, ob.chain.ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") return nil } @@ -333,10 +336,10 @@ func (ob *ChainClient) BuildInboundVoteMsgForTokenSentToTSS(tx *ethrpc.Transacti // #nosec G703 err is already checked data, _ := hex.DecodeString(message) if bytes.Equal(data, []byte(constant.DonationMessage)) { - ob.logger.ExternalChainWatcher.Info().Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.logger.InTx.Info().Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.chain.ChainId) return nil } - ob.logger.ExternalChainWatcher.Info().Msgf("TSS inTx detected on chain %d tx %s block %d from %s value %s message %s", + ob.logger.InTx.Info().Msgf("TSS inTx detected on chain %d tx %s block %d from %s value %s message %s", ob.chain.ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) return zetabridge.GetInBoundVoteMessage( diff --git a/zetaclient/evm/outbound_transaction_data_test.go b/zetaclient/evm/outbound_transaction_data_test.go index 8943091157..85c812119b 100644 --- a/zetaclient/evm/outbound_transaction_data_test.go +++ b/zetaclient/evm/outbound_transaction_data_test.go @@ -13,9 +13,7 @@ import ( func TestSigner_SetChainAndSender(t *testing.T) { // setup inputs - cctx, err := getCCTX() - require.NoError(t, err) - + cctx := getCCTX(t) txData := &OutBoundTransactionData{} logger := zerolog.Logger{} @@ -45,9 +43,7 @@ func TestSigner_SetChainAndSender(t *testing.T) { } func TestSigner_SetupGas(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) - + cctx := getCCTX(t) evmSigner, err := getNewEvmSigner() require.NoError(t, err) @@ -77,16 +73,14 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { require.NoError(t, err) t.Run("NewOutBoundTransactionData success", func(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) require.NoError(t, err) }) t.Run("NewOutBoundTransactionData skip", func(t *testing.T) { - cctx, err := getCCTX() - require.NoError(t, err) + cctx := getCCTX(t) cctx.CctxStatus.Status = types.CctxStatus_Aborted _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.NoError(t, err) @@ -94,7 +88,7 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { }) t.Run("NewOutBoundTransactionData unknown chain", func(t *testing.T) { - cctx, err := getInvalidCCTX() + cctx := getInvalidCCTX(t) require.NoError(t, err) _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.ErrorContains(t, err, "unknown chain") @@ -102,7 +96,7 @@ func TestSigner_NewOutBoundTransactionData(t *testing.T) { }) t.Run("NewOutBoundTransactionData setup gas error", func(t *testing.T) { - cctx, err := getCCTX() + cctx := getCCTX(t) require.NoError(t, err) cctx.GetCurrentOutTxParam().OutboundTxGasPrice = "invalidGasPrice" _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) diff --git a/zetaclient/interfaces/interfaces.go b/zetaclient/interfaces/interfaces.go index 75242ab17d..1c01b7e486 100644 --- a/zetaclient/interfaces/interfaces.go +++ b/zetaclient/interfaces/interfaces.go @@ -43,7 +43,7 @@ type ChainClient interface { SetChainParams(observertypes.ChainParams) GetChainParams() observertypes.ChainParams GetTxID(nonce uint64) string - ExternalChainWatcherForNewInboundTrackerSuggestions() + WatchIntxTracker() } // ChainSigner is the interface to sign transactions for a chain @@ -124,6 +124,7 @@ type BTCRPCClient interface { ListUnspentMinMaxAddresses(minConf int, maxConf int, addrs []btcutil.Address) ([]btcjson.ListUnspentResult, error) EstimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) GetTransaction(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, error) + GetRawTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) GetBlockCount() (int64, error) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) diff --git a/zetaclient/testdata/btc/block_trimmed_8332_831071.json b/zetaclient/testdata/btc/block_trimmed_8332_831071.json new file mode 100644 index 0000000000..4a662f8263 --- /dev/null +++ b/zetaclient/testdata/btc/block_trimmed_8332_831071.json @@ -0,0 +1,108 @@ +{ + "hash": "0000000000000000000157d7c042e957e5005bdce3b10cf1219c846ff88871f6", + "confirmations": 4636, + "strippedsize": 767728, + "size": 1689876, + "weight": 3993060, + "height": 831071, + "version": 536887296, + "versionHex": "20004000", + "merkleroot": "d2e93c6392c54f1877f4b2f8821af02c551afa4c30a9eac61c4d49c6aa5ad70a", + "tx": [ + { + "hex": "", + "txid": "b350eafdbf61a9f5718410ba2851a5d350d59548608b399e4fc15f4ce593a54a", + "hash": "6b400359ca1f3e33ce049ff411535815f0ebdde0de6aa425b9a8c56b470be096", + "size": 214, + "vsize": 187, + "weight": 748, + "version": 2, + "locktime": 0, + "vin": [ + { + "coinbase": "035fae0c0445ced2652f466f756e6472792055534120506f6f6c202364726f70676f6c642f23714ef33cd5000000000000", + "sequence": 4294967295, + "witness": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ] + } + ], + "vout": [ + { + "value": 6.43696349, + "n": 0, + "scriptPubKey": { + "asm": "0 35f6de260c9f3bdee47524c473a6016c0c055cb9", + "hex": "001435f6de260c9f3bdee47524c473a6016c0c055cb9", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN aa21a9ed4ce95738e15b620273a0ceb3f5e243e6e50b84355c1c602783d77ad11ce54c66", + "hex": "6a24aa21a9ed4ce95738e15b620273a0ceb3f5e243e6e50b84355c1c602783d77ad11ce54c66", + "type": "nulldata" + } + } + ] + }, + { + "hex": "", + "txid": "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867", + "hash": "39f6a6cfe5c8d3f0066b2bf3813f2cc5986dd0a425d5df2cbf9aa298433cf1c2", + "size": 226, + "vsize": 175, + "weight": 700, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "9b5071d68ac8f3a9fd5da76bf1880915ea51f16dfa48e4404d4fe2c5ee5fc574", + "vout": 2, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "9e622c221101926bc0134b564419e385ad1708118442a567d51483558c9035949becc91fc84386fa2b1b11268e035e2e756c7c1e6f248c7690cfad469e0c1412" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0, + "n": 0, + "scriptPubKey": { + "asm": "OP_RETURN bf9686022c1cbe7d07d8e473b6f4e1bd91811cb60000", + "hex": "6a16bf9686022c1cbe7d07d8e473b6f4e1bd91811cb60000", + "type": "nulldata" + } + }, + { + "value": 0.0001085, + "n": 1, + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0.0008211, + "n": 2, + "scriptPubKey": { + "asm": "1 34439061ba7dede70080b475a2fb85b8b8803c2da7c4c9e48589042180954d82", + "hex": "512034439061ba7dede70080b475a2fb85b8b8803c2da7c4c9e48589042180954d82", + "type": "witness_v1_taproot" + } + } + ] + } + ], + "time": 1708314180, + "nonce": 1304717940, + "bits": "170371b1", + "difficulty": 81725299822043.22, + "previousblockhash": "00000000000000000000fa6c7c8d518d28c265e4d93618dc11476a3bd9b2c1c2", + "nextblockhash": "0000000000000000000135e541c901f610309100dd7a078ab94969006e6e20e2" +} diff --git a/zetaclient/testdata/btc/chain_8332_intx_raw_result_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json b/zetaclient/testdata/btc/chain_8332_intx_raw_result_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json new file mode 100644 index 0000000000..5445d50411 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_intx_raw_result_847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa.json @@ -0,0 +1,51 @@ +{ + "hex": "0200000000010197266596828f8e001991cc881a485eb2172a34c25172599a0bfc32389624d2c50200000000ffffffff031027000000000000160014daaae0d3de9d8fdee31661e61aea828b59be78640000000000000000166a1467ed0bcc4e1256bc2ce87d22e190d63a120114bfdf24010000000000160014d1ec69828aedc54637583e059643aee3f862cd2302473044022047ecada1e409279fe2b714db2b126714b88a67032b1bd1247e935c4b7b71ff50022055a480a97d2dbdd8cf8f7585296e62b538fc735b61d672b4565b9a1df4a8225e0121035ce366bfd01fde742562f7cc5e6ae125ec2bac862d3c3a11d2d70b1b3baa9ae000000000", + "txid": "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa", + "hash": "79988813059cd0c82f9291299d4d6324a3da56c8a6507797c0f2ebf0a13cee56", + "size": 253, + "vsize": 172, + "weight": 685, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697", + "vout": 2, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022047ecada1e409279fe2b714db2b126714b88a67032b1bd1247e935c4b7b71ff50022055a480a97d2dbdd8cf8f7585296e62b538fc735b61d672b4565b9a1df4a8225e01", + "035ce366bfd01fde742562f7cc5e6ae125ec2bac862d3c3a11d2d70b1b3baa9ae0" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0001, + "n": 0, + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0, + "n": 1, + "scriptPubKey": { + "asm": "OP_RETURN 67ed0bcc4e1256bc2ce87d22e190d63a120114bf", + "hex": "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf", + "type": "nulldata" + } + }, + { + "value": 0.00074975, + "n": 2, + "scriptPubKey": { + "asm": "0 d1ec69828aedc54637583e059643aee3f862cd23", + "hex": "0014d1ec69828aedc54637583e059643aee3f862cd23", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a.json b/zetaclient/testdata/btc/chain_8332_msgtx_211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a.json new file mode 100644 index 0000000000..6dcd43a643 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_211568441340fd5e10b1a8dcb211a18b9e853dbdf265ebb1c728f9b52813455a.json @@ -0,0 +1,25 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 30, 42, 54, 220, 104, 77, 18, 96, 207, 100, 83, 218, 222, 30, 81, 215, + 17, 216, 241, 23, 140, 26, 84, 197, 241, 73, 15, 53, 249, 182, 38, 134 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "MEUCIQDJuzu34uVBPrsi8DgNfQH5TwM2uX//dXWkULGhElDYzAIgYxSUnwOnyHIBR546z56in6he+zNI5xMEso/0szFIy7QB", + "Am5WKFBuzTMkLlzrX9r+TTBmtcDxWbPAWmIe9l8XfqKG" + ], + "Sequence": 4294967293 + } + ], + "TxOut": [ + { "Value": 249140, "PkScript": "qRTc+XVrIoqedsOz7aKs1GSUk2FjrYc=" }, + { "Value": 965576774, "PkScript": "ABT2CDTvFlJTxXGxHOn6dORmkvxewQ==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867.json b/zetaclient/testdata/btc/chain_8332_msgtx_3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867.json new file mode 100644 index 0000000000..15152341cd --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867.json @@ -0,0 +1,29 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 116, 197, 95, 238, 197, 226, 79, 77, 64, 228, 72, 250, 109, 241, 81, + 234, 21, 9, 136, 241, 107, 167, 93, 253, 169, 243, 200, 138, 214, 113, + 80, 155 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "nmIsIhEBkmvAE0tWRBnjha0XCBGEQqVn1RSDVYyQNZSb7MkfyEOG+isbESaOA14udWx8Hm8kjHaQz61GngwUEg==" + ], + "Sequence": 4294967295 + } + ], + "TxOut": [ + { "Value": 0, "PkScript": "aha/loYCLBy+fQfY5HO29OG9kYEctgAA" }, + { "Value": 10850, "PkScript": "ABTaquDT3p2P3uMWYeYa6oKLWb54ZA==" }, + { + "Value": 82110, + "PkScript": "USA0Q5Bhun3t5wCAtHWi+4W4uIA8LafEyeSFiQQhgJVNgg==" + } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7.json b/zetaclient/testdata/btc/chain_8332_msgtx_781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7.json new file mode 100644 index 0000000000..13789f13b1 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_781fc8d41b476dbceca283ebff9573fda52c8fdbba5e78152aeb4432286836a7.json @@ -0,0 +1,23 @@ +{ + "Version": 1, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 88, 35, 68, 215, 98, 226, 80, 135, 144, 112, 176, 41, 10, 204, 196, + 243, 171, 206, 47, 130, 190, 82, 88, 31, 234, 63, 168, 65, 219, 15, + 248, 81 + ], + "Index": 1 + }, + "SignatureScript": "SDBFAiEAtm10q/gt84o5QCkWsxouad5aqiEDgBd3h3ci/8tuDl4CIB9wKaSz2HDRhPKwkoXBbYqZwTD+8fhiZOolJcwvgV49ASED25w7Nl3+PHENu2OrUD2EmDvsdylmoWe0hSDOMtQvj04=", + "Witness": null, + "Sequence": 268435456 + } + ], + "TxOut": [ + { "Value": 1425000, "PkScript": "qRRdKyXqq2FtT8BTLay8+A2Un6cUTIc=" }, + { "Value": 19064706, "PkScript": "dqkUk2fpBSIxBUekNfA/gxxYw1RZ3KGIrA==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697.json b/zetaclient/testdata/btc/chain_8332_msgtx_c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697.json new file mode 100644 index 0000000000..1d0004422e --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697.json @@ -0,0 +1,42 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 127, 3, 175, 215, 135, 178, 52, 169, 234, 18, 205, 65, 18, 193, 182, + 123, 2, 102, 254, 107, 221, 18, 243, 92, 98, 226, 161, 209, 185, 140, + 72, 91 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "MEUCIQCWzKD96dT53Y9o+KCtLvB5WXrJL4X2XGgpiz3rRBdJMwIgcdwc7Z870dOeiVUZdSLjRAJ8hkLz0l7TXVSLF0ZArekB", + "A1zjZr/QH950JWL3zF5q4SXsK6yGLTw6EdLXCxs7qprg" + ], + "Sequence": 4294967295 + }, + { + "PreviousOutPoint": { + "Hash": [ + 175, 165, 125, 14, 98, 13, 62, 204, 88, 160, 18, 74, 177, 75, 159, 44, + 131, 4, 151, 145, 200, 2, 177, 50, 227, 26, 2, 89, 184, 155, 179, 54 + ], + "Index": 2 + }, + "SignatureScript": "", + "Witness": [ + "MEQCIGbWmCHGFlrSKF7+tnQHhrQGDNS0PR2E4lxq/OLz68CFAiB/yHWTSaAs1GbDzngvzFkGu76N/YKJX1iy88irfDg0tAE=", + "A1zjZr/QH950JWL3zF5q4SXsK6yGLTw6EdLXCxs7qprg" + ], + "Sequence": 4294967295 + } + ], + "TxOut": [ + { "Value": 1000, "PkScript": "ABTaquDT3p2P3uMWYeYa6oKLWb54ZA==" }, + { "Value": 0, "PkScript": "ahRn7QvMThJWvCzofSLhkNY6EgEUvw==" }, + { "Value": 89858, "PkScript": "ABTR7GmCiu3FRjdYPgWWQ67j+GLNIw==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_msgtx_d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016.json b/zetaclient/testdata/btc/chain_8332_msgtx_d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016.json new file mode 100644 index 0000000000..e7343425a4 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_msgtx_d13de30b0cc53b5c4702b184ae0a0b0f318feaea283185c1cddb8b341c27c016.json @@ -0,0 +1,28 @@ +{ + "Version": 2, + "TxIn": [ + { + "PreviousOutPoint": { + "Hash": [ + 121, 61, 82, 212, 158, 94, 66, 22, 92, 111, 162, 24, 255, 33, 79, 208, + 234, 255, 133, 0, 255, 110, 97, 104, 105, 49, 187, 30, 61, 168, 9, 166 + ], + "Index": 1 + }, + "SignatureScript": "", + "Witness": [ + "MEQCIDeXH61fcBuKz091+3kS2ZETW4hjkbfDIxRcnVU+TrIZAiBBg9boZh2AcAa5HIXpKS5YSfDLsJz817yDC5lPEnzYLAE=", + "AxAyc+pMvZfG+8ELyKyr2TBo0kulM0XM4QisuLyR7L3f" + ], + "Sequence": 4294967295 + } + ], + "TxOut": [ + { + "Value": 50000000, + "PkScript": "ACDxbbwTHn6bqd/LCK85n+IPtDC9eQhvG4HDv3AE4JX7jA==" + }, + { "Value": 9992996, "PkScript": "ABT8w6bf+cf2lmZLrUWPhgVJeNjEVw==" } + ], + "LockTime": 0 +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2PKH_9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2PKH_9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca.json new file mode 100644 index 0000000000..2aa2635469 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2PKH_9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca.json @@ -0,0 +1,42 @@ +{ + "hex": "01000000000101f163b41b5a56ac3dd741d8695d964c50eb4acda02d08b9e0f9827a4c5d2accad0100000000ffffffff0220651100000000001976a914a386e1676ff60f1a0d1645e1c73d06ab7872467488acb778880000000000160014e7f663e64af624f3654f2d23812c6ac9b13dc00c02473044022071bdf8aeb210418daacf0cb5e4185ed7629a2db94be13746985a356bdcc067ac02203a7dc27af4959e03a5d8ee3100170aafc5710e5cdaaf5d2991a509584eb0a7040121020476f998fae688c3f412755b3b850139995781835f66a62368794f938669350600000000", + "txid": "9c741de6e17382b7a9113fc811e3558981a35a360e3d1262a6675892c91322ca", + "hash": "3de8e19acb8e05fc19aa6196ef4b10d303a107b22a035d97808867aacaea335d", + "size": 225, + "vsize": 144, + "weight": 573, + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "adcc2a5d4c7a82f9e0b9082da0cd4aeb504c965d69d841d73dac565a1bb463f1", + "vout": 1, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022071bdf8aeb210418daacf0cb5e4185ed7629a2db94be13746985a356bdcc067ac02203a7dc27af4959e03a5d8ee3100170aafc5710e5cdaaf5d2991a509584eb0a70401", + "020476f998fae688c3f412755b3b850139995781835f66a62368794f9386693506" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0114, + "n": 0, + "scriptPubKey": { + "asm": "OP_DUP OP_HASH160 a386e1676ff60f1a0d1645e1c73d06ab78724674 OP_EQUALVERIFY OP_CHECKSIG", + "hex": "76a914a386e1676ff60f1a0d1645e1c73d06ab7872467488ac", + "type": "pubkeyhash" + } + }, + { + "value": 0.08943799, + "n": 1, + "scriptPubKey": { + "asm": "0 e7f663e64af624f3654f2d23812c6ac9b13dc00c", + "hex": "0014e7f663e64af624f3654f2d23812c6ac9b13dc00c", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2SH_fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2SH_fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21.json new file mode 100644 index 0000000000..a388ad429c --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2SH_fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21.json @@ -0,0 +1,42 @@ +{ + "hex": "020000000001019481d17b8fb50d38fa5f726b858c2300567620447c93b4d77582ebd8ee286ec70000000000000000800269510f000000000017a91404b8d73fbfeccaea8c253279811e60c5e1d4a9a887ef03160000000000160014811535e6ed76ba320d54bb8f418fcb53d527e2c40247304402200e757ee5c1400ae06a47a558e11540ea7acc75ed366b2acc3301e4a17ebeccdc02204e00c03de16b6c14850cd556c503f1258da7d9f25886b4bc4d4510d182e1ec77012102f9edaa905fbafdacddbbae0fb3cefeb092c5a0dce8a7aeaeb7bd17cf6fa116c900000000", + "txid": "fd68c8b4478686ca6f5ae4c28eaab055490650dbdaa6c2c8e380a7e075958a21", + "hash": "6e3db4ed0fcaad7daff5166615b5c432ba151fb885a603cbb33645fe6f360a86", + "size": 223, + "vsize": 142, + "weight": 565, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "c76e28eed8eb8275d7b4937c4420765600238c856b725ffa380db58f7bd18194", + "vout": 0, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "304402200e757ee5c1400ae06a47a558e11540ea7acc75ed366b2acc3301e4a17ebeccdc02204e00c03de16b6c14850cd556c503f1258da7d9f25886b4bc4d4510d182e1ec7701", + "02f9edaa905fbafdacddbbae0fb3cefeb092c5a0dce8a7aeaeb7bd17cf6fa116c9" + ], + "sequence": 2147483648 + } + ], + "vout": [ + { + "value": 0.01003881, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 04b8d73fbfeccaea8c253279811e60c5e1d4a9a8 OP_EQUAL", + "hex": "a91404b8d73fbfeccaea8c253279811e60c5e1d4a9a887", + "type": "scripthash" + } + }, + { + "value": 0.01442799, + "n": 1, + "scriptPubKey": { + "asm": "0 811535e6ed76ba320d54bb8f418fcb53d527e2c4", + "hex": "0014811535e6ed76ba320d54bb8f418fcb53d527e2c4", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2TR_259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2TR_259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7.json new file mode 100644 index 0000000000..2de51e3356 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2TR_259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7.json @@ -0,0 +1,42 @@ +{ + "hex": "02000000000101ab6912b3e805bb584ed67d71aea4f5504f18c279d77409001b0fe307277e23020100000000ffffffff02c8af000000000000225120ac30d6fed6e38b53ea9e2f78b37366dd99c0df7408ab2627c9d05b7a3c2ae0816efd00000000000016001433d1c36b26902298ac55fc176653854fc2f9bc6e02473044022072fee83302148a971a5e1c24e063d518665dfe76c1791f20da5ec8df4a352fdb02204f1a2ba4068b86fe3c1ca2ec77a5c45f0dcb3af8d9dbc2d38091afae6011d83f0121038bdc9021a5d81cbfc28b5b41d2465ca891ba233e9d6ca72cfef654a1ef37749200000000", + "txid": "259fc21e63e138136c8f19270a0f7ca10039a66a474f91d23a17896f46e677a7", + "hash": "253639775c305a4c28707be417bbb64ce3fa1b33ce582a60f95262f0e847d553", + "size": 234, + "vsize": 153, + "weight": 609, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "02237e2707e30f1b000974d779c2184f50f5a4ae717dd64e58bb05e8b31269ab", + "vout": 1, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022072fee83302148a971a5e1c24e063d518665dfe76c1791f20da5ec8df4a352fdb02204f1a2ba4068b86fe3c1ca2ec77a5c45f0dcb3af8d9dbc2d38091afae6011d83f01", + "038bdc9021a5d81cbfc28b5b41d2465ca891ba233e9d6ca72cfef654a1ef377492" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.00045, + "n": 0, + "scriptPubKey": { + "asm": "1 ac30d6fed6e38b53ea9e2f78b37366dd99c0df7408ab2627c9d05b7a3c2ae081", + "hex": "5120ac30d6fed6e38b53ea9e2f78b37366dd99c0df7408ab2627c9d05b7a3c2ae081", + "type": "witness_v1_taproot" + } + }, + { + "value": 0.00064878, + "n": 1, + "scriptPubKey": { + "asm": "0 33d1c36b26902298ac55fc176653854fc2f9bc6e", + "hex": "001433d1c36b26902298ac55fc176653854fc2f9bc6e", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WPKH_5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WPKH_5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b.json new file mode 100644 index 0000000000..4f68b6653c --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WPKH_5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b.json @@ -0,0 +1,42 @@ +{ + "hex": "0100000000010147073577cf72e6227bb0bfbcc4ada7ecb28323c32c5c3b7c415b2b6e873022e37600000000fdffffff024238010000000000160014e99275308221c877b1ef7e7cbd55015e67e475d9bee10e0000000000225120ce403c2e1563b1e05eb213b4f601c2adf26293b86532146aab241c51a742eacf024730440220126e8c6a5fb11a1c0cbafc348601d427e9126d17c7136cb99fa6e6e61259066b02206fae29f384575cfed54946794b8784bf0f68b4ea5ed6caa5a12bb7d18a226de30121031f88031b5aa6e540ac109e4b6875b1c2826f84a3a4b23f4520d65ed54947b13b00000000", + "txid": "5d09d232bfe41c7cb831bf53fc2e4029ab33a99087fd5328a2331b52ff2ebe5b", + "hash": "197248f8cae9b569f871f728b8163f1f322c145095b4df6425578e752928873e", + "size": 234, + "vsize": 153, + "weight": 609, + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "e32230876e2b5b417c3b5c2cc32383b2eca7adc4bcbfb07b22e672cf77350747", + "vout": 118, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "30440220126e8c6a5fb11a1c0cbafc348601d427e9126d17c7136cb99fa6e6e61259066b02206fae29f384575cfed54946794b8784bf0f68b4ea5ed6caa5a12bb7d18a226de301", + "031f88031b5aa6e540ac109e4b6875b1c2826f84a3a4b23f4520d65ed54947b13b" + ], + "sequence": 4294967293 + } + ], + "vout": [ + { + "value": 0.00079938, + "n": 0, + "scriptPubKey": { + "asm": "0 e99275308221c877b1ef7e7cbd55015e67e475d9", + "hex": "0014e99275308221c877b1ef7e7cbd55015e67e475d9", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0.00975294, + "n": 1, + "scriptPubKey": { + "asm": "1 ce403c2e1563b1e05eb213b4f601c2adf26293b86532146aab241c51a742eacf", + "hex": "5120ce403c2e1563b1e05eb213b4f601c2adf26293b86532146aab241c51a742eacf", + "type": "witness_v1_taproot" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WSH_791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53.json b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WSH_791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53.json new file mode 100644 index 0000000000..adc78f14c8 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_tx_raw_result_P2WSH_791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53.json @@ -0,0 +1,73 @@ +{ + "hex": "0200000000010583fd136d03a9483695a252222fb2d0cb73ba8c76793ad5f876c375f58aa300920600000000ffffffff5a0e44799e3256044c7f4a89ed2c0d1e1e4341c8d9d41b08b96d2b0592d1d7c00e00000000ffffffff48245fa209bbdcbe9009a5a38eb871972ae1dcf6ffe665095fe4f142682401690c00000000ffffffff6b288db05995de2a600225624e9e3e9f59eadb8f3a9654de144d953df62a12831300000000ffffffff5354734d187ee87102beea3c59b707337281e269e869657936ef271cb40a01b90000000000ffffffff0193d12d02000000002200200334174ebe7b38f5c20d4dfb5136c72b2383e2f4fc26421e94b16f81e191cb3202473044022100c532ad6675bdd13b3c8dd3202d6d576bfb9db739f2b8f3895e5358c075c1273f021f4b201148c88cc8208f32151490e3b236c473553ffd6fe3e7a4d14fe3291b1f012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02483045022100d374b7f988b8aeaf8a971ff6358373e3aa015e5ab9c8a325bfa837c36ffef6360220615ce57849e5499592161f404493b6db197021e280c34d0cf30fd825d3bbddae012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02473044022046cc9dc40d51a6978edcb15b6b404618691f3ff0255f1afa431a973d7246c8c3022027e64ec89d09862ef8485f54352728344ca871b4f5bc536f019db07023f8d3bf012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02473044022037dc56be5a40c2f7f13da618aa56745c8ef6f60ae70e926bf6af55b504179dd902200227ab4ba869dd658f7956161c0d3d5df2fa6255ea064839588d9a42636b15c6012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a02473044022033c0b3daac006e07e7537e7f821ad60ae363a1c7cf01ae4dcd1cba1f1409cadd02200844428654b8ed4f6e6e3cab7d7613666eaf667f8d8b1e0e7c7ac921838137a3012103a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a00000000", + "txid": "791bb9d16f7ab05f70a116d18eaf3552faf77b9d5688699a480261424b4f7e53", + "hash": "f9610c875ed2084ea51e8c87077f4eb2d6bfc9c40a6a4ab011046b59a1a35a04", + "size": 796, + "vsize": 393, + "weight": 1570, + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "9200a38af575c376f8d53a79768cba73cbd0b22f2252a2953648a9036d13fd83", + "vout": 6, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022100c532ad6675bdd13b3c8dd3202d6d576bfb9db739f2b8f3895e5358c075c1273f021f4b201148c88cc8208f32151490e3b236c473553ffd6fe3e7a4d14fe3291b1f01", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "c0d7d192052b6db9081bd4d9c841431e1e0d2ced894a7f4c0456329e79440e5a", + "vout": 14, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3045022100d374b7f988b8aeaf8a971ff6358373e3aa015e5ab9c8a325bfa837c36ffef6360220615ce57849e5499592161f404493b6db197021e280c34d0cf30fd825d3bbddae01", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "6901246842f1e45f0965e6fff6dce12a9771b88ea3a50990bedcbb09a25f2448", + "vout": 12, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022046cc9dc40d51a6978edcb15b6b404618691f3ff0255f1afa431a973d7246c8c3022027e64ec89d09862ef8485f54352728344ca871b4f5bc536f019db07023f8d3bf01", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "83122af63d954d14de54963a8fdbea599f3e9e4e622502602ade9559b08d286b", + "vout": 19, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022037dc56be5a40c2f7f13da618aa56745c8ef6f60ae70e926bf6af55b504179dd902200227ab4ba869dd658f7956161c0d3d5df2fa6255ea064839588d9a42636b15c601", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + }, + { + "txid": "b9010ab41c27ef36796569e869e281723307b7593ceabe0271e87e184d735453", + "vout": 0, + "scriptSig": { "asm": "", "hex": "" }, + "txinwitness": [ + "3044022033c0b3daac006e07e7537e7f821ad60ae363a1c7cf01ae4dcd1cba1f1409cadd02200844428654b8ed4f6e6e3cab7d7613666eaf667f8d8b1e0e7c7ac921838137a301", + "03a81f3fe200eb9637c2f553ce1daad935d1cd62c3f2a0bd030817ee812395332a" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.36557203, + "n": 0, + "scriptPubKey": { + "asm": "0 0334174ebe7b38f5c20d4dfb5136c72b2383e2f4fc26421e94b16f81e191cb32", + "hex": "00200334174ebe7b38f5c20d4dfb5136c72b2383e2f4fc26421e94b16f81e191cb32", + "type": "witness_v0_scripthash" + } + } + ] +} diff --git a/zetaclient/testutils/mempool_client.go b/zetaclient/testutils/mempool_client.go index 09b48b8a53..03c79120dc 100644 --- a/zetaclient/testutils/mempool_client.go +++ b/zetaclient/testutils/mempool_client.go @@ -9,8 +9,10 @@ import ( ) const ( - APIURLBlocks = "https://mempool.space/api/v1/blocks" - APIUrlBlocksTestnet = "https://mempool.space/testnet/api/v1/blocks" + APIURLBlocks = "https://mempool.space/api/v1/blocks" + APIURLBlockTxs = "https://mempool.space/api/block/%s/txs" + APIURLBlocksTestnet = "https://mempool.space/testnet/api/v1/blocks" + APIURLBlockTxsTestnet = "https://mempool.space/testnet/api/block/%s/txs" ) type MempoolBlock struct { @@ -30,6 +32,39 @@ type MempoolBlock struct { Extras BlockExtra `json:"extras"` } +type Vin struct { + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Prevout struct { + Scriptpubkey string `json:"scriptpubkey"` + ScriptpubkeyAsm string `json:"scriptpubkey_asm"` + ScriptpubkeyType string `json:"scriptpubkey_type"` + ScriptpubkeyAddress string `json:"scriptpubkey_address"` + Value int64 `json:"value"` + } `json:"prevout"` + Scriptsig string `json:"scriptsig"` + IsCoinbase bool `json:"is_coinbase"` + Sequence uint32 `json:"sequence"` +} + +type Vout struct { + Scriptpubkey string `json:"scriptpubkey"` + ScriptpubkeyAsm string `json:"scriptpubkey_asm"` + ScriptpubkeyType string `json:"scriptpubkey_type"` + Value int64 `json:"value"` +} + +type MempoolTx struct { + TxID string `json:"txid"` + Version int `json:"version"` + LockTime int `json:"locktime"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` + Size int `json:"size"` + Weight int `json:"weight"` + Fee int `json:"fee"` +} + type BlockExtra struct { TotalFees int `json:"totalFees"` MedianFee float64 `json:"medianFee"` @@ -91,10 +126,11 @@ func Get(ctx context.Context, path string, v interface{}) error { return json.NewDecoder(r.Body).Decode(v) } +// GetBlocks returns return 15 mempool.space blocks [n-14, n] per request func GetBlocks(ctx context.Context, n int, testnet bool) ([]MempoolBlock, error) { path := fmt.Sprintf("%s/%d", APIURLBlocks, n) if testnet { - path = fmt.Sprintf("%s/%d", APIUrlBlocksTestnet, n) + path = fmt.Sprintf("%s/%d", APIURLBlocksTestnet, n) } blocks := make([]MempoolBlock, 0) if err := Get(ctx, path, &blocks); err != nil { @@ -102,3 +138,16 @@ func GetBlocks(ctx context.Context, n int, testnet bool) ([]MempoolBlock, error) } return blocks, nil } + +// GetBlockTxs a list of transactions in the block (up to 25 transactions beginning at index 0) +func GetBlockTxs(ctx context.Context, blockHash string, testnet bool) ([]MempoolTx, error) { + path := fmt.Sprintf(APIURLBlockTxs, blockHash) + if testnet { + path = fmt.Sprintf(APIURLBlockTxsTestnet, blockHash) + } + txs := make([]MempoolTx, 0) + if err := Get(ctx, path, &txs); err != nil { + return nil, err + } + return txs, nil +} diff --git a/zetaclient/testutils/stub/btc_rpc.go b/zetaclient/testutils/stub/btc_rpc.go new file mode 100644 index 0000000000..6b8ac84c53 --- /dev/null +++ b/zetaclient/testutils/stub/btc_rpc.go @@ -0,0 +1,121 @@ +package stub + +import ( + "errors" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" +) + +// EvmClient interface +var _ interfaces.BTCRPCClient = &MockBTCRPCClient{} + +// MockBTCRPCClient is a mock implementation of the BTCRPCClient interface +type MockBTCRPCClient struct { + Txs []*btcutil.Tx +} + +// NewMockBTCRPCClient creates a new mock BTC RPC client +func NewMockBTCRPCClient() *MockBTCRPCClient { + client := &MockBTCRPCClient{} + return client.Reset() +} + +// Reset clears the mock data +func (c *MockBTCRPCClient) Reset() *MockBTCRPCClient { + c.Txs = []*btcutil.Tx{} + return c +} + +func (c *MockBTCRPCClient) GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) CreateWallet(_ string, _ ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetNewAddress(_ string) (btcutil.Address, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GenerateToAddress(_ int64, _ btcutil.Address, _ *int64) ([]*chainhash.Hash, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBalance(_ string) (btcutil.Amount, error) { + return 0, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) SendRawTransaction(_ *wire.MsgTx, _ bool) (*chainhash.Hash, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) ListUnspent() ([]btcjson.ListUnspentResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) ListUnspentMinMaxAddresses(_ int, _ int, _ []btcutil.Address) ([]btcjson.ListUnspentResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) EstimateSmartFee(_ int64, _ *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetTransaction(_ *chainhash.Hash) (*btcjson.GetTransactionResult, error) { + return nil, errors.New("not implemented") +} + +// GetRawTransaction returns a pre-loaded transaction or nil +func (c *MockBTCRPCClient) GetRawTransaction(_ *chainhash.Hash) (*btcutil.Tx, error) { + // pop a transaction from the list + if len(c.Txs) > 0 { + tx := c.Txs[len(c.Txs)-1] + c.Txs = c.Txs[:len(c.Txs)-1] + return tx, nil + } + return nil, errors.New("no transaction found") +} + +func (c *MockBTCRPCClient) GetRawTransactionVerbose(_ *chainhash.Hash) (*btcjson.TxRawResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockCount() (int64, error) { + return 0, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockHash(_ int64) (*chainhash.Hash, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockVerbose(_ *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockVerboseTx(_ *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { + return nil, errors.New("not implemented") +} + +func (c *MockBTCRPCClient) GetBlockHeader(_ *chainhash.Hash) (*wire.BlockHeader, error) { + return nil, errors.New("not implemented") +} + +// ---------------------------------------------------------------------------- +// Feed data to the mock BTC RPC client for testing +// ---------------------------------------------------------------------------- + +func (c *MockBTCRPCClient) WithRawTransaction(tx *btcutil.Tx) *MockBTCRPCClient { + c.Txs = append(c.Txs, tx) + return c +} + +func (c *MockBTCRPCClient) WithRawTransactions(txs []*btcutil.Tx) *MockBTCRPCClient { + c.Txs = append(c.Txs, txs...) + return c +} diff --git a/zetaclient/testutils/stub/chain_client.go b/zetaclient/testutils/stub/chain_client.go index 642de62792..f5a5368511 100644 --- a/zetaclient/testutils/stub/chain_client.go +++ b/zetaclient/testutils/stub/chain_client.go @@ -45,7 +45,7 @@ func (s *EVMClient) GetTxID(_ uint64) string { return "" } -func (s *EVMClient) ExternalChainWatcherForNewInboundTrackerSuggestions() { +func (s *EVMClient) WatchIntxTracker() { } // ---------------------------------------------------------------------------- @@ -86,5 +86,5 @@ func (s *BTCClient) GetTxID(_ uint64) string { return "" } -func (s *BTCClient) ExternalChainWatcherForNewInboundTrackerSuggestions() { +func (s *BTCClient) WatchIntxTracker() { } diff --git a/zetaclient/testutils/stub/tss_signer.go b/zetaclient/testutils/stub/tss_signer.go index 5270618df0..0f6d9787e6 100644 --- a/zetaclient/testutils/stub/tss_signer.go +++ b/zetaclient/testutils/stub/tss_signer.go @@ -26,23 +26,25 @@ func init() { // TSS is a mock of TSS signer for testing type TSS struct { + chain chains.Chain evmAddress string btcAddress string } -func NewMockTSS(evmAddress string, btcAddress string) *TSS { +func NewMockTSS(chain chains.Chain, evmAddress string, btcAddress string) *TSS { return &TSS{ + chain: chain, evmAddress: evmAddress, btcAddress: btcAddress, } } func NewTSSMainnet() *TSS { - return NewMockTSS(testutils.TSSAddressEVMMainnet, testutils.TSSAddressBTCMainnet) + return NewMockTSS(chains.BtcMainnetChain(), testutils.TSSAddressEVMMainnet, testutils.TSSAddressBTCMainnet) } func NewTSSAthens3() *TSS { - return NewMockTSS(testutils.TSSAddressEVMAthens3, testutils.TSSAddressBTCAthens3) + return NewMockTSS(chains.BscTestnetChain(), testutils.TSSAddressEVMAthens3, testutils.TSSAddressBTCAthens3) } // Sign uses test key unrelated to any tss key in production @@ -76,7 +78,16 @@ func (s *TSS) BTCAddress() string { } func (s *TSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { - return nil + net, err := chains.GetBTCChainParams(s.chain.ChainId) + if err != nil { + panic(err) + } + tssAddress := s.BTCAddress() + addr, err := btcutil.DecodeAddress(tssAddress, net) + if err != nil { + return nil + } + return addr.(*btcutil.AddressWitnessPubKeyHash) } func (s *TSS) PubKeyCompressedBytes() []byte { diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index c8d5788d8d..37b6cbab14 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -38,16 +38,15 @@ func SaveObjectToJSONFile(obj interface{}, filename string) error { } // LoadObjectFromJSONFile loads an object from a file in JSON format -func LoadObjectFromJSONFile(obj interface{}, filename string) error { +func LoadObjectFromJSONFile(t *testing.T, obj interface{}, filename string) { file, err := os.Open(filepath.Clean(filename)) - if err != nil { - return err - } + require.NoError(t, err) defer file.Close() // read the struct from the file decoder := json.NewDecoder(file) - return decoder.Decode(&obj) + err = decoder.Decode(&obj) + require.NoError(t, err) } func ComplianceConfigTest() config.ComplianceConfig { @@ -81,23 +80,28 @@ func SaveBTCBlockTrimTx(blockVb *btcjson.GetBlockVerboseTxResult, filename strin func LoadEVMBlock(t *testing.T, chainID int64, blockNumber uint64, trimmed bool) *ethrpc.Block { name := path.Join("../", TestDataPathEVM, FileNameEVMBlock(chainID, blockNumber, trimmed)) block := ðrpc.Block{} - err := LoadObjectFromJSONFile(block, name) - require.NoError(t, err) + LoadObjectFromJSONFile(t, block, name) return block } +// LoadBTCIntxRawResult loads archived Bitcoin intx raw result from file +func LoadBTCIntxRawResult(t *testing.T, chainID int64, txHash string, donation bool) *btcjson.TxRawResult { + name := path.Join("../", TestDataPathBTC, FileNameBTCIntx(chainID, txHash, donation)) + rawResult := &btcjson.TxRawResult{} + LoadObjectFromJSONFile(t, rawResult, name) + return rawResult +} + // LoadBTCTxRawResultNCctx loads archived Bitcoin outtx raw result and corresponding cctx func LoadBTCTxRawResultNCctx(t *testing.T, chainID int64, nonce uint64) (*btcjson.TxRawResult, *crosschaintypes.CrossChainTx) { //nameTx := FileNameBTCOuttx(chainID, nonce) nameTx := path.Join("../", TestDataPathBTC, FileNameBTCOuttx(chainID, nonce)) rawResult := &btcjson.TxRawResult{} - err := LoadObjectFromJSONFile(rawResult, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(t, rawResult, nameTx) nameCctx := path.Join("../", TestDataPathCctx, FileNameCctxByNonce(chainID, nonce)) cctx := &crosschaintypes.CrossChainTx{} - err = LoadObjectFromJSONFile(cctx, nameCctx) - require.NoError(t, err) + LoadObjectFromJSONFile(t, cctx, nameCctx) return rawResult, cctx } @@ -110,8 +114,7 @@ func LoadEVMIntx( nameTx := path.Join("../", TestDataPathEVM, FileNameEVMIntx(chainID, intxHash, coinType, false)) tx := ðrpc.Transaction{} - err := LoadObjectFromJSONFile(&tx, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &tx, nameTx) return tx } @@ -124,8 +127,7 @@ func LoadEVMIntxReceipt( nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMIntxReceipt(chainID, intxHash, coinType, false)) receipt := ðtypes.Receipt{} - err := LoadObjectFromJSONFile(&receipt, nameReceipt) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &receipt, nameReceipt) return receipt } @@ -138,8 +140,7 @@ func LoadEVMIntxCctx( nameCctx := path.Join("../", TestDataPathCctx, FileNameEVMIntxCctx(chainID, intxHash, coinType)) cctx := &crosschaintypes.CrossChainTx{} - err := LoadObjectFromJSONFile(&cctx, nameCctx) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &cctx, nameCctx) return cctx } @@ -151,8 +152,7 @@ func LoadCctxByNonce( nameCctx := path.Join("../", TestDataPathCctx, FileNameCctxByNonce(chainID, nonce)) cctx := &crosschaintypes.CrossChainTx{} - err := LoadObjectFromJSONFile(&cctx, nameCctx) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &cctx, nameCctx) return cctx } @@ -178,8 +178,7 @@ func LoadEVMIntxDonation( nameTx := path.Join("../", TestDataPathEVM, FileNameEVMIntx(chainID, intxHash, coinType, true)) tx := ðrpc.Transaction{} - err := LoadObjectFromJSONFile(&tx, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &tx, nameTx) return tx } @@ -192,8 +191,7 @@ func LoadEVMIntxReceiptDonation( nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMIntxReceipt(chainID, intxHash, coinType, true)) receipt := ðtypes.Receipt{} - err := LoadObjectFromJSONFile(&receipt, nameReceipt) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &receipt, nameReceipt) return receipt } @@ -233,8 +231,7 @@ func LoadEVMOuttx( nameTx := path.Join("../", TestDataPathEVM, FileNameEVMOuttx(chainID, intxHash, coinType)) tx := ðtypes.Transaction{} - err := LoadObjectFromJSONFile(&tx, nameTx) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &tx, nameTx) return tx } @@ -247,8 +244,7 @@ func LoadEVMOuttxReceipt( nameReceipt := path.Join("../", TestDataPathEVM, FileNameEVMOuttxReceipt(chainID, intxHash, coinType)) receipt := ðtypes.Receipt{} - err := LoadObjectFromJSONFile(&receipt, nameReceipt) - require.NoError(t, err) + LoadObjectFromJSONFile(t, &receipt, nameReceipt) return receipt } diff --git a/zetaclient/testutils/testdata_naming.go b/zetaclient/testutils/testdata_naming.go index bfe842310e..a920680335 100644 --- a/zetaclient/testutils/testdata_naming.go +++ b/zetaclient/testutils/testdata_naming.go @@ -48,6 +48,17 @@ func FileNameBTCOuttx(chainID int64, nonce uint64) string { return fmt.Sprintf("chain_%d_outtx_raw_result_nonce_%d.json", chainID, nonce) } +// FileNameBTCTxByType returns unified archive file name for tx by type +// txType: "P2TR", "P2WPKH", "P2WSH", "P2PKH", "P2SH +func FileNameBTCTxByType(chainID int64, txType string, txHash string) string { + return fmt.Sprintf("chain_%d_tx_raw_result_%s_%s.json", chainID, txType, txHash) +} + +// FileNameBTCMsgTx returns unified archive file name for btc MsgTx +func FileNameBTCMsgTx(chainID int64, txHash string) string { + return fmt.Sprintf("chain_%d_msgtx_%s.json", chainID, txHash) +} + // FileNameCctxByNonce returns unified archive file name for cctx by nonce func FileNameCctxByNonce(chainID int64, nonce uint64) string { return fmt.Sprintf("cctx_%d_%d.json", chainID, nonce) diff --git a/zetaclient/zetabridge/zetacore_bridge.go b/zetaclient/zetabridge/zetacore_bridge.go index 684092c744..d8a29fa665 100644 --- a/zetaclient/zetabridge/zetacore_bridge.go +++ b/zetaclient/zetabridge/zetacore_bridge.go @@ -220,14 +220,15 @@ func (b *ZetaCoreBridge) UpdateZetaCoreContext(coreContext *corecontext.ZetaCore var newBTCParams *observertypes.ChainParams // check and update chain params for each chain + sampledLogger := b.logger.Sample(&zerolog.BasicSampler{N: 10}) for _, chainParam := range chainParams { - err := observertypes.ValidateChainParams(chainParam) - if err != nil { - b.logger.Warn().Err(err).Msgf("Invalid chain params for chain %d", chainParam.ChainId) + if !chainParam.GetIsSupported() { + sampledLogger.Info().Msgf("Chain %d is not supported yet", chainParam.ChainId) continue } - if !chainParam.GetIsSupported() { - b.logger.Info().Msgf("Chain %d is not supported yet", chainParam.ChainId) + err := observertypes.ValidateChainParams(chainParam) + if err != nil { + sampledLogger.Warn().Err(err).Msgf("Invalid chain params for chain %d", chainParam.ChainId) continue } if chains.IsBitcoinChain(chainParam.ChainId) { @@ -237,12 +238,12 @@ func (b *ZetaCoreBridge) UpdateZetaCoreContext(coreContext *corecontext.ZetaCore } } - supporteChains, err := b.GetSupportedChains() + supportedChains, err := b.GetSupportedChains() if err != nil { return err } - newChains := make([]chains.Chain, len(supporteChains)) - for i, chain := range supporteChains { + newChains := make([]chains.Chain, len(supportedChains)) + for i, chain := range supportedChains { newChains[i] = *chain } keyGen, err := b.GetKeyGen() diff --git a/zetaclient/zetacore_observer.go b/zetaclient/zetacore_observer.go index 8ec76013a5..34d0774507 100644 --- a/zetaclient/zetacore_observer.go +++ b/zetaclient/zetacore_observer.go @@ -154,6 +154,10 @@ func (co *CoreObserver) startCctxScheduler(appContext *appcontext.AppContext) { co.logger.ZetaChainWatcher.Error().Err(err).Msgf("startCctxScheduler: getTargetChainOb failed for chain %d", c.ChainId) continue } + if !ob.GetChainParams().IsSupported { + co.logger.ZetaChainWatcher.Info().Msgf("startCctxScheduler: chain %d is not supported", c.ChainId) + continue + } cctxList, totalPending, err := co.bridge.ListPendingCctx(c.ChainId) if err != nil {