diff --git a/app/setup_handlers.go b/app/setup_handlers.go index bdb18a0aa8..0ed66638d1 100644 --- a/app/setup_handlers.go +++ b/app/setup_handlers.go @@ -5,10 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/x/upgrade/types" - observerTypes "github.com/zeta-chain/zetacore/x/observer/types" ) -const releaseVersion = "v12.1.0" +const releaseVersion = "v12.2.0" func SetupHandlers(app *App) { app.UpgradeKeeper.SetUpgradeHandler(releaseVersion, func(ctx sdk.Context, plan types.Plan, vm module.VersionMap) (module.VersionMap, error) { @@ -17,7 +16,6 @@ func SetupHandlers(app *App) { for m, mb := range app.mm.Modules { vm[m] = mb.ConsensusVersion() } - vm = VersionMigrator{vm}.TriggerMigration(observerTypes.ModuleName) return app.mm.RunMigrations(ctx, app.configurator, vm) }) diff --git a/changelog.md b/changelog.md index 4fb28e0d31..65721bc236 100644 --- a/changelog.md +++ b/changelog.md @@ -3,7 +3,16 @@ ## Unreleased ### Fixes + +* [1638](https://github.com/zeta-chain/node/issues/1638) - additional check to make sure external chain height always increases +* [1672](https://github.com/zeta-chain/node/pull/1672) - paying 50% more than base gas price to buffer EIP1559 gas price increase +* [1642](https://github.com/zeta-chain/node/pull/1642) - Change WhitelistERC20 authorization from group1 to group2 * [1610](https://github.com/zeta-chain/node/issues/1610) - add pending outtx hash to tracker after monitoring for 10 minutes +* [1656](https://github.com/zeta-chain/node/issues/1656) - schedule bitcoin keysign with intervals to avoid keysign failures +* [1662](https://github.com/zeta-chain/node/issues/1662) - skip Goerli BlobTxType transactions introduced in Dencun upgrade +* [1663](https://github.com/zeta-chain/node/issues/1663) - skip Mumbai empty block if ethclient sanity check fails +* [1661](https://github.com/zeta-chain/node/issues/1661) - use estimated SegWit tx size for Bitcoin gas fee calculation +* [1667](https://github.com/zeta-chain/node/issues/1667) - estimate SegWit tx size in uinit of vByte ## Chores * [1694](https://github.com/zeta-chain/node/pull/1694) - remove standalone network, use assert testing package for the entire node folder @@ -14,6 +23,7 @@ * [1577](https://github.com/zeta-chain/node/pull/1577) - add chain header tests in E2E tests and fix admin tests ### Features +* [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 diff --git a/common/coin.go b/common/coin.go index 4c875d97bb..b05a6d3cec 100644 --- a/common/coin.go +++ b/common/coin.go @@ -3,6 +3,8 @@ package common import ( "fmt" "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" ) func GetCoinType(coin string) (CoinType, error) { @@ -16,3 +18,15 @@ func GetCoinType(coin string) (CoinType, error) { // #nosec G701 always in range return CoinType(coinInt), nil } + +func GetAzetaDecFromAmountInZeta(zetaAmount string) (sdk.Dec, error) { + zetaDec, err := sdk.NewDecFromStr(zetaAmount) + if err != nil { + return sdk.Dec{}, err + } + zetaToAzetaConvertionFactor, err := sdk.NewDecFromStr("1000000000000000000") + if err != nil { + return sdk.Dec{}, err + } + return zetaDec.Mul(zetaToAzetaConvertionFactor), nil +} diff --git a/common/coin_test.go b/common/coin_test.go new file mode 100644 index 0000000000..4dd03fa2db --- /dev/null +++ b/common/coin_test.go @@ -0,0 +1,65 @@ +package common_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/zeta-chain/zetacore/common" +) + +func Test_GetAzetaDecFromAmountInZeta(t *testing.T) { + tt := []struct { + name string + zetaAmount string + err assert.ErrorAssertionFunc + azetaAmount sdk.Dec + }{ + { + name: "valid zeta amount", + zetaAmount: "210000000", + err: assert.NoError, + azetaAmount: sdk.MustNewDecFromStr("210000000000000000000000000"), + }, + { + name: "very high zeta amount", + zetaAmount: "21000000000000000000", + err: assert.NoError, + azetaAmount: sdk.MustNewDecFromStr("21000000000000000000000000000000000000"), + }, + { + name: "very low zeta amount", + zetaAmount: "1", + err: assert.NoError, + azetaAmount: sdk.MustNewDecFromStr("1000000000000000000"), + }, + { + name: "zero zeta amount", + zetaAmount: "0", + err: assert.NoError, + azetaAmount: sdk.MustNewDecFromStr("0"), + }, + { + name: "decimal zeta amount", + zetaAmount: "0.1", + err: assert.NoError, + azetaAmount: sdk.MustNewDecFromStr("100000000000000000"), + }, + { + name: "invalid zeta amount", + zetaAmount: "%%%%%$#", + err: assert.Error, + azetaAmount: sdk.MustNewDecFromStr("0"), + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + azeta, err := common.GetAzetaDecFromAmountInZeta(tc.zetaAmount) + tc.err(t, err) + if err == nil { + assert.Equal(t, tc.azetaAmount, azeta) + } + }) + } + +} diff --git a/go.mod b/go.mod index aff1f815c3..8341c0bddf 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,10 @@ require ( gorm.io/gorm v1.24.6 ) -require github.com/binance-chain/tss-lib v0.0.0-20201118045712-70b2cb4bf916 +require ( + github.com/binance-chain/tss-lib v0.0.0-20201118045712-70b2cb4bf916 + github.com/onrik/ethrpc v1.2.0 +) require ( github.com/DataDog/zstd v1.5.2 // indirect diff --git a/go.sum b/go.sum index 96ac6b81eb..be10cf47d8 100644 --- a/go.sum +++ b/go.sum @@ -1869,6 +1869,7 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea/go.mod h1:QMdK4dGB3YhEW2BmA1wgGpPYI3HZy/5gD705PXKUVSg= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= @@ -2358,6 +2359,8 @@ github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c/go.mod h1 github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onrik/ethrpc v1.2.0 h1:BBcr1iWxW1RBP/eyZfzvSKtGgeqexq5qS0yyf4pmKbc= +github.com/onrik/ethrpc v1.2.0/go.mod h1:uvyqpn8+WbsTgBYfouImgEfpIMb0hR8fWGjwdgPHtFU= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index 9dc082ec31..cadd5b51e0 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -25,7 +25,7 @@ import ( // Authorized: admin policy group 1. func (k msgServer) WhitelistERC20(goCtx context.Context, msg *types.MsgWhitelistERC20) (*types.MsgWhitelistERC20Response, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if msg.Creator != k.zetaObserverKeeper.GetParams(ctx).GetAdminPolicyAccount(zetaObserverTypes.Policy_Type_group1) { + if msg.Creator != k.zetaObserverKeeper.GetParams(ctx).GetAdminPolicyAccount(zetaObserverTypes.Policy_Type_group2) { return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Deploy can only be executed by the correct policy account") } erc20Addr := ethcommon.HexToAddress(msg.Erc20Address) diff --git a/x/emissions/abci.go b/x/emissions/abci.go index 50f2f3daae..bc15398209 100644 --- a/x/emissions/abci.go +++ b/x/emissions/abci.go @@ -1,6 +1,7 @@ package emissions import ( + "fmt" "sort" sdkmath "cosmossdk.io/math" @@ -11,29 +12,38 @@ import ( ) func BeginBlocker(ctx sdk.Context, keeper keeper.Keeper) { + emissionPoolBalance := keeper.GetReservesFactor(ctx) + blockRewards := types.BlockReward - reservesFactor, bondFactor, durationFactor := keeper.GetBlockRewardComponents(ctx) - blockRewards := reservesFactor.Mul(bondFactor).Mul(durationFactor) - if blockRewards.IsZero() { + if blockRewards.GT(emissionPoolBalance) { + ctx.Logger().Info(fmt.Sprintf("Block rewards %s are greater than emission pool balance %s", blockRewards.String(), emissionPoolBalance.String())) return } validatorRewards := sdk.MustNewDecFromStr(keeper.GetParams(ctx).ValidatorEmissionPercentage).Mul(blockRewards).TruncateInt() observerRewards := sdk.MustNewDecFromStr(keeper.GetParams(ctx).ObserverEmissionPercentage).Mul(blockRewards).TruncateInt() tssSignerRewards := sdk.MustNewDecFromStr(keeper.GetParams(ctx).TssSignerEmissionPercentage).Mul(blockRewards).TruncateInt() - err := DistributeValidatorRewards(ctx, validatorRewards, keeper.GetBankKeeper(), keeper.GetFeeCollector()) + // Use a tmpCtx, which is a cache-wrapped context to avoid writing to the store + // We commit only if all three distributions are successful, if not the funds stay in the emission pool + tmpCtx, commit := ctx.CacheContext() + err := DistributeValidatorRewards(tmpCtx, validatorRewards, keeper.GetBankKeeper(), keeper.GetFeeCollector()) if err != nil { - panic(err) + ctx.Logger().Error(fmt.Sprintf("Error while distributing validator rewards %s", err)) + return } - err = DistributeObserverRewards(ctx, observerRewards, keeper) + err = DistributeObserverRewards(tmpCtx, observerRewards, keeper) if err != nil { - panic(err) + ctx.Logger().Error(fmt.Sprintf("Error while distributing observer rewards %s", err)) + return } - err = DistributeTssRewards(ctx, tssSignerRewards, keeper.GetBankKeeper()) + err = DistributeTssRewards(tmpCtx, tssSignerRewards, keeper.GetBankKeeper()) if err != nil { - panic(err) + ctx.Logger().Error(fmt.Sprintf("Error while distributing tss signer rewards %s", err)) + return } - types.EmitValidatorEmissions(ctx, bondFactor.String(), reservesFactor.String(), - durationFactor.String(), + commit() + + types.EmitValidatorEmissions(ctx, "", "", + "", validatorRewards.String(), observerRewards.String(), tssSignerRewards.String()) @@ -44,6 +54,7 @@ func BeginBlocker(ctx sdk.Context, keeper keeper.Keeper) { // This function uses the distribution module of cosmos-sdk , by directly sending funds to the feecollector. func DistributeValidatorRewards(ctx sdk.Context, amount sdkmath.Int, bankKeeper types.BankKeeper, feeCollector string) error { coin := sdk.NewCoins(sdk.NewCoin(config.BaseDenom, amount)) + ctx.Logger().Info(fmt.Sprintf(fmt.Sprintf("Distributing Validator Rewards Total:%s To FeeCollector : %s", amount.String(), feeCollector))) return bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, feeCollector, coin) } @@ -76,7 +87,7 @@ func DistributeObserverRewards(ctx sdk.Context, amount sdkmath.Int, keeper keepe if totalRewardsUnits > 0 && amount.IsPositive() { rewardPerUnit = amount.Quo(sdk.NewInt(totalRewardsUnits)) } - + ctx.Logger().Debug(fmt.Sprintf("Total Rewards Units : %d , rewards per Unit %s ,number of ballots :%d", totalRewardsUnits, rewardPerUnit.String(), len(ballotIdentifiers))) sortedKeys := make([]string, 0, len(rewardsDistributer)) for k := range rewardsDistributer { sortedKeys = append(sortedKeys, k) diff --git a/x/emissions/abci_test.go b/x/emissions/abci_test.go index fd7a49f0b2..50651ca3e2 100644 --- a/x/emissions/abci_test.go +++ b/x/emissions/abci_test.go @@ -1,252 +1,229 @@ package emissions_test -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strconv" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/stretchr/testify/assert" - "github.com/tendermint/tendermint/crypto/ed25519" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - tmtypes "github.com/tendermint/tendermint/types" - zetaapp "github.com/zeta-chain/zetacore/app" - "github.com/zeta-chain/zetacore/cmd/zetacored/config" - "github.com/zeta-chain/zetacore/testutil/simapp" - emissionsModule "github.com/zeta-chain/zetacore/x/emissions" - emissionsModuleTypes "github.com/zeta-chain/zetacore/x/emissions/types" -) - -func getaZetaFromString(amount string) sdk.Coins { - emissionPoolInt, _ := sdk.NewIntFromString(amount) - return sdk.NewCoins(sdk.NewCoin(config.BaseDenom, emissionPoolInt)) -} - -func SetupApp(t *testing.T, params emissionsModuleTypes.Params, emissionPoolCoins sdk.Coins) (*zetaapp.App, sdk.Context, *tmtypes.ValidatorSet, *authtypes.BaseAccount) { - pk1 := ed25519.GenPrivKey().PubKey() - acc1 := authtypes.NewBaseAccountWithAddress(sdk.AccAddress(pk1.Address())) - // genDelActs and genDelBalances need to have the same addresses - // bondAmount is specified separately , the Balances here are additional tokens for delegators to have in their accounts - genDelActs := make(authtypes.GenesisAccounts, 1) - genDelBalances := make([]banktypes.Balance, 1) - genDelActs[0] = acc1 - genDelBalances[0] = banktypes.Balance{ - Address: acc1.GetAddress().String(), - Coins: emissionPoolCoins, - } - delBondAmount := getaZetaFromString("1000000000000000000000000") - - //genBalances := make([]banktypes.Balance, 1) - //genBalances[0] = banktypes.Balance{ - // Address: emissionsModuleTypes.EmissionsModuleAddress.String(), - // Coins: emissionPoolCoins, - //} - - vset := tmtypes.NewValidatorSet([]*tmtypes.Validator{}) - for i := 0; i < 1; i++ { - privKey := ed25519.GenPrivKey() - pubKey := privKey.PubKey() - val := tmtypes.NewValidator(pubKey, 1) - err := vset.UpdateWithChangeSet([]*tmtypes.Validator{val}) - if err != nil { - panic("Failed to add validator") - } - } - - app := simapp.SetupWithGenesisValSet(t, vset, genDelActs, delBondAmount.AmountOf(config.BaseDenom), params, genDelBalances, nil) - ctx := app.BaseApp.NewContext(false, tmproto.Header{}) - ctx = ctx.WithBlockHeight(app.LastBlockHeight()) - return app, ctx, vset, acc1 -} - -type EmissionTestData struct { - BlockHeight int64 `json:"blockHeight,omitempty"` - BondFactor sdk.Dec `json:"bondFactor"` - ReservesFactor sdk.Dec `json:"reservesFactor"` - DurationFactor string `json:"durationFactor"` -} - -func TestAppModule_GetBlockRewardComponents(t *testing.T) { - - tests := []struct { - name string - startingEmissionPool string - params emissionsModuleTypes.Params - testMaxHeight int64 - inputFilename string - checkValues []EmissionTestData - generateOnly bool - }{ - { - name: "default values", - params: emissionsModuleTypes.DefaultParams(), - startingEmissionPool: "1000000000000000000000000", - testMaxHeight: 300, - inputFilename: "simulations.json", - generateOnly: false, - }, - { - name: "higher starting pool", - params: emissionsModuleTypes.DefaultParams(), - startingEmissionPool: "100000000000000000000000000000000", - testMaxHeight: 300, - inputFilename: "simulations.json", - generateOnly: false, - }, - { - name: "lower starting pool", - params: emissionsModuleTypes.DefaultParams(), - startingEmissionPool: "100000000000000000", - testMaxHeight: 300, - inputFilename: "simulations.json", - generateOnly: false, - }, - { - name: "different distribution percentages", - params: emissionsModuleTypes.Params{ - MaxBondFactor: "1.25", - MinBondFactor: "0.75", - AvgBlockTime: "6.00", - TargetBondRatio: "00.67", - ValidatorEmissionPercentage: "00.10", - ObserverEmissionPercentage: "00.85", - TssSignerEmissionPercentage: "00.05", - DurationFactorConstant: "0.001877876953694702", - }, - startingEmissionPool: "1000000000000000000000000", - testMaxHeight: 300, - inputFilename: "simulations.json", - generateOnly: false, - }, - { - name: "higher block time", - params: emissionsModuleTypes.Params{ - MaxBondFactor: "1.25", - MinBondFactor: "0.75", - AvgBlockTime: "20.00", - TargetBondRatio: "00.67", - ValidatorEmissionPercentage: "00.10", - ObserverEmissionPercentage: "00.85", - TssSignerEmissionPercentage: "00.05", - DurationFactorConstant: "0.1", - }, - startingEmissionPool: "1000000000000000000000000", - testMaxHeight: 300, - inputFilename: "simulations.json", - generateOnly: false, - }, - { - name: "different duration constant", - params: emissionsModuleTypes.Params{ - MaxBondFactor: "1.25", - MinBondFactor: "0.75", - AvgBlockTime: "6.00", - TargetBondRatio: "00.67", - ValidatorEmissionPercentage: "00.10", - ObserverEmissionPercentage: "00.85", - TssSignerEmissionPercentage: "00.05", - DurationFactorConstant: "0.1", - }, - startingEmissionPool: "1000000000000000000000000", - testMaxHeight: 300, - inputFilename: "simulations.json", - generateOnly: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - app, ctx, _, minter := SetupApp(t, tt.params, getaZetaFromString(tt.startingEmissionPool)) - err := app.BankKeeper.SendCoinsFromAccountToModule(ctx, minter.GetAddress(), emissionsModuleTypes.ModuleName, getaZetaFromString(tt.startingEmissionPool)) - assert.NoError(t, err) - GenerateTestDataMaths(app, ctx, tt.testMaxHeight, tt.inputFilename) - defer func(t *testing.T, fp string) { - err := os.RemoveAll(fp) - assert.NoError(t, err) - }(t, tt.inputFilename) - - if tt.generateOnly { - return - } - inputTestData, err := GetInputData(tt.inputFilename) - assert.NoError(t, err) - sort.SliceStable(inputTestData, func(i, j int) bool { return inputTestData[i].BlockHeight < inputTestData[j].BlockHeight }) - startHeight := ctx.BlockHeight() - assert.Equal(t, startHeight, inputTestData[0].BlockHeight, "starting block height should be equal to the first block height in the input data") - for i := startHeight; i < tt.testMaxHeight; i++ { - //The First distribution will occur only when begin-block is triggered - reservesFactor, bondFactor, durationFactor := app.EmissionsKeeper.GetBlockRewardComponents(ctx) - assert.Equal(t, inputTestData[i-1].ReservesFactor, reservesFactor, "reserves factor should be equal to the input data"+fmt.Sprintf(" , block height: %d", i)) - assert.Equal(t, inputTestData[i-1].BondFactor, bondFactor, "bond factor should be equal to the input data"+fmt.Sprintf(" , block height: %d", i)) - assert.Equal(t, inputTestData[i-1].DurationFactor, durationFactor.String(), "duration factor should be equal to the input data"+fmt.Sprintf(" , block height: %d", i)) - emissionsModule.BeginBlocker(ctx, app.EmissionsKeeper) - ctx = ctx.WithBlockHeight(i + 1) - } - }) - } -} - -func GetInputData(fp string) ([]EmissionTestData, error) { - data := []EmissionTestData{} - file, err := filepath.Abs(fp) - if err != nil { - - return nil, err - } - file = filepath.Clean(file) - input, err := ioutil.ReadFile(file) // #nosec G304 - if err != nil { - return nil, err - } - err = json.Unmarshal(input, &data) - if err != nil { - return nil, err - } - formatedData := make([]EmissionTestData, len(data)) - for i, dd := range data { - fl, err := strconv.ParseFloat(dd.DurationFactor, 64) - if err != nil { - return nil, err - } - dd.DurationFactor = fmt.Sprintf("%0.18f", fl) - formatedData[i] = dd - } - return formatedData, nil -} - -func GenerateTestDataMaths(app *zetaapp.App, ctx sdk.Context, testMaxHeight int64, fileName string) { - var generatedTestData []EmissionTestData - reserverCoins := app.BankKeeper.GetBalance(ctx, emissionsModuleTypes.EmissionsModuleAddress, config.BaseDenom) - startHeight := ctx.BlockHeight() - for i := startHeight; i < testMaxHeight; i++ { - reservesFactor := sdk.NewDecFromInt(reserverCoins.Amount) - bondFactor := app.EmissionsKeeper.GetBondFactor(ctx, app.StakingKeeper) - durationFactor := app.EmissionsKeeper.GetDurationFactor(ctx) - blockRewards := reservesFactor.Mul(bondFactor).Mul(durationFactor) - generatedTestData = append(generatedTestData, EmissionTestData{ - BlockHeight: i, - BondFactor: bondFactor, - DurationFactor: durationFactor.String(), - ReservesFactor: reservesFactor, - }) - validatorRewards := sdk.MustNewDecFromStr(app.EmissionsKeeper.GetParams(ctx).ValidatorEmissionPercentage).Mul(blockRewards).TruncateInt() - observerRewards := sdk.MustNewDecFromStr(app.EmissionsKeeper.GetParams(ctx).ObserverEmissionPercentage).Mul(blockRewards).TruncateInt() - tssSignerRewards := sdk.MustNewDecFromStr(app.EmissionsKeeper.GetParams(ctx).TssSignerEmissionPercentage).Mul(blockRewards).TruncateInt() - truncatedRewards := validatorRewards.Add(observerRewards).Add(tssSignerRewards) - reserverCoins = reserverCoins.Sub(sdk.NewCoin(config.BaseDenom, truncatedRewards)) - ctx = ctx.WithBlockHeight(i + 1) - } - GenerateSampleFile(fileName, generatedTestData) -} - -func GenerateSampleFile(fp string, data []EmissionTestData) { - file, _ := json.MarshalIndent(data, "", " ") - _ = ioutil.WriteFile(fp, file, 0600) -} +//TODO : https://github.com/zeta-chain/node/issues/1659 +//func getaZetaFromString(amount string) sdk.Coins { +// emissionPoolInt, _ := sdk.NewIntFromString(amount) +// return sdk.NewCoins(sdk.NewCoin(config.BaseDenom, emissionPoolInt)) +//} +// +//func SetupApp(t *testing.T, params emissionsModuleTypes.Params, emissionPoolCoins sdk.Coins) (*zetaapp.App, sdk.Context, *tmtypes.ValidatorSet, *authtypes.BaseAccount) { +// pk1 := ed25519.GenPrivKey().PubKey() +// acc1 := authtypes.NewBaseAccountWithAddress(sdk.AccAddress(pk1.Address())) +// // genDelActs and genDelBalances need to have the same addresses +// // bondAmount is specified separately , the Balances here are additional tokens for delegators to have in their accounts +// genDelActs := make(authtypes.GenesisAccounts, 1) +// genDelBalances := make([]banktypes.Balance, 1) +// genDelActs[0] = acc1 +// genDelBalances[0] = banktypes.Balance{ +// Address: acc1.GetAddress().String(), +// Coins: emissionPoolCoins, +// } +// delBondAmount := getaZetaFromString("1000000000000000000000000") +// +// //genBalances := make([]banktypes.Balance, 1) +// //genBalances[0] = banktypes.Balance{ +// // Address: emissionsModuleTypes.EmissionsModuleAddress.String(), +// // Coins: emissionPoolCoins, +// //} +// +// vset := tmtypes.NewValidatorSet([]*tmtypes.Validator{}) +// for i := 0; i < 1; i++ { +// privKey := ed25519.GenPrivKey() +// pubKey := privKey.PubKey() +// val := tmtypes.NewValidator(pubKey, 1) +// err := vset.UpdateWithChangeSet([]*tmtypes.Validator{val}) +// if err != nil { +// panic("Failed to add validator") +// } +// } +// +// app := simapp.SetupWithGenesisValSet(t, vset, genDelActs, delBondAmount.AmountOf(config.BaseDenom), params, genDelBalances, nil) +// ctx := app.BaseApp.NewContext(false, tmproto.Header{}) +// ctx = ctx.WithBlockHeight(app.LastBlockHeight()) +// return app, ctx, vset, acc1 +//} +// +//type EmissionTestData struct { +// BlockHeight int64 `json:"blockHeight,omitempty"` +// BondFactor sdk.Dec `json:"bondFactor"` +// ReservesFactor sdk.Dec `json:"reservesFactor"` +// DurationFactor string `json:"durationFactor"` +//} +// +//func TestAppModule_GetBlockRewardComponents(t *testing.T) { +// +// tests := []struct { +// name string +// startingEmissionPool string +// params emissionsModuleTypes.Params +// testMaxHeight int64 +// inputFilename string +// checkValues []EmissionTestData +// generateOnly bool +// }{ +// { +// name: "default values", +// params: emissionsModuleTypes.DefaultParams(), +// startingEmissionPool: "1000000000000000000000000", +// testMaxHeight: 300, +// inputFilename: "simulations.json", +// generateOnly: false, +// }, +// { +// name: "higher starting pool", +// params: emissionsModuleTypes.DefaultParams(), +// startingEmissionPool: "100000000000000000000000000000000", +// testMaxHeight: 300, +// inputFilename: "simulations.json", +// generateOnly: false, +// }, +// { +// name: "lower starting pool", +// params: emissionsModuleTypes.DefaultParams(), +// startingEmissionPool: "100000000000000000", +// testMaxHeight: 300, +// inputFilename: "simulations.json", +// generateOnly: false, +// }, +// { +// name: "different distribution percentages", +// params: emissionsModuleTypes.Params{ +// MaxBondFactor: "1.25", +// MinBondFactor: "0.75", +// AvgBlockTime: "6.00", +// TargetBondRatio: "00.67", +// ValidatorEmissionPercentage: "00.10", +// ObserverEmissionPercentage: "00.85", +// TssSignerEmissionPercentage: "00.05", +// DurationFactorConstant: "0.001877876953694702", +// }, +// startingEmissionPool: "1000000000000000000000000", +// testMaxHeight: 300, +// inputFilename: "simulations.json", +// generateOnly: false, +// }, +// { +// name: "higher block time", +// params: emissionsModuleTypes.Params{ +// MaxBondFactor: "1.25", +// MinBondFactor: "0.75", +// AvgBlockTime: "20.00", +// TargetBondRatio: "00.67", +// ValidatorEmissionPercentage: "00.10", +// ObserverEmissionPercentage: "00.85", +// TssSignerEmissionPercentage: "00.05", +// DurationFactorConstant: "0.1", +// }, +// startingEmissionPool: "1000000000000000000000000", +// testMaxHeight: 300, +// inputFilename: "simulations.json", +// generateOnly: false, +// }, +// { +// name: "different duration constant", +// params: emissionsModuleTypes.Params{ +// MaxBondFactor: "1.25", +// MinBondFactor: "0.75", +// AvgBlockTime: "6.00", +// TargetBondRatio: "00.67", +// ValidatorEmissionPercentage: "00.10", +// ObserverEmissionPercentage: "00.85", +// TssSignerEmissionPercentage: "00.05", +// DurationFactorConstant: "0.1", +// }, +// startingEmissionPool: "1000000000000000000000000", +// testMaxHeight: 300, +// inputFilename: "simulations.json", +// generateOnly: false, +// }, +// } +// +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// app, ctx, _, minter := SetupApp(t, tt.params, getaZetaFromString(tt.startingEmissionPool)) +// err := app.BankKeeper.SendCoinsFromAccountToModule(ctx, minter.GetAddress(), emissionsModuleTypes.ModuleName, getaZetaFromString(tt.startingEmissionPool)) +// assert.NoError(t, err) +// GenerateTestDataMaths(app, ctx, tt.testMaxHeight, tt.inputFilename) +// defer func(t *testing.T, fp string) { +// err := os.RemoveAll(fp) +// assert.NoError(t, err) +// }(t, tt.inputFilename) +// +// if tt.generateOnly { +// return +// } +// inputTestData, err := GetInputData(tt.inputFilename) +// assert.NoError(t, err) +// sort.SliceStable(inputTestData, func(i, j int) bool { return inputTestData[i].BlockHeight < inputTestData[j].BlockHeight }) +// startHeight := ctx.BlockHeight() +// assert.Equal(t, startHeight, inputTestData[0].BlockHeight, "starting block height should be equal to the first block height in the input data") +// for i := startHeight; i < tt.testMaxHeight; i++ { +// //The First distribution will occur only when begin-block is triggered +// reservesFactor, bondFactor, durationFactor := app.EmissionsKeeper.GetBlockRewardComponents(ctx) +// assert.Equal(t, inputTestData[i-1].ReservesFactor, reservesFactor, "reserves factor should be equal to the input data"+fmt.Sprintf(" , block height: %d", i)) +// assert.Equal(t, inputTestData[i-1].BondFactor, bondFactor, "bond factor should be equal to the input data"+fmt.Sprintf(" , block height: %d", i)) +// assert.Equal(t, inputTestData[i-1].DurationFactor, durationFactor.String(), "duration factor should be equal to the input data"+fmt.Sprintf(" , block height: %d", i)) +// emissionsModule.BeginBlocker(ctx, app.EmissionsKeeper) +// ctx = ctx.WithBlockHeight(i + 1) +// } +// }) +// } +//} +// +//func GetInputData(fp string) ([]EmissionTestData, error) { +// data := []EmissionTestData{} +// file, err := filepath.Abs(fp) +// if err != nil { +// +// return nil, err +// } +// file = filepath.Clean(file) +// input, err := ioutil.ReadFile(file) // #nosec G304 +// if err != nil { +// return nil, err +// } +// err = json.Unmarshal(input, &data) +// if err != nil { +// return nil, err +// } +// formatedData := make([]EmissionTestData, len(data)) +// for i, dd := range data { +// fl, err := strconv.ParseFloat(dd.DurationFactor, 64) +// if err != nil { +// return nil, err +// } +// dd.DurationFactor = fmt.Sprintf("%0.18f", fl) +// formatedData[i] = dd +// } +// return formatedData, nil +//} +// +//func GenerateTestDataMaths(app *zetaapp.App, ctx sdk.Context, testMaxHeight int64, fileName string) { +// var generatedTestData []EmissionTestData +// reserverCoins := app.BankKeeper.GetBalance(ctx, emissionsModuleTypes.EmissionsModuleAddress, config.BaseDenom) +// startHeight := ctx.BlockHeight() +// for i := startHeight; i < testMaxHeight; i++ { +// reservesFactor := sdk.NewDecFromInt(reserverCoins.Amount) +// bondFactor := app.EmissionsKeeper.GetBondFactor(ctx, app.StakingKeeper) +// durationFactor := app.EmissionsKeeper.GetDurationFactor(ctx) +// blockRewards := reservesFactor.Mul(bondFactor).Mul(durationFactor) +// generatedTestData = append(generatedTestData, EmissionTestData{ +// BlockHeight: i, +// BondFactor: bondFactor, +// DurationFactor: durationFactor.String(), +// ReservesFactor: reservesFactor, +// }) +// validatorRewards := sdk.MustNewDecFromStr(app.EmissionsKeeper.GetParams(ctx).ValidatorEmissionPercentage).Mul(blockRewards).TruncateInt() +// observerRewards := sdk.MustNewDecFromStr(app.EmissionsKeeper.GetParams(ctx).ObserverEmissionPercentage).Mul(blockRewards).TruncateInt() +// tssSignerRewards := sdk.MustNewDecFromStr(app.EmissionsKeeper.GetParams(ctx).TssSignerEmissionPercentage).Mul(blockRewards).TruncateInt() +// truncatedRewards := validatorRewards.Add(observerRewards).Add(tssSignerRewards) +// reserverCoins = reserverCoins.Sub(sdk.NewCoin(config.BaseDenom, truncatedRewards)) +// ctx = ctx.WithBlockHeight(i + 1) +// } +// GenerateSampleFile(fileName, generatedTestData) +//} +// +//func GenerateSampleFile(fp string, data []EmissionTestData) { +// file, _ := json.MarshalIndent(data, "", " ") +// _ = ioutil.WriteFile(fp, file, 0600) +//} diff --git a/x/emissions/client/tests/observer_rewards_test.go b/x/emissions/client/tests/observer_rewards_test.go index 853e3a3ad8..c8f11d2449 100644 --- a/x/emissions/client/tests/observer_rewards_test.go +++ b/x/emissions/client/tests/observer_rewards_test.go @@ -8,8 +8,10 @@ import ( clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/bank/client/cli" + "github.com/stretchr/testify/suite" "github.com/zeta-chain/zetacore/cmd/zetacored/config" emissionscli "github.com/zeta-chain/zetacore/x/emissions/client/cli" + emissionskeeper "github.com/zeta-chain/zetacore/x/emissions/keeper" emissionstypes "github.com/zeta-chain/zetacore/x/emissions/types" observercli "github.com/zeta-chain/zetacore/x/observer/client/cli" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -59,7 +61,7 @@ func (s *CliTestSuite) TestObserverRewards() { // Duration factor is calculated in the same block,so we need to query based from the committed state at which the distribution is done // Would be cleaner to use `--height` flag, but it is not supported by the ExecTestCLICmd function yet emissionFactors.DurationFactor = resFactorsNewBlocks.DurationFactor - asertValues := CalculateObserverRewards(s.ballots, emissionParams.Params.ObserverEmissionPercentage, emissionFactors.ReservesFactor, emissionFactors.BondFactor, emissionFactors.DurationFactor) + asertValues := CalculateObserverRewards(&s.Suite, s.ballots, emissionParams.Params.ObserverEmissionPercentage, emissionFactors.ReservesFactor, emissionFactors.BondFactor, emissionFactors.DurationFactor) // Assert withdrawable rewards for each validator resAvailable := emissionstypes.QueryShowAvailableEmissionsResponse{} @@ -72,9 +74,11 @@ func (s *CliTestSuite) TestObserverRewards() { } -func CalculateObserverRewards(ballots []*observertypes.Ballot, observerEmissionPercentage, reservesFactor, bondFactor, durationFactor string) map[string]sdkmath.Int { +func CalculateObserverRewards(s *suite.Suite, ballots []*observertypes.Ballot, observerEmissionPercentage, reservesFactor, bondFactor, durationFactor string) map[string]sdkmath.Int { calculatedDistributer := map[string]sdkmath.Int{} - blockRewards := sdk.MustNewDecFromStr(reservesFactor).Mul(sdk.MustNewDecFromStr(bondFactor)).Mul(sdk.MustNewDecFromStr(durationFactor)) + //blockRewards := sdk.MustNewDecFromStr(reservesFactor).Mul(sdk.MustNewDecFromStr(bondFactor)).Mul(sdk.MustNewDecFromStr(durationFactor)) + blockRewards, err := emissionskeeper.CalculateFixedValidatorRewards(emissionstypes.AvgBlockTime) + s.Require().NoError(err) observerRewards := sdk.MustNewDecFromStr(observerEmissionPercentage).Mul(blockRewards).TruncateInt() rewardsDistributer := map[string]int64{} totalRewardsUnits := int64(0) diff --git a/x/emissions/keeper/block_rewards_components.go b/x/emissions/keeper/block_rewards_components.go index cc93fb3111..ab70e13de5 100644 --- a/x/emissions/keeper/block_rewards_components.go +++ b/x/emissions/keeper/block_rewards_components.go @@ -3,11 +3,12 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/zetacore/cmd/zetacored/config" + "github.com/zeta-chain/zetacore/common" "github.com/zeta-chain/zetacore/x/emissions/types" ) func (k Keeper) GetBlockRewardComponents(ctx sdk.Context) (sdk.Dec, sdk.Dec, sdk.Dec) { - reservesFactor := GetReservesFactor(ctx, k.GetBankKeeper()) + reservesFactor := k.GetReservesFactor(ctx) if reservesFactor.LTE(sdk.ZeroDec()) { return sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec() } @@ -55,7 +56,26 @@ func (k Keeper) GetDurationFactor(ctx sdk.Context) sdk.Dec { return fractionNumerator.Quo(fractionDenominator) } -func GetReservesFactor(ctx sdk.Context, keeper types.BankKeeper) sdk.Dec { - reserveAmount := keeper.GetBalance(ctx, types.EmissionsModuleAddress, config.BaseDenom) +func (k Keeper) GetReservesFactor(ctx sdk.Context) sdk.Dec { + reserveAmount := k.GetBankKeeper().GetBalance(ctx, types.EmissionsModuleAddress, config.BaseDenom) return sdk.NewDecFromInt(reserveAmount.Amount) } + +func (k Keeper) GetFixedBlockRewards() (sdk.Dec, error) { + return CalculateFixedValidatorRewards(types.AvgBlockTime) +} + +func CalculateFixedValidatorRewards(avgBlockTimeString string) (sdk.Dec, error) { + azetaAmountTotalRewards, err := common.GetAzetaDecFromAmountInZeta(types.BlockRewardsInZeta) + if err != nil { + return sdk.ZeroDec(), err + } + avgBlockTime, err := sdk.NewDecFromStr(avgBlockTimeString) + if err != nil { + return sdk.ZeroDec(), err + } + numberOfBlocksInAMonth := sdk.NewDec(types.SecsInMonth).Quo(avgBlockTime) + numberOfBlocksTotal := numberOfBlocksInAMonth.Mul(sdk.NewDec(12)).Mul(sdk.NewDec(types.EmissionScheduledYears)) + constantRewardPerBlock := azetaAmountTotalRewards.Quo(numberOfBlocksTotal) + return constantRewardPerBlock, nil +} diff --git a/x/emissions/keeper/block_rewards_components_test.go b/x/emissions/keeper/block_rewards_components_test.go new file mode 100644 index 0000000000..f8d206fbde --- /dev/null +++ b/x/emissions/keeper/block_rewards_components_test.go @@ -0,0 +1,50 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + emissionskeeper "github.com/zeta-chain/zetacore/x/emissions/keeper" +) + +func TestKeeper_CalculateFixedValidatorRewards(t *testing.T) { + tt := []struct { + name string + blockTimeInSecs string + expectedBlockRewards sdk.Dec + }{ + { + name: "Block Time 5.7", + blockTimeInSecs: "5.7", + expectedBlockRewards: sdk.MustNewDecFromStr("9620949074074074074.074070733466756687"), + }, + { + name: "Block Time 6", + blockTimeInSecs: "6", + expectedBlockRewards: sdk.MustNewDecFromStr("10127314814814814814.814814814814814815"), + }, + { + name: "Block Time 3", + blockTimeInSecs: "3", + expectedBlockRewards: sdk.MustNewDecFromStr("5063657407407407407.407407407407407407"), + }, + { + name: "Block Time 2", + blockTimeInSecs: "2", + expectedBlockRewards: sdk.MustNewDecFromStr("3375771604938271604.938271604938271605"), + }, + { + name: "Block Time 8", + blockTimeInSecs: "8", + expectedBlockRewards: sdk.MustNewDecFromStr("13503086419753086419.753086419753086420"), + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + blockRewards, err := emissionskeeper.CalculateFixedValidatorRewards(tc.blockTimeInSecs) + assert.NoError(t, err) + assert.Equal(t, tc.expectedBlockRewards, blockRewards) + }) + } +} diff --git a/x/emissions/types/keys.go b/x/emissions/types/keys.go index e78b0ddade..4be2d268b4 100644 --- a/x/emissions/types/keys.go +++ b/x/emissions/types/keys.go @@ -1,6 +1,7 @@ package types import ( + sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) @@ -23,7 +24,11 @@ const ( MemStoreKey = "mem_emissions" WithdrawableEmissionsKey = "WithdrawableEmissions-value-" - SecsInMonth = 30 * 24 * 60 * 60 + SecsInMonth = 30 * 24 * 60 * 60 + BlockRewardsInZeta = "210000000" + + EmissionScheduledYears = 4 + AvgBlockTime = "5.7" ) func KeyPrefix(p string) []byte { @@ -46,4 +51,5 @@ var ( EmissionsModuleAddress = authtypes.NewModuleAddress(ModuleName) UndistributedObserverRewardsPoolAddress = authtypes.NewModuleAddress(UndistributedObserverRewardsPool) UndistributedTssRewardsPoolAddress = authtypes.NewModuleAddress(UndistributedTssRewardsPool) + BlockReward = sdk.MustNewDecFromStr("9620949074074074074.074070733466756687") ) diff --git a/zetaclient/bitcoin_client.go b/zetaclient/bitcoin_client.go index d802d2b953..5dee82bdb8 100644 --- a/zetaclient/bitcoin_client.go +++ b/zetaclient/bitcoin_client.go @@ -372,10 +372,13 @@ func (ob *BitcoinChainClient) observeInTx() error { // get and update latest block height cnt, err := ob.rpcClient.GetBlockCount() if err != nil { - return fmt.Errorf("observeInTxBTC: error getting block count: %s", err) + return fmt.Errorf("observeInTxBTC: error getting block number: %s", err) } if cnt < 0 { - return fmt.Errorf("observeInTxBTC: block count is negative: %d", cnt) + return fmt.Errorf("observeInTxBTC: block number is negative: %d", cnt) + } + if cnt < ob.GetLastBlockHeight() { + return fmt.Errorf("observeInTxBTC: block number should not decrease: current %d last %d", cnt, ob.GetLastBlockHeight()) } ob.SetLastBlockHeight(cnt) @@ -439,7 +442,7 @@ func (ob *BitcoinChainClient) observeInTx() error { ob.logger.WatchInTx.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: BTC deposit detected and reported: PostVoteInbound zeta tx: %s ballot %s", zetaHash, ballot) + ob.logger.WatchInTx.Info().Msgf("observeInTxBTC: PostVoteInbound zeta tx hash: %s inTx %s ballot %s", zetaHash, inTx.TxHash, ballot) } } @@ -633,11 +636,12 @@ func FilterAndParseIncomingTx( } inTx, err := GetBtcEvent(tx, targetAddress, blockNumber, logger, chainID) if err != nil { - logger.Error().Err(err).Msg("error getting btc event") + logger.Error().Err(err).Msgf("FilterAndParseIncomingTx: error getting btc event for tx %s in block %d", tx.Txid, blockNumber) continue } 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 diff --git a/zetaclient/broadcast.go b/zetaclient/broadcast.go index 97870d3791..a40fc04743 100644 --- a/zetaclient/broadcast.go +++ b/zetaclient/broadcast.go @@ -25,6 +25,12 @@ const ( DefaultBaseGasPrice = 1_000_000 ) +var ( + // paying 50% more than the current base gas price to buffer for potential block-by-block + // gas price increase due to EIP1559 feemarket on ZetaChain + bufferMultiplier = sdktypes.MustNewDecFromStr("1.5") +) + // Broadcast Broadcasts tx to metachain. Returns txHash and error func (b *ZetaCoreBridge) Broadcast(gaslimit uint64, authzWrappedMsg sdktypes.Msg, authzSigner AuthZSigner) (string, error) { b.broadcastLock.Lock() @@ -44,7 +50,7 @@ func (b *ZetaCoreBridge) Broadcast(gaslimit uint64, authzWrappedMsg sdktypes.Msg } reductionRate := sdktypes.MustNewDecFromStr(ante.GasPriceReductionRate) // multiply gas price by the system tx reduction rate - adjustedBaseGasPrice := sdktypes.NewDec(baseGasPrice).Mul(reductionRate) + adjustedBaseGasPrice := sdktypes.NewDec(baseGasPrice).Mul(reductionRate).Mul(bufferMultiplier) if blockHeight > b.blockHeight { b.blockHeight = blockHeight diff --git a/zetaclient/btc_signer.go b/zetaclient/btc_signer.go index ba493a9fce..245f4039ff 100644 --- a/zetaclient/btc_signer.go +++ b/zetaclient/btc_signer.go @@ -23,19 +23,11 @@ import ( const ( maxNoOfInputsPerTx = 20 - consolidationRank = 10 // the rank below (or equal to) which we consolidate UTXOs + 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) ) -var ( - outTxBytesMin uint64 - outTxBytesMax uint64 -) - -func init() { - outTxBytesMin = EstimateSegWitTxSize(2, 3) // 403B, estimated size for a 2-input, 3-output SegWit tx - outTxBytesMax = EstimateSegWitTxSize(21, 3) // 3234B, estimated size for a 21-input, 3-output SegWit tx -} - type BTCSigner struct { tssSigner TSSSigner rpcClient BTCRPCClient @@ -114,9 +106,9 @@ func (signer *BTCSigner) SignWithdrawTx( // size checking // #nosec G701 always positive - txSize := uint64(tx.SerializeSize()) - if txSize > sizeLimit { // ZRC20 'withdraw' charged less fee from end user - signer.logger.Info().Msgf("sizeLimit %d is less than txSize %d for nonce %d", sizeLimit, txSize, nonce) + 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) } if txSize < outTxBytesMin { // outbound shouldn't be blocked a low sizeLimit signer.logger.Warn().Msgf("txSize %d is less than outTxBytesMin %d; use outTxBytesMin", txSize, outTxBytesMin) @@ -275,18 +267,7 @@ func (signer *BTCSigner) TryProcessOutTx( return } myid := zetaBridge.GetKeys().GetAddress() - // Early return if the send is already processed - // FIXME: handle revert case outboundTxTssNonce := params.OutboundTxTssNonce - included, confirmed, err := btcClient.IsSendOutTxProcessed(cctx.Index, outboundTxTssNonce, common.CoinType_Gas, logger) - if err != nil { - logger.Error().Err(err).Msgf("cannot check if send %s is processed", cctx.Index) - return - } - if included || confirmed { - logger.Info().Msgf("CCTX %s already processed; exit signer", outTxID) - return - } sizelimit := params.OutboundTxGasLimit gasprice, ok := new(big.Int).SetString(params.OutboundTxGasPrice, 10) diff --git a/zetaclient/btc_signer_test.go b/zetaclient/btc_signer_test.go index 3f40ad9de0..75d039664b 100644 --- a/zetaclient/btc_signer_test.go +++ b/zetaclient/btc_signer_test.go @@ -9,6 +9,7 @@ import ( "sync" "testing" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" @@ -30,6 +31,31 @@ 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 @@ -218,11 +244,10 @@ func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, []b func addTxInputs(t *testing.T, tx *wire.MsgTx, txids []string) { preTxSize := tx.SerializeSize() - assert.Equal(t, bytesEmptyTx, preTxSize) - for i, txid := range txids { + for _, txid := range txids { hash, err := chainhash.NewHashFromStr(txid) assert.Nil(t, err) - outpoint := wire.NewOutPoint(hash, uint32(i%3)) + outpoint := wire.NewOutPoint(hash, uint32(rand.Intn(100))) txIn := wire.NewTxIn(outpoint, nil, nil) tx.AddTxIn(txIn) assert.Equal(t, bytesPerInput, tx.SerializeSize()-preTxSize) @@ -306,13 +331,12 @@ func TestP2WPHSize2In3Out(t *testing.T) { // Payer sign the redeeming transaction. signTx(t, tx, payerScript, privateKey) - // Estimate the tx size + // Estimate the tx size in vByte // #nosec G701 always positive - txSize := uint64(tx.SerializeSize()) - sizeEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3) - assert.Equal(t, outTxBytesMin, sizeEstimated) - assert.True(t, outTxBytesMin >= txSize) - assert.True(t, outTxBytesMin-txSize <= 2) // 2 witness may vary + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3) + assert.Equal(t, vBytes, vBytesEstimated) + assert.Equal(t, vBytes, outTxBytesMin) } func TestP2WPHSize21In3Out(t *testing.T) { @@ -320,35 +344,9 @@ func TestP2WPHSize21In3Out(t *testing.T) { privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params) _, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params) - // 21 example UTXO txids to use in the test. - utxosTxids := []string{ - "c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e", - "54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa", - "b18a55a34319cfbedebfcfe1a80fef2b92ad8894d06caf8293a0344824c2cfbc", - "969fb309a4df7c299972700da788b5d601c0c04bab4ab46fff79d0335a7d75de", - "6c71913061246ffc20e268c1b0e65895055c36bfbf1f8faf92dcad6f8242121e", - "ba6d6e88cb5a97556684a1232719a3ffe409c5c9501061e1f59741bc412b3585", - "69b56c3c8c5d1851f9eaec256cd49f290b477a5d43e2aef42ef25d3c1d9f4b33", - "b87effd4cb46fe1a575b5b1ba0289313dc9b4bc9e615a3c6cbc0a14186921fdf", - "3135433054523f5e220621c9e3d48efbbb34a6a2df65635c2a3e7d462d3e1cda", - "8495c22a9ce6359ab53aa048c13b41c64fdf5fe141f516ba2573cc3f9313f06e", - "f31583544b475370d7b9187c9a01b92e44fb31ac5fcfa7fc55565ac64043aa9a", - "c03d55f9f717c1df978623e2e6b397b720999242f9ead7db9b5988fee3fb3933", - "ee55688439b47a5410cdc05bac46be0094f3af54d307456fdfe6ba8caf336e0b", - "61895f86c70f0bc3eef55d9a00347b509fa90f7a344606a9774be98a3ee9e02a", - "ffabb401a19d04327bd4a076671d48467dbcde95459beeab23df21686fd01525", - "b7e1c03b9b73e4e90fc06da893072c5604203c49e66699acbb2f61485d822981", - "185614d21973990138e478ce10e0a4014352df58044276d4e4c0093aa140f482", - "4a2800f13d15dc0c82308761d6fe8f6d13b65e42d7ca96a42a3a7048830e8c55", - "fb98f52e91db500735b185797cebb5848afbfe1289922d87e03b98c3da5b85ef", - "7901c5e36d9e8456ac61b29b82048650672a889596cbd30a9f8910a589ffc5b3", - "6bcd0850fd2fa1404290ed04d78d4ae718414f16d4fbfd344951add8dcf60326", - } - // Create a new transaction and add inputs tx := wire.NewMsgTx(wire.TxVersion) - assert.Equal(t, bytesEmptyTx, tx.SerializeSize()) - addTxInputs(t, tx, utxosTxids) + addTxInputs(t, tx, exampleTxids) // Add P2WPKH outputs addTxOutputs(t, tx, payerScript, payeeScript) @@ -356,13 +354,48 @@ func TestP2WPHSize21In3Out(t *testing.T) { // Payer sign the redeeming transaction. signTx(t, tx, payerScript, privateKey) - // Estimate the tx size + // Estimate the tx size in vByte // #nosec G701 always positive - txSize := uint64(tx.SerializeSize()) - sizeEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3) - assert.Equal(t, outTxBytesMax, sizeEstimated) - assert.True(t, outTxBytesMax >= txSize) - assert.True(t, outTxBytesMax-txSize <= 21) // 21 witness may vary + vError := uint64(21 / 4) // 5 vBytes error tolerance + vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor) + vBytesEstimated := EstimateSegWitTxSize(uint64(len(exampleTxids)), 3) + assert.Equal(t, vBytesEstimated, outTxBytesMax) + if vBytes > vBytesEstimated { + assert.True(t, vBytes-vBytesEstimated <= vError) + } else { + assert.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 { + assert.True(t, vBytes-vBytesEstimated <= vError) + //fmt.Printf("%d error percentage: %.2f%%\n", float64(vBytes-vBytesEstimated)/float64(vBytes)*100) + } else { + assert.True(t, vBytesEstimated-vBytes <= vError) + //fmt.Printf("error percentage: %.2f%%\n", float64(vBytesEstimated-vBytes)/float64(vBytes)*100) + } + } } func TestP2WPHSizeBreakdown(t *testing.T) { @@ -373,14 +406,14 @@ func TestP2WPHSizeBreakdown(t *testing.T) { fmt.Printf("1 input, 1 output: %d\n", sz) txSizeDepositor := SegWitTxSizeDepositor() - assert.Equal(t, uint64(149), txSizeDepositor) + assert.Equal(t, uint64(68), txSizeDepositor) txSizeWithdrawer := SegWitTxSizeWithdrawer() - assert.Equal(t, uint64(254), txSizeWithdrawer) - assert.Equal(t, txSize2In3Out, txSizeDepositor+txSizeWithdrawer) // 403 = 149 + 254 + assert.Equal(t, uint64(171), txSizeWithdrawer) + assert.Equal(t, txSize2In3Out, txSizeDepositor+txSizeWithdrawer) // 239 = 68 + 171 - depositFee := DepositorFee(5) - assert.Equal(t, depositFee, 0.00000745) + depositFee := DepositorFee(20) + assert.Equal(t, depositFee, 0.00001360) } // helper function to create a new BitcoinChainClient diff --git a/zetaclient/evm_client.go b/zetaclient/evm_client.go index 36d5ee0570..a4cf459c8b 100644 --- a/zetaclient/evm_client.go +++ b/zetaclient/evm_client.go @@ -1,6 +1,7 @@ package zetaclient import ( + "bytes" "context" "fmt" "math" @@ -8,6 +9,7 @@ import ( "os" "sort" "strconv" + "strings" "sync" "sync/atomic" "time" @@ -22,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rlp" lru "github.com/hashicorp/golang-lru" + "github.com/onrik/ethrpc" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -70,6 +73,7 @@ type EVMChainClient struct { *ChainMetrics chain common.Chain evmClient EVMRPCClient + evmClientAlternate *ethrpc.EthRPC // a fallback rpc client KlaytnClient KlaytnRPCClient zetaClient ZetaCoreBridger Tss TSSSigner @@ -92,7 +96,9 @@ type EVMChainClient struct { params observertypes.ChainParams ts *TelemetryServer - BlockCache *lru.Cache + blockCache *lru.Cache + blockCacheV3 *lru.Cache // blockCacheV3 caches blocks containing type-3 (BlobTxType) transactions + headerCache *lru.Cache } var _ ChainClient = (*EVMChainClient)(nil) @@ -146,12 +152,24 @@ func NewEVMChainClient( return nil, err } ob.evmClient = client + ob.evmClientAlternate = ethrpc.NewEthRPC(evmCfg.Endpoint) - ob.BlockCache, err = lru.New(1000) + // 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") return nil, err } + ob.blockCacheV3, err = lru.New(1000) + if err != nil { + ob.logger.ChainLogger.Error().Err(err).Msg("failed to create block cache v3") + return nil, err + } + ob.headerCache, err = lru.New(1000) + if err != nil { + ob.logger.ChainLogger.Error().Err(err).Msg("failed to create header cache") + return nil, err + } if ob.chain.IsKlaytnChain() { client, err := Dial(evmCfg.Endpoint) @@ -756,26 +774,6 @@ func (ob *EVMChainClient) checkConfirmedTx(txHash string, nonce uint64) (*ethtyp return nil, nil, false } - // cross-check receipt against the block - block, err := ob.GetBlockByNumberCached(receipt.BlockNumber.Uint64()) - if err != nil { - log.Error().Err(err).Msgf("confirmTxByHash: GetBlockByNumberCached error, txHash %s nonce %d block %d", - txHash, nonce, receipt.BlockNumber) - return nil, nil, false - } - // #nosec G701 non negative value - if receipt.TransactionIndex >= uint(len(block.Transactions())) { - log.Error().Msgf("confirmTxByHash: transaction index %d out of range [0, %d), txHash %s nonce %d block %d", - receipt.TransactionIndex, len(block.Transactions()), txHash, nonce, receipt.BlockNumber) - return nil, nil, false - } - txAtIndex := block.Transactions()[receipt.TransactionIndex] - if txAtIndex.Hash() != transaction.Hash() { - log.Error().Msgf("confirmTxByHash: transaction at index %d has different hash %s, txHash %s nonce %d block %d", - receipt.TransactionIndex, txAtIndex.Hash().Hex(), txHash, nonce, receipt.BlockNumber) - return nil, nil, false - } - // check confirmations if !ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()) { log.Debug().Msgf("confirmTxByHash: txHash %s nonce %d included but not confirmed: receipt block %d, current block %d", @@ -783,9 +781,49 @@ func (ob *EVMChainClient) checkConfirmedTx(txHash string, nonce uint64) (*ethtyp return nil, nil, false } + // cross-check tx inclusion against the block + // Note: a guard for false BlockNumber in receipt. The blob-carrying tx won't come here + err = ob.checkTxInclusion(transaction, receipt.BlockNumber.Uint64(), receipt.TransactionIndex) + if err != nil { + log.Error().Err(err).Msgf("confirmTxByHash: checkTxInclusion error for txHash %s nonce %d", txHash, nonce) + return nil, nil, false + } + return receipt, transaction, true } +// checkTxInclusion returns nil only if tx is included in the block at blockNumber and txIndex +func (ob *EVMChainClient) checkTxInclusion(tx *ethtypes.Transaction, blockNumber uint64, txIndex uint) error { + block, blockRPC, fallBack, _, err := ob.GetBlockByNumberCached(blockNumber) + if err != nil { + return fmt.Errorf("GetBlockByNumberCached error for block %d txHash %s nonce %d: %w", blockNumber, tx.Hash(), tx.Nonce(), err) + } + if !fallBack { + // #nosec G701 non negative value + if txIndex >= uint(len(block.Transactions())) { + return fmt.Errorf("transaction index %d out of range [0, %d), txHash %s nonce %d block %d", + txIndex, len(block.Transactions()), tx.Hash(), tx.Nonce(), blockNumber) + } + txAtIndex := block.Transactions()[txIndex] + if txAtIndex.Hash() != tx.Hash() { + return fmt.Errorf("transaction at index %d has different hash %s, txHash %s nonce %d block %d", + txIndex, txAtIndex.Hash().Hex(), tx.Hash(), tx.Nonce(), blockNumber) + } + } else { // fell back on ETH RPC as ethclient failed to parse the block + // #nosec G701 non negative value + if txIndex >= uint(len(blockRPC.Transactions)) { + return fmt.Errorf("transaction index %d out of range [0, %d), txHash %s nonce %d block %d", + txIndex, len(block.Transactions()), tx.Hash(), tx.Nonce(), blockNumber) + } + txAtIndex := blockRPC.Transactions[txIndex] + if ethcommon.HexToHash(txAtIndex.Hash) != tx.Hash() { + return fmt.Errorf("transaction at index %d has different hash %s, txHash %s nonce %d block %d", + txIndex, txAtIndex.Hash, tx.Hash(), tx.Nonce(), blockNumber) + } + } + return nil +} + // SetLastBlockHeightScanned set last block height scanned (not necessarily caught up with external block; could be slow/paused) func (ob *EVMChainClient) SetLastBlockHeightScanned(height uint64) { atomic.StoreUint64(&ob.lastBlockScanned, height) @@ -863,12 +901,12 @@ func (ob *EVMChainClient) postBlockHeader(tip uint64) error { return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) } - block, err := ob.GetBlockByNumberCached(bn) + header, err := ob.GetBlockHeaderCached(bn) if err != nil { ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) return err } - headerRLP, err := rlp.EncodeToBytes(block.Header()) + headerRLP, err := rlp.EncodeToBytes(header) if err != nil { ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) return err @@ -876,8 +914,8 @@ func (ob *EVMChainClient) postBlockHeader(tip uint64) error { _, err = ob.zetaClient.PostAddBlockHeader( ob.chain.ChainId, - block.Hash().Bytes(), - block.Number().Int64(), + header.Hash().Bytes(), + header.Number.Int64(), common.NewEthereumHeader(headerRLP), ) if err != nil { @@ -902,6 +940,9 @@ func (ob *EVMChainClient) observeInTX(sampledLogger zerolog.Logger) error { if err != nil { return err } + if blockNumber < ob.GetLastBlockHeight() { + return fmt.Errorf("observeInTX: block number should not decrease: current %d last %d", blockNumber, ob.GetLastBlockHeight()) + } ob.SetLastBlockHeight(blockNumber) // increment prom counter @@ -1159,48 +1200,39 @@ func (ob *EVMChainClient) observeTssRecvd(startBlock, toBlock uint64, flags obse } // TODO: we can track the total number of 'getBlockByNumber' RPC calls made - block, err := ob.GetBlockByNumberCached(bn) + block, blockRPC, fallBack, skip, err := ob.GetBlockByNumberCached(bn) if err != nil { + if skip { + ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("observeTssRecvd: skip block %d for chain %d", bn, ob.chain.ChainId) + continue + } ob.logger.ExternalChainWatcher.Error().Err(err).Msgf("observeTssRecvd: error getting block %d for chain %d", bn, ob.chain.ChainId) return bn - 1 // we have to re-scan from this block next time } - for _, tx := range block.Transactions() { - if tx.To() == nil { - continue - } - - if *tx.To() == tssAddress { - receipt, err := ob.evmClient.TransactionReceipt(context.Background(), tx.Hash()) - if err != nil { - ob.logger.ExternalChainWatcher.Err(err).Msgf( - "observeTssRecvd: TransactionReceipt error for tx %s chain %d", tx.Hash().Hex(), ob.chain.ChainId) - return bn - 1 // we have to re-scan this block next time - } - if receipt.Status != 1 { // 1: successful, 0: failed - ob.logger.ExternalChainWatcher.Info().Msgf("observeTssRecvd: tx %s chain %d failed; don't act", tx.Hash().Hex(), ob.chain.ChainId) - continue - } - - sender, err := ob.GetTransactionSender(tx, block.Hash(), receipt.TransactionIndex) - if err != nil { - ob.logger.ExternalChainWatcher.Err(err).Msgf( - "observeTssRecvd: GetTransactionSender error for tx %s chain %d", tx.Hash().Hex(), ob.chain.ChainId) - return bn - 1 // we have to re-scan this block next time - } - - msg := ob.GetInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) - if msg == nil { - continue + if !fallBack { + for _, tx := range block.Transactions() { + if tx.To() != nil && *tx.To() == tssAddress { + if ok := ob.processIntxToTss(tx, bn, block.Hash()); !ok { + return bn - 1 // we have to re-scan this block next time + } } - zetaHash, ballot, err := ob.zetaClient.PostVoteInbound(PostVoteInboundGasLimit, PostVoteInboundExecutionGasLimit, msg) - if err != nil { - ob.logger.ExternalChainWatcher.Error().Err(err).Msgf( - "observeTssRecvd: error posting to zeta core for tx %s at height %d for chain %d", tx.Hash().Hex(), bn, ob.chain.ChainId) - return bn - 1 // we have to re-scan this block next time - } else if zetaHash != "" { - ob.logger.ExternalChainWatcher.Info().Msgf( - "observeTssRecvd: gas asset deposit detected in tx %s at height %d for chain %d, PostVoteInbound zeta tx: %s ballot %s", - tx.Hash().Hex(), bn, ob.chain.ChainId, zetaHash, ballot) + } + } else { // fell back on ETH RPC as ethclient failed to parse the block + ob.logger.ExternalChainWatcher.Info().Msgf("observeTssRecvd: processing block %d using fallback for chain %d", bn, ob.chain.ChainId) + for _, txRPC := range blockRPC.Transactions { + if ethcommon.HexToAddress(txRPC.To) == tssAddress { + tx, _, err := ob.evmClient.TransactionByHash(context.Background(), ethcommon.HexToHash(txRPC.Hash)) + if err != nil { + if strings.Contains(err.Error(), "transaction type not supported") { + ob.logger.ExternalChainWatcher.Err(err).Msgf( + "observeTssRecvd: transaction type not supported for tx %s chain %d", txRPC.Hash, ob.chain.ChainId) + continue // skip blob-carrying tx to TSS address + } + return bn - 1 // we have to re-scan this block next time + } + if ok := ob.processIntxToTss(tx, bn, ethcommon.HexToHash(blockRPC.Hash)); !ok { + return bn - 1 // we have to re-scan this block next time + } } } } @@ -1209,6 +1241,48 @@ func (ob *EVMChainClient) observeTssRecvd(startBlock, toBlock uint64, flags obse return toBlock } +// processIntxToTss processes the incoming tx to TSS address and posts to zetacore +// returns true if the tx is successfully processed, false otherwise +func (ob *EVMChainClient) processIntxToTss(tx *ethtypes.Transaction, bn uint64, blockHash ethcommon.Hash) bool { + receipt, err := ob.evmClient.TransactionReceipt(context.Background(), tx.Hash()) + if err != nil { + ob.logger.ExternalChainWatcher.Err(err).Msgf( + "processIntxToTss: TransactionReceipt error for tx %s chain %d", tx.Hash().Hex(), ob.chain.ChainId) + return false // we have to re-scan this block next time + } + if receipt.Status != 1 { // 1: successful, 0: failed + ob.logger.ExternalChainWatcher.Info().Msgf("processIntxToTss: tx %s chain %d failed; don't act", tx.Hash().Hex(), ob.chain.ChainId) + return true // skip failed tx + } + if bytes.Equal(tx.Data(), []byte(DonationMessage)) { + ob.logger.ExternalChainWatcher.Info().Msgf( + "processIntxToTss: thank you rich folk for your donation!: %s chain %d", tx.Hash().Hex(), ob.chain.ChainId) + return true // skip donation tx + } + sender, err := ob.GetTransactionSender(tx, blockHash, receipt.TransactionIndex) + if err != nil { + ob.logger.ExternalChainWatcher.Err(err).Msgf( + "processIntxToTss: GetTransactionSender error for tx %s chain %d", tx.Hash().Hex(), ob.chain.ChainId) + return false // we have to re-scan this block next time + } + + msg := ob.GetInboundVoteMsgForTokenSentToTSS(tx, sender, bn) + if msg == nil { + return true // should never happen, always non-nil + } + zetaHash, ballot, err := ob.zetaClient.PostVoteInbound(PostVoteInboundGasLimit, PostVoteInboundExecutionGasLimit, msg) + if err != nil { + ob.logger.ExternalChainWatcher.Error().Err(err).Msgf( + "processIntxToTss: error posting to zeta core for tx %s at height %d for chain %d", tx.Hash().Hex(), bn, ob.chain.ChainId) + return false // we have to re-scan this block next time + } else if zetaHash != "" { + ob.logger.ExternalChainWatcher.Info().Msgf( + "processIntxToTss: gas asset deposit detected in tx %s at height %d for chain %d, PostSend zeta tx: %s ballot %s", + tx.Hash().Hex(), bn, ob.chain.ChainId, zetaHash, ballot) + } + return true +} + func (ob *EVMChainClient) WatchGasPrice() { ob.logger.WatchGasPrice.Info().Msg("WatchGasPrice starting...") err := ob.PostGasPrice() @@ -1411,15 +1485,45 @@ func (ob *EVMChainClient) GetTxID(nonce uint64) string { return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) } -func (ob *EVMChainClient) GetBlockByNumberCached(blockNumber uint64) (*ethtypes.Block, error) { - if block, ok := ob.BlockCache.Get(blockNumber); ok { - return block.(*ethtypes.Block), nil +func (ob *EVMChainClient) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, error) { + if header, ok := ob.headerCache.Get(blockNumber); ok { + return header.(*ethtypes.Header), nil } - block, err := ob.evmClient.BlockByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) + header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) if err != nil { return nil, err } - ob.BlockCache.Add(blockNumber, block) - ob.BlockCache.Add(block.Hash(), block) - return block, nil + ob.headerCache.Add(blockNumber, header) + return header, nil +} + +// GetBlockByNumberCached get block by number from cache +// returns block, ethrpc.Block, isFallback, isSkip, error +func (ob *EVMChainClient) GetBlockByNumberCached(blockNumber uint64) (*ethtypes.Block, *ethrpc.Block, bool, bool, error) { + if block, ok := ob.blockCache.Get(blockNumber); ok { + return block.(*ethtypes.Block), nil, false, false, nil + } + if block, ok := ob.blockCacheV3.Get(blockNumber); ok { + return nil, block.(*ethrpc.Block), true, false, nil + } + block, err := ob.evmClient.BlockByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) + if err != nil { + if strings.Contains(err.Error(), "block header indicates no transactions") { + return nil, nil, false, true, err // it's ok skip empty block + } else if strings.Contains(err.Error(), "transaction type not supported") { + if blockNumber > math.MaxInt32 { + return nil, nil, true, false, fmt.Errorf("block number %d is too large", blockNumber) + } + // #nosec G701 always in range, checked above + rpcBlock, err := ob.evmClientAlternate.EthGetBlockByNumber(int(blockNumber), true) + if err != nil { + return nil, nil, true, false, err // fall back on ethRPC but still fail + } + ob.blockCacheV3.Add(blockNumber, rpcBlock) + return nil, rpcBlock, true, false, nil // fall back on ethRPC without error + } + return nil, nil, false, false, err + } + ob.blockCache.Add(blockNumber, block) + return block, nil, false, false, nil } diff --git a/zetaclient/tss_signer.go b/zetaclient/tss_signer.go index dd75835024..2bb59a5550 100644 --- a/zetaclient/tss_signer.go +++ b/zetaclient/tss_signer.go @@ -32,6 +32,10 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/metrics" ) +const ( + envFlagPostBlame = "POST_BLAME" +) + type TSSKey struct { PubkeyInBytes []byte // FIXME: compressed pubkey? PubkeyInBech32 string // FIXME: same above @@ -200,13 +204,16 @@ func (tss *TSS) Sign(digest []byte, height uint64, nonce uint64, chain *common.C if ksRes.Status == thorcommon.Fail { log.Warn().Msgf("keysign status FAIL posting blame to core, blaming node(s): %#v", ksRes.Blame.BlameNodes) - digest := hex.EncodeToString(digest) - index := observertypes.GetBlameIndex(chain.ChainId, nonce, digest, height) - - zetaHash, err := tss.CoreBridge.PostBlameData(&ksRes.Blame, chain.ChainId, index) - if err != nil { - log.Error().Err(err).Msg("error sending blame data to core") - return [65]byte{}, err + // post blame data if enabled + if IsEnvFlagEnabled(envFlagPostBlame) { + digest := hex.EncodeToString(digest) + index := observertypes.GetBlameIndex(chain.ChainId, nonce, digest, height) + zetaHash, err := tss.CoreBridge.PostBlameData(&ksRes.Blame, chain.ChainId, index) + if err != nil { + log.Error().Err(err).Msg("error sending blame data to core") + return [65]byte{}, err + } + log.Info().Msgf("keysign posted blame data tx hash: %s", zetaHash) } // Increment Blame counter @@ -218,8 +225,6 @@ func (tss *TSS) Sign(digest []byte, height uint64, nonce uint64, chain *common.C } counter.Inc() } - - log.Info().Msgf("keysign posted blame data tx hash: %s", zetaHash) } signature := ksRes.Signatures @@ -273,13 +278,17 @@ func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chain * if ksRes.Status == thorcommon.Fail { log.Warn().Msg("keysign status FAIL posting blame to core") - digest := combineDigests(digestBase64) - index := observertypes.GetBlameIndex(chain.ChainId, nonce, hex.EncodeToString(digest), height) - zetaHash, err := tss.CoreBridge.PostBlameData(&ksRes.Blame, chain.ChainId, index) - if err != nil { - log.Error().Err(err).Msg("error sending blame data to core") - return [][65]byte{}, err + // post blame data if enabled + if IsEnvFlagEnabled(envFlagPostBlame) { + digest := combineDigests(digestBase64) + index := observertypes.GetBlameIndex(chain.ChainId, nonce, hex.EncodeToString(digest), height) + zetaHash, err := tss.CoreBridge.PostBlameData(&ksRes.Blame, chain.ChainId, index) + if err != nil { + log.Error().Err(err).Msg("error sending blame data to core") + return [][65]byte{}, err + } + log.Info().Msgf("keysign posted blame data tx hash: %s", zetaHash) } // Increment Blame counter @@ -291,8 +300,6 @@ func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chain * } counter.Inc() } - - log.Info().Msgf("keysign posted blame data tx hash: %s", zetaHash) } signatures := ksRes.Signatures diff --git a/zetaclient/utils.go b/zetaclient/utils.go index 5a0fcd9ab2..06f701a20d 100644 --- a/zetaclient/utils.go +++ b/zetaclient/utils.go @@ -9,11 +9,14 @@ import ( "fmt" "math" "math/big" + "os" "strings" "time" sdkmath "cosmossdk.io/math" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" @@ -36,22 +39,23 @@ const ( ) var ( - BtcOutTxBytesMin uint64 - BtcOutTxBytesMax uint64 BtcOutTxBytesDepositor uint64 BtcOutTxBytesWithdrawer uint64 BtcDepositorFeeMin float64 ) func init() { - BtcOutTxBytesMin = EstimateSegWitTxSize(2, 3) // 403B, estimated size for a 2-input, 3-output SegWit tx - BtcOutTxBytesMax = EstimateSegWitTxSize(21, 3) // 3234B, estimated size for a 21-input, 3-output SegWit tx - BtcOutTxBytesDepositor = SegWitTxSizeDepositor() // 149B, the outtx size incurred by the depositor - BtcOutTxBytesWithdrawer = SegWitTxSizeWithdrawer() // 254B, the outtx size incurred by the withdrawer + BtcOutTxBytesDepositor = SegWitTxSizeDepositor() // 68vB, the outtx size incurred by the depositor + BtcOutTxBytesWithdrawer = SegWitTxSizeWithdrawer() // 171vB, the outtx size incurred by the withdrawer - // depositor fee calculation is based on a fixed fee rate of 5 sat/byte just for simplicity. + // 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. - BtcDepositorFeeMin = DepositorFee(5) // 0.00000745 (5 * 149B / 100000000), the minimum deposit fee in BTC for 5 sat/byte + BtcDepositorFeeMin = DepositorFee(20) // 0.00001360 (20 * 68vB / 100000000), the minimum deposit fee in BTC for 20 sat/byte +} + +func IsEnvFlagEnabled(flag string) bool { + value := os.Getenv(flag) + return value == "true" || value == "1" } func PrettyPrintStruct(val interface{}) (string, error) { @@ -73,27 +77,40 @@ func FeeRateToSatPerByte(rate float64) *big.Int { return new(big.Int).Div(satPerKB, big.NewInt(bytesPerKB)) } -// EstimateSegWitTxSize estimates SegWit tx size +// 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 in vByte 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 - return bytesEmptyTx + bytesInput + bytesOutput + bytesWitness + + // 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 (149B) incurred by the depositor +// SegWitTxSizeDepositor returns SegWit tx size (68vB) incurred by the depositor func SegWitTxSizeDepositor() uint64 { - return bytesPerInput + bytesPerWitness + return bytesPerInput + bytesPerWitness/blockchain.WitnessScaleFactor } -// SegWitTxSizeWithdrawer returns SegWit tx size (254B) incurred by the withdrawer +// 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 bytesEmptyTx + bytesInput + bytesOutput + bytes1stWitness + return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor } // DepositorFee calculates the depositor fee in BTC for a given sat/byte fee rate diff --git a/zetaclient/zetacore_observer.go b/zetaclient/zetacore_observer.go index 333c6ce262..6fae898ece 100644 --- a/zetaclient/zetacore_observer.go +++ b/zetaclient/zetacore_observer.go @@ -257,7 +257,7 @@ func (co *CoreObserver) scheduleCctxEVM( // try confirming the outtx included, _, err := ob.IsSendOutTxProcessed(cctx.Index, params.OutboundTxTssNonce, params.CoinType, co.logger.ZetaChainWatcher) if err != nil { - co.logger.ZetaChainWatcher.Error().Err(err).Msgf("scheduleCctxEVM: IsSendOutTxProcessed faild for chain %d", chainID) + co.logger.ZetaChainWatcher.Error().Err(err).Msgf("scheduleCctxEVM: IsSendOutTxProcessed faild for chain %d nonce %d", chainID, nonce) continue } if included { @@ -320,6 +320,8 @@ func (co *CoreObserver) scheduleCctxBTC( co.logger.ZetaChainWatcher.Error().Msgf("scheduleCctxBTC: chain client is not a bitcoin client") return } + // #nosec G701 positive + interval := uint64(ob.GetChainParams().OutboundTxScheduleInterval) lookahead := ob.GetChainParams().OutboundTxScheduleLookahead // schedule at most one keysign per ticker @@ -332,6 +334,17 @@ func (co *CoreObserver) scheduleCctxBTC( co.logger.ZetaChainWatcher.Error().Msgf("scheduleCctxBTC: outtx %s chainid mismatch: want %d, got %d", outTxID, chainID, params.ReceiverChainId) continue } + // try confirming the outtx + included, confirmed, err := btcClient.IsSendOutTxProcessed(cctx.Index, nonce, params.CoinType, co.logger.ZetaChainWatcher) + if err != nil { + co.logger.ZetaChainWatcher.Error().Err(err).Msgf("scheduleCctxBTC: IsSendOutTxProcessed faild for chain %d nonce %d", chainID, nonce) + continue + } + if included || confirmed { + co.logger.ZetaChainWatcher.Info().Msgf("scheduleCctxBTC: outtx %s already included; do not schedule keysign", outTxID) + return + } + // stop if the nonce being processed is higher than the pending nonce if nonce > btcClient.GetPendingNonce() { break @@ -342,7 +355,7 @@ func (co *CoreObserver) scheduleCctxBTC( break } // try confirming the outtx or scheduling a keysign - if !outTxMan.IsOutTxActive(outTxID) { + if nonce%interval == zetaHeight%interval && !outTxMan.IsOutTxActive(outTxID) { outTxMan.StartTryProcess(outTxID) co.logger.ZetaChainWatcher.Debug().Msgf("scheduleCctxBTC: sign outtx %s with value %d\n", outTxID, params.Amount) go signer.TryProcessOutTx(cctx, outTxMan, outTxID, ob, co.bridge, zetaHeight)