From 6af523b0f0abd9b8e23544859c33fbe0940ecb76 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 17 Jan 2025 15:20:20 -0600 Subject: [PATCH 1/3] have zetacore feed latest gas price to pending Bitcoin CCTXs --- testutil/sample/crypto.go | 13 +- x/crosschain/keeper/abci.go | 138 +++++-- x/crosschain/keeper/abci_test.go | 478 +++++++++++++++++----- x/crosschain/module.go | 2 +- x/crosschain/types/cctx_test.go | 2 +- x/crosschain/types/revert_options_test.go | 2 +- x/observer/types/crosschain_flags.go | 2 +- 7 files changed, 489 insertions(+), 148 deletions(-) diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index 783ffa4a8d..6ebf295010 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/cometbft/cometbft/crypto/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -91,7 +92,7 @@ func EthAddressFromRand(r *rand.Rand) ethcommon.Address { } // BtcAddressP2WPKH returns a sample btc P2WPKH address -func BtcAddressP2WPKH(t *testing.T, net *chaincfg.Params) string { +func BtcAddressP2WPKH(t *testing.T, net *chaincfg.Params) *btcutil.AddressWitnessPubKeyHash { privateKey, err := btcec.NewPrivateKey() require.NoError(t, err) @@ -99,7 +100,15 @@ func BtcAddressP2WPKH(t *testing.T, net *chaincfg.Params) string { addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) require.NoError(t, err) - return addr.String() + return addr +} + +// BtcAddressP2WPKH returns a pkscript for a sample btc P2WPKH address +func BtcAddressP2WPKHScript(t *testing.T, net *chaincfg.Params) []byte { + addr := BtcAddressP2WPKH(t, net) + script, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + return script } // SolanaPrivateKey returns a sample solana private key diff --git a/x/crosschain/keeper/abci.go b/x/crosschain/keeper/abci.go index 03d13fb6d5..2af2efb420 100644 --- a/x/crosschain/keeper/abci.go +++ b/x/crosschain/keeper/abci.go @@ -19,20 +19,20 @@ const ( RemainingFeesToStabilityPoolPercent = 95 ) -// CheckAndUpdateCctxGasPriceFunc is a function type for checking and updating the gas price of a cctx -type CheckAndUpdateCctxGasPriceFunc func( +// CheckAndUpdateCCTXGasPriceFunc is a function type for checking and updating the gas price of a cctx +type CheckAndUpdateCCTXGasPriceFunc func( ctx sdk.Context, k Keeper, cctx types.CrossChainTx, flags observertypes.GasPriceIncreaseFlags, ) (math.Uint, math.Uint, error) -// IterateAndUpdateCctxGasPrice iterates through all cctx and updates the gas price if pending for too long +// IterateAndUpdateCCTXGasPrice iterates through all cctx and updates the gas price if pending for too long // The function returns the number of cctxs updated and the gas price increase flags used -func (k Keeper) IterateAndUpdateCctxGasPrice( +func (k Keeper) IterateAndUpdateCCTXGasPrice( ctx sdk.Context, chains []zetachains.Chain, - updateFunc CheckAndUpdateCctxGasPriceFunc, + updateFunc CheckAndUpdateCCTXGasPriceFunc, ) (int, observertypes.GasPriceIncreaseFlags) { // fetch the gas price increase flags or use default gasPriceIncreaseFlags := observertypes.DefaultGasPriceIncreaseFlags @@ -52,46 +52,47 @@ func (k Keeper) IterateAndUpdateCctxGasPrice( IterateChains: for _, chain := range chains { - // support only external evm chains - if zetachains.IsEVMChain(chain.ChainId, additionalChains) && !zetachains.IsZetaChain(chain.ChainId, additionalChains) { - res, err := k.ListPendingCctx(sdk.UnwrapSDKContext(ctx), &types.QueryListPendingCctxRequest{ - ChainId: chain.ChainId, - Limit: gasPriceIncreaseFlags.MaxPendingCctxs, - }) - if err != nil { - ctx.Logger().Info("GasStabilityPool: fetching pending cctx failed", - "chainID", chain.ChainId, - "err", err.Error(), - ) - continue IterateChains - } + if !IsCCTXGasPriceUpdateSupported(chain.ChainId, additionalChains) { + continue + } + + res, err := k.ListPendingCctx(sdk.UnwrapSDKContext(ctx), &types.QueryListPendingCctxRequest{ + ChainId: chain.ChainId, + Limit: gasPriceIncreaseFlags.MaxPendingCctxs, + }) + if err != nil { + ctx.Logger().Info("GasStabilityPool: fetching pending cctx failed", + "chainID", chain.ChainId, + "err", err.Error(), + ) + continue IterateChains + } - // iterate through all pending cctx - for _, pendingCctx := range res.CrossChainTx { - if pendingCctx != nil { - gasPriceIncrease, additionalFees, err := updateFunc(ctx, k, *pendingCctx, gasPriceIncreaseFlags) - if err != nil { - ctx.Logger().Info("GasStabilityPool: updating gas price for pending cctx failed", - "cctxIndex", pendingCctx.Index, + // iterate through all pending cctx + for _, pendingCctx := range res.CrossChainTx { + if pendingCctx != nil { + gasPriceIncrease, additionalFees, err := updateFunc(ctx, k, *pendingCctx, gasPriceIncreaseFlags) + if err != nil { + ctx.Logger().Info("GasStabilityPool: updating gas price for pending cctx failed", + "cctxIndex", pendingCctx.Index, + "err", err.Error(), + ) + continue IterateChains + } + if !gasPriceIncrease.IsNil() && !gasPriceIncrease.IsZero() { + // Emit typed event for gas price increase + if err := ctx.EventManager().EmitTypedEvent( + &types.EventCCTXGasPriceIncreased{ + CctxIndex: pendingCctx.Index, + GasPriceIncrease: gasPriceIncrease.String(), + AdditionalFees: additionalFees.String(), + }); err != nil { + ctx.Logger().Error( + "GasStabilityPool: failed to emit EventCCTXGasPriceIncreased", "err", err.Error(), ) - continue IterateChains - } - if !gasPriceIncrease.IsNil() && !gasPriceIncrease.IsZero() { - // Emit typed event for gas price increase - if err := ctx.EventManager().EmitTypedEvent( - &types.EventCCTXGasPriceIncreased{ - CctxIndex: pendingCctx.Index, - GasPriceIncrease: gasPriceIncrease.String(), - AdditionalFees: additionalFees.String(), - }); err != nil { - ctx.Logger().Error( - "GasStabilityPool: failed to emit EventCCTXGasPriceIncreased", - "err", err.Error(), - ) - } - cctxCount++ } + cctxCount++ } } } @@ -102,7 +103,7 @@ IterateChains: // CheckAndUpdateCctxGasPrice checks if the retry interval is reached and updates the gas price if so // The function returns the gas price increase and the additional fees paid from the gas stability pool -func CheckAndUpdateCctxGasPrice( +func CheckAndUpdateCCTXGasPrice( ctx sdk.Context, k Keeper, cctx types.CrossChainTx, @@ -128,6 +129,30 @@ func CheckAndUpdateCctxGasPrice( fmt.Sprintf("cannot get gas price for chain %d", chainID), ) } + + // dispatch to chain-specific gas price update function + additionalChains := k.GetAuthorityKeeper().GetAdditionalChainList(ctx) + switch { + case zetachains.IsEVMChain(chainID, additionalChains): + return CheckAndUpdateCCTXGasPriceEVM(ctx, k, medianGasPrice, medianPriorityFee, cctx, flags) + case zetachains.IsBitcoinChain(chainID, additionalChains): + return CheckAndUpdateCCTXGasPriceBTC(ctx, k, medianGasPrice, cctx) + default: + return math.ZeroUint(), math.ZeroUint(), nil + } +} + +// CheckAndUpdateCCTXGasPriceEVM updates the gas price for the given EVM chain CCTX +func CheckAndUpdateCCTXGasPriceEVM( + ctx sdk.Context, + k Keeper, + medianGasPrice math.Uint, + medianPriorityFee math.Uint, + cctx types.CrossChainTx, + flags observertypes.GasPriceIncreaseFlags, +) (math.Uint, math.Uint, error) { + // compute gas price increase + chainID := cctx.GetCurrentOutboundParam().ReceiverChainId gasPriceIncrease := medianGasPrice.MulUint64(uint64(flags.GasPriceIncreasePercent)).QuoUint64(100) // compute new gas price @@ -175,3 +200,32 @@ func CheckAndUpdateCctxGasPrice( return gasPriceIncrease, additionalFees, nil } + +// CheckAndUpdateCCTXGasPriceBTC updates the fee rate for the given Bitcoin chain CCTX +func CheckAndUpdateCCTXGasPriceBTC( + ctx sdk.Context, + k Keeper, + medianGasPrice math.Uint, + cctx types.CrossChainTx, +) (math.Uint, math.Uint, error) { + // zetacore simply update 'GasPriorityFee', and zetaclient will use it to schedule RBF tx + // there is no priority fee in Bitcoin, the 'GasPriorityFee' is repurposed to store latest fee rate in sat/vB + cctx.GetCurrentOutboundParam().GasPriorityFee = medianGasPrice.String() + k.SetCrossChainTx(ctx, cctx) + + return math.ZeroUint(), math.ZeroUint(), nil +} + +// IsCCTXGasPriceUpdateSupported checks if the given chain supports gas price update +func IsCCTXGasPriceUpdateSupported(chainID int64, additionalChains []zetachains.Chain) bool { + switch { + case zetachains.IsZetaChain(chainID, additionalChains): + return false + case zetachains.IsEVMChain(chainID, additionalChains): + return true + case zetachains.IsBitcoinChain(chainID, additionalChains): + return true + default: + return false + } +} diff --git a/x/crosschain/keeper/abci_test.go b/x/crosschain/keeper/abci_test.go index 32499e1827..986d7151ee 100644 --- a/x/crosschain/keeper/abci_test.go +++ b/x/crosschain/keeper/abci_test.go @@ -63,7 +63,7 @@ func TestKeeper_IterateAndUpdateCctxGasPrice(t *testing.T) { // test that the default crosschain flags are used when not set and the epoch length is not reached ctx = ctx.WithBlockHeight(observertypes.DefaultCrosschainFlags().GasPriceIncreaseFlags.EpochLength + 1) - cctxCount, flags := k.IterateAndUpdateCctxGasPrice(ctx, supportedChains, updateFunc) + cctxCount, flags := k.IterateAndUpdateCCTXGasPrice(ctx, supportedChains, updateFunc) require.Equal(t, 0, cctxCount) require.Equal(t, *observertypes.DefaultCrosschainFlags().GasPriceIncreaseFlags, flags) @@ -79,23 +79,29 @@ func TestKeeper_IterateAndUpdateCctxGasPrice(t *testing.T) { crosschainFlags.GasPriceIncreaseFlags = &customFlags zk.ObserverKeeper.SetCrosschainFlags(ctx, *crosschainFlags) - cctxCount, flags = k.IterateAndUpdateCctxGasPrice(ctx, supportedChains, updateFunc) + cctxCount, flags = k.IterateAndUpdateCCTXGasPrice(ctx, supportedChains, updateFunc) require.Equal(t, 0, cctxCount) require.Equal(t, customFlags, flags) // test that cctx are iterated and updated when the epoch length is reached ctx = ctx.WithBlockHeight(observertypes.DefaultCrosschainFlags().GasPriceIncreaseFlags.EpochLength * 2) - cctxCount, flags = k.IterateAndUpdateCctxGasPrice(ctx, supportedChains, updateFunc) + cctxCount, flags = k.IterateAndUpdateCCTXGasPrice(ctx, supportedChains, updateFunc) - // 2 eth + 5 bsc = 7 - require.Equal(t, 7, cctxCount) + // 2 eth + 5 btc + 5 bsc = 12 + require.Equal(t, 12, cctxCount) require.Equal(t, customFlags, flags) // check that the update function was called with the cctx index - require.Equal(t, 7, len(updateFuncMap)) + require.Equal(t, 12, len(updateFuncMap)) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("1-10")) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("1-11")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-20")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-21")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-22")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-23")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-24")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-30")) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-31")) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-32")) @@ -103,7 +109,7 @@ func TestKeeper_IterateAndUpdateCctxGasPrice(t *testing.T) { require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-34")) } -func TestCheckAndUpdateCctxGasPrice(t *testing.T) { +func Test_CheckAndUpdateCCTXGasPrice(t *testing.T) { sampleTimestamp := time.Now() retryIntervalReached := sampleTimestamp.Add(observertypes.DefaultGasPriceIncreaseFlags.RetryInterval + time.Second) retryIntervalNotReached := sampleTimestamp.Add( @@ -133,7 +139,7 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { }, OutboundParams: []*types.OutboundParams{ { - ReceiverChainId: 42, + ReceiverChainId: 1, CallOptions: &types.CallOptions{ GasLimit: 1000, }, @@ -151,9 +157,9 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { expectedAdditionalFees: math.NewUint(50000), // gasLimit * increase }, { - name: "can update gas price at max limit", + name: "skip if gas price is not set", cctx: types.CrossChainTx{ - Index: "a2", + Index: "b1", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), @@ -162,29 +168,23 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { { ReceiverChainId: 42, CallOptions: &types.CallOptions{ - GasLimit: 1000, + GasLimit: 100, }, - GasPrice: "100", + GasPrice: "", }, }, }, - flags: observertypes.GasPriceIncreaseFlags{ - EpochLength: 100, - RetryInterval: time.Minute * 10, - GasPriceIncreasePercent: 200, // Increase gas price to 100+50*2 = 200 - GasPriceIncreaseMax: 400, // Max gas price is 50*4 = 200 - }, + flags: observertypes.DefaultGasPriceIncreaseFlags, blockTimestamp: retryIntervalReached, - medianGasPrice: 50, - withdrawFromGasStabilityPoolReturn: nil, - expectWithdrawFromGasStabilityPoolCall: true, - expectedGasPriceIncrease: math.NewUint(100), // 200% medianGasPrice - expectedAdditionalFees: math.NewUint(100000), // gasLimit * increase + medianGasPrice: 100, + expectWithdrawFromGasStabilityPoolCall: false, + expectedGasPriceIncrease: math.NewUint(0), + expectedAdditionalFees: math.NewUint(0), }, { - name: "default gas price increase limit used if not defined", + name: "skip if gas limit is not set", cctx: types.CrossChainTx{ - Index: "a3", + Index: "b2", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), @@ -193,29 +193,23 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { { ReceiverChainId: 42, CallOptions: &types.CallOptions{ - GasLimit: 1000, + GasLimit: 0, }, GasPrice: "100", }, }, }, - flags: observertypes.GasPriceIncreaseFlags{ - EpochLength: 100, - RetryInterval: time.Minute * 10, - GasPriceIncreasePercent: 100, - GasPriceIncreaseMax: 0, // Limit should not be reached - }, + flags: observertypes.DefaultGasPriceIncreaseFlags, blockTimestamp: retryIntervalReached, - medianGasPrice: 50, - withdrawFromGasStabilityPoolReturn: nil, - expectWithdrawFromGasStabilityPoolCall: true, - expectedGasPriceIncrease: math.NewUint(50), // 100% medianGasPrice - expectedAdditionalFees: math.NewUint(50000), // gasLimit * increase + medianGasPrice: 100, + expectWithdrawFromGasStabilityPoolCall: false, + expectedGasPriceIncrease: math.NewUint(0), + expectedAdditionalFees: math.NewUint(0), }, { - name: "skip if max limit reached", + name: "skip if retry interval is not reached", cctx: types.CrossChainTx{ - Index: "b0", + Index: "b3", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), @@ -224,28 +218,23 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { { ReceiverChainId: 42, CallOptions: &types.CallOptions{ - GasLimit: 1000, + GasLimit: 0, }, GasPrice: "100", }, }, }, - flags: observertypes.GasPriceIncreaseFlags{ - EpochLength: 100, - RetryInterval: time.Minute * 10, - GasPriceIncreasePercent: 200, // Increase gas price to 100+50*2 = 200 - GasPriceIncreaseMax: 300, // Max gas price is 50*3 = 150 - }, - blockTimestamp: retryIntervalReached, - medianGasPrice: 50, + flags: observertypes.DefaultGasPriceIncreaseFlags, + blockTimestamp: retryIntervalNotReached, + medianGasPrice: 100, expectWithdrawFromGasStabilityPoolCall: false, expectedGasPriceIncrease: math.NewUint(0), expectedAdditionalFees: math.NewUint(0), }, { - name: "skip if gas price is not set", + name: "returns error if can't find median gas price", cctx: types.CrossChainTx{ - Index: "b1", + Index: "b4", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), @@ -254,32 +243,159 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { { ReceiverChainId: 42, CallOptions: &types.CallOptions{ - GasLimit: 100, + GasLimit: 1000, }, - GasPrice: "", + GasPrice: "100", }, }, }, flags: observertypes.DefaultGasPriceIncreaseFlags, - blockTimestamp: retryIntervalReached, - medianGasPrice: 100, expectWithdrawFromGasStabilityPoolCall: false, - expectedGasPriceIncrease: math.NewUint(0), - expectedAdditionalFees: math.NewUint(0), + blockTimestamp: retryIntervalReached, + medianGasPrice: 0, + isError: true, }, { - name: "skip if gas limit is not set", + name: "do nothing for non-EVM, non-BTC chain", cctx: types.CrossChainTx{ - Index: "b2", + Index: "c", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), }, OutboundParams: []*types.OutboundParams{ { - ReceiverChainId: 42, + ReceiverChainId: 100, CallOptions: &types.CallOptions{ - GasLimit: 0, + GasLimit: 1000, + }, + GasPrice: "100", + }, + }, + }, + flags: observertypes.DefaultGasPriceIncreaseFlags, + blockTimestamp: retryIntervalReached, + medianGasPrice: 50, + medianPriorityFee: 20, + withdrawFromGasStabilityPoolReturn: nil, + expectedGasPriceIncrease: math.NewUint(0), + expectedAdditionalFees: math.NewUint(0), + }, + } + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + k, ctx := testkeeper.CrosschainKeeperAllMocks(t) + fungibleMock := testkeeper.GetCrosschainFungibleMock(t, k) + authorityMock := testkeeper.GetCrosschainAuthorityMock(t, k) + chainID := tc.cctx.GetCurrentOutboundParam().ReceiverChainId + previousGasPrice, err := tc.cctx.GetCurrentOutboundParam().GetGasPriceUInt64() + if err != nil { + previousGasPrice = 0 + } + + // set median gas price if not zero + if tc.medianGasPrice != 0 { + k.SetGasPrice(ctx, types.GasPrice{ + ChainId: chainID, + Prices: []uint64{tc.medianGasPrice}, + PriorityFees: []uint64{tc.medianPriorityFee}, + MedianIndex: 0, + }) + + // ensure median gas price is set + medianGasPrice, medianPriorityFee, isFound := k.GetMedianGasValues(ctx, chainID) + require.True(t, isFound) + require.True(t, medianGasPrice.Equal(math.NewUint(tc.medianGasPrice))) + require.True(t, medianPriorityFee.Equal(math.NewUint(tc.medianPriorityFee))) + } + + // set block timestamp + ctx = ctx.WithBlockTime(tc.blockTimestamp) + + authorityMock.On("GetAdditionalChainList", ctx).Maybe().Return([]chains.Chain{}) + + if tc.expectWithdrawFromGasStabilityPoolCall { + fungibleMock.On( + "WithdrawFromGasStabilityPool", ctx, chainID, tc.expectedAdditionalFees.BigInt(), + ).Return(tc.withdrawFromGasStabilityPoolReturn) + } + + // check and update gas price + gasPriceIncrease, feesPaid, err := keeper.CheckAndUpdateCCTXGasPrice(ctx, *k, tc.cctx, tc.flags) + + if tc.isError { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check values + require.True( + t, + gasPriceIncrease.Equal(tc.expectedGasPriceIncrease), + "expected %s, got %s", + tc.expectedGasPriceIncrease.String(), + gasPriceIncrease.String(), + ) + require.True( + t, + feesPaid.Equal(tc.expectedAdditionalFees), + "expected %s, got %s", + tc.expectedAdditionalFees.String(), + feesPaid.String(), + ) + + // check cctx + if !tc.expectedGasPriceIncrease.IsZero() { + cctx, found := k.GetCrossChainTx(ctx, tc.cctx.Index) + require.True(t, found) + newGasPrice, err := cctx.GetCurrentOutboundParam().GetGasPriceUInt64() + require.NoError(t, err) + require.EqualValues( + t, + tc.expectedGasPriceIncrease.AddUint64(previousGasPrice).Uint64(), + newGasPrice, + "%d - %d", + tc.expectedGasPriceIncrease.Uint64(), + previousGasPrice, + ) + require.EqualValues(t, tc.blockTimestamp.Unix(), cctx.CctxStatus.LastUpdateTimestamp) + } + }) + } +} + +func Test_CheckAndUpdateCCTXGasPriceEVM(t *testing.T) { + sampleTimestamp := time.Now() + retryIntervalReached := sampleTimestamp.Add(observertypes.DefaultGasPriceIncreaseFlags.RetryInterval + time.Second) + + tt := []struct { + name string + cctx types.CrossChainTx + flags observertypes.GasPriceIncreaseFlags + blockTimestamp time.Time + medianGasPrice uint64 + medianPriorityFee uint64 + withdrawFromGasStabilityPoolReturn error + expectWithdrawFromGasStabilityPoolCall bool + expectedGasPriceIncrease math.Uint + expectedAdditionalFees math.Uint + isError bool + }{ + { + name: "can update gas price", + cctx: types.CrossChainTx{ + Index: "a1", + CctxStatus: &types.Status{ + CreatedTimestamp: sampleTimestamp.Unix(), + LastUpdateTimestamp: sampleTimestamp.Unix(), + }, + OutboundParams: []*types.OutboundParams{ + { + ReceiverChainId: 1, + CallOptions: &types.CallOptions{ + GasLimit: 1000, }, GasPrice: "100", }, @@ -287,47 +403,56 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { }, flags: observertypes.DefaultGasPriceIncreaseFlags, blockTimestamp: retryIntervalReached, - medianGasPrice: 100, - expectWithdrawFromGasStabilityPoolCall: false, - expectedGasPriceIncrease: math.NewUint(0), - expectedAdditionalFees: math.NewUint(0), + medianGasPrice: 50, + medianPriorityFee: 20, + withdrawFromGasStabilityPoolReturn: nil, + expectWithdrawFromGasStabilityPoolCall: true, + expectedGasPriceIncrease: math.NewUint(50), // 100% medianGasPrice + expectedAdditionalFees: math.NewUint(50000), // gasLimit * increase }, { - name: "skip if retry interval is not reached", + name: "can update gas price at max limit", cctx: types.CrossChainTx{ - Index: "b3", + Index: "a2", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), }, OutboundParams: []*types.OutboundParams{ { - ReceiverChainId: 42, + ReceiverChainId: 1, CallOptions: &types.CallOptions{ - GasLimit: 0, + GasLimit: 1000, }, GasPrice: "100", }, }, }, - flags: observertypes.DefaultGasPriceIncreaseFlags, - blockTimestamp: retryIntervalNotReached, - medianGasPrice: 100, - expectWithdrawFromGasStabilityPoolCall: false, - expectedGasPriceIncrease: math.NewUint(0), - expectedAdditionalFees: math.NewUint(0), + flags: observertypes.GasPriceIncreaseFlags{ + EpochLength: 100, + RetryInterval: time.Minute * 10, + GasPriceIncreasePercent: 200, // Increase gas price to 100+50*2 = 200 + GasPriceIncreaseMax: 400, // Max gas price is 50*4 = 200 + }, + blockTimestamp: retryIntervalReached, + medianGasPrice: 50, + medianPriorityFee: 20, + withdrawFromGasStabilityPoolReturn: nil, + expectWithdrawFromGasStabilityPoolCall: true, + expectedGasPriceIncrease: math.NewUint(100), // 200% medianGasPrice + expectedAdditionalFees: math.NewUint(100000), // gasLimit * increase }, { - name: "returns error if can't find median gas price", + name: "default gas price increase limit used if not defined", cctx: types.CrossChainTx{ - Index: "c1", + Index: "a3", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), }, OutboundParams: []*types.OutboundParams{ { - ReceiverChainId: 42, + ReceiverChainId: 1, CallOptions: &types.CallOptions{ GasLimit: 1000, }, @@ -335,23 +460,62 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { }, }, }, - flags: observertypes.DefaultGasPriceIncreaseFlags, - expectWithdrawFromGasStabilityPoolCall: false, + flags: observertypes.GasPriceIncreaseFlags{ + EpochLength: 100, + RetryInterval: time.Minute * 10, + GasPriceIncreasePercent: 100, + GasPriceIncreaseMax: 0, // Limit should not be reached + }, blockTimestamp: retryIntervalReached, - medianGasPrice: 0, - isError: true, + medianGasPrice: 50, + medianPriorityFee: 20, + withdrawFromGasStabilityPoolReturn: nil, + expectWithdrawFromGasStabilityPoolCall: true, + expectedGasPriceIncrease: math.NewUint(50), // 100% medianGasPrice + expectedAdditionalFees: math.NewUint(50000), // gasLimit * increase + }, + { + name: "skip if max limit reached", + cctx: types.CrossChainTx{ + Index: "b", + CctxStatus: &types.Status{ + CreatedTimestamp: sampleTimestamp.Unix(), + LastUpdateTimestamp: sampleTimestamp.Unix(), + }, + OutboundParams: []*types.OutboundParams{ + { + ReceiverChainId: 1, + CallOptions: &types.CallOptions{ + GasLimit: 1000, + }, + GasPrice: "100", + }, + }, + }, + flags: observertypes.GasPriceIncreaseFlags{ + EpochLength: 100, + RetryInterval: time.Minute * 10, + GasPriceIncreasePercent: 200, // Increase gas price to 100+50*2 = 200 + GasPriceIncreaseMax: 300, // Max gas price is 50*3 = 150 + }, + blockTimestamp: retryIntervalReached, + medianGasPrice: 50, + medianPriorityFee: 20, + expectWithdrawFromGasStabilityPoolCall: false, + expectedGasPriceIncrease: math.NewUint(0), + expectedAdditionalFees: math.NewUint(0), }, { name: "returns error if can't withdraw from gas stability pool", cctx: types.CrossChainTx{ - Index: "c2", + Index: "c", CctxStatus: &types.Status{ CreatedTimestamp: sampleTimestamp.Unix(), LastUpdateTimestamp: sampleTimestamp.Unix(), }, OutboundParams: []*types.OutboundParams{ { - ReceiverChainId: 42, + ReceiverChainId: 1, CallOptions: &types.CallOptions{ GasLimit: 1000, }, @@ -362,6 +526,7 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { flags: observertypes.DefaultGasPriceIncreaseFlags, blockTimestamp: retryIntervalReached, medianGasPrice: 50, + medianPriorityFee: 20, expectWithdrawFromGasStabilityPoolCall: true, expectedGasPriceIncrease: math.NewUint(50), // 100% medianGasPrice expectedAdditionalFees: math.NewUint(50000), // gasLimit * increase @@ -380,22 +545,6 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { previousGasPrice = 0 } - // set median gas price if not zero - if tc.medianGasPrice != 0 { - k.SetGasPrice(ctx, types.GasPrice{ - ChainId: chainID, - Prices: []uint64{tc.medianGasPrice}, - PriorityFees: []uint64{tc.medianPriorityFee}, - MedianIndex: 0, - }) - - // ensure median gas price is set - medianGasPrice, medianPriorityFee, isFound := k.GetMedianGasValues(ctx, chainID) - require.True(t, isFound) - require.True(t, medianGasPrice.Equal(math.NewUint(tc.medianGasPrice))) - require.True(t, medianPriorityFee.Equal(math.NewUint(tc.medianPriorityFee))) - } - // set block timestamp ctx = ctx.WithBlockTime(tc.blockTimestamp) @@ -406,7 +555,14 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { } // check and update gas price - gasPriceIncrease, feesPaid, err := keeper.CheckAndUpdateCctxGasPrice(ctx, *k, tc.cctx, tc.flags) + gasPriceIncrease, feesPaid, err := keeper.CheckAndUpdateCCTXGasPriceEVM( + ctx, + *k, + math.NewUint(tc.medianGasPrice), + math.NewUint(tc.medianPriorityFee), + tc.cctx, + tc.flags, + ) if tc.isError { require.Error(t, err) @@ -449,3 +605,125 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { }) } } + +func Test_CheckAndUpdateCCTXGasPriceBTC(t *testing.T) { + sampleTimestamp := time.Now() + gasRateUpdateInterval := observertypes.DefaultGasPriceIncreaseFlags.RetryInterval + retryIntervalReached := sampleTimestamp.Add(gasRateUpdateInterval + time.Second) + + tt := []struct { + name string + cctx types.CrossChainTx + blockTimestamp time.Time + medianGasPrice uint64 + }{ + { + name: "can update fee rate", + cctx: types.CrossChainTx{ + Index: "a", + CctxStatus: &types.Status{ + CreatedTimestamp: sampleTimestamp.Unix(), + LastUpdateTimestamp: sampleTimestamp.Unix(), + }, + OutboundParams: []*types.OutboundParams{ + { + ReceiverChainId: 8332, + CallOptions: &types.CallOptions{ + GasLimit: 254, + }, + GasPrice: "10", + }, + }, + }, + blockTimestamp: retryIntervalReached, + medianGasPrice: 12, + }, + } + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + k, ctx := testkeeper.CrosschainKeeperAllMocks(t) + + // set block timestamp + ctx = ctx.WithBlockTime(tc.blockTimestamp) + + // check and update gas rate + gasPriceIncrease, feesPaid, err := keeper.CheckAndUpdateCCTXGasPriceBTC( + ctx, + *k, + math.NewUint(tc.medianGasPrice), + tc.cctx, + ) + require.NoError(t, err) + + // check values + require.True(t, gasPriceIncrease.IsZero()) + require.True(t, feesPaid.IsZero()) + + // check cctx if fee rate is updated + cctx, found := k.GetCrossChainTx(ctx, tc.cctx.Index) + require.True(t, found) + newGasPrice, err := cctx.GetCurrentOutboundParam().GetGasPriorityFeeUInt64() + require.NoError(t, err) + require.Equal(t, tc.medianGasPrice, newGasPrice) + require.EqualValues(t, tc.blockTimestamp.Unix(), cctx.CctxStatus.LastUpdateTimestamp) + }) + } +} + +func Test_IsCCTXGasPriceUpdateSupported(t *testing.T) { + tt := []struct { + name string + chainID int64 + isSupport bool + }{ + { + name: "Zetachain is unsupported for gas price update", + chainID: chains.ZetaChainMainnet.ChainId, + isSupport: false, + }, + { + name: "Ethereum is supported for gas price update", + chainID: chains.Ethereum.ChainId, + isSupport: true, + }, + { + name: "BSC is supported for gas price update", + chainID: chains.BscMainnet.ChainId, + isSupport: true, + }, + { + name: "Polygon is supported for gas price update", + chainID: chains.Polygon.ChainId, + isSupport: true, + }, + { + name: "Base is supported for gas price update", + chainID: chains.BaseMainnet.ChainId, + isSupport: true, + }, + { + name: "Bitcoin is supported for gas price update", + chainID: chains.BitcoinMainnet.ChainId, + isSupport: true, + }, + { + name: "Solana is unsupported for gas price update", + chainID: chains.SolanaMainnet.ChainId, + isSupport: false, + }, + { + name: "TON is unsupported for gas price update", + chainID: chains.TONMainnet.ChainId, + isSupport: false, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + isSupported := keeper.IsCCTXGasPriceUpdateSupported(tc.chainID, []chains.Chain{}) + require.Equal(t, tc.isSupport, isSupported) + }) + } +} diff --git a/x/crosschain/module.go b/x/crosschain/module.go index e3b71cfeb0..5015a8e7fc 100644 --- a/x/crosschain/module.go +++ b/x/crosschain/module.go @@ -172,7 +172,7 @@ func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { // iterate and update gas price for cctx that are pending for too long // error is logged in the function - am.keeper.IterateAndUpdateCctxGasPrice(ctx, supportedChains, keeper.CheckAndUpdateCctxGasPrice) + am.keeper.IterateAndUpdateCCTXGasPrice(ctx, supportedChains, keeper.CheckAndUpdateCCTXGasPrice) } // EndBlock executes all ABCI EndBlock logic respective to the crosschain module. It diff --git a/x/crosschain/types/cctx_test.go b/x/crosschain/types/cctx_test.go index 1369e2dee0..1e2d6c830d 100644 --- a/x/crosschain/types/cctx_test.go +++ b/x/crosschain/types/cctx_test.go @@ -140,7 +140,7 @@ func Test_SetRevertOutboundValues(t *testing.T) { cctx := sample.CrossChainTx(t, "test") cctx.InboundParams.SenderChainId = chains.BitcoinTestnet.ChainId cctx.OutboundParams = cctx.OutboundParams[:1] - cctx.RevertOptions.RevertAddress = sample.BtcAddressP2WPKH(t, &chaincfg.TestNet3Params) + cctx.RevertOptions.RevertAddress = sample.BtcAddressP2WPKH(t, &chaincfg.TestNet3Params).String() err := cctx.AddRevertOutbound(100) require.NoError(t, err) diff --git a/x/crosschain/types/revert_options_test.go b/x/crosschain/types/revert_options_test.go index c91927dd86..3fa6a6e8ba 100644 --- a/x/crosschain/types/revert_options_test.go +++ b/x/crosschain/types/revert_options_test.go @@ -49,7 +49,7 @@ func TestRevertOptions_GetEVMRevertAddress(t *testing.T) { func TestRevertOptions_GetBTCRevertAddress(t *testing.T) { t.Run("valid Bitcoin revert address", func(t *testing.T) { - addr := sample.BtcAddressP2WPKH(t, &chaincfg.TestNet3Params) + addr := sample.BtcAddressP2WPKH(t, &chaincfg.TestNet3Params).String() actualAddr, valid := types.RevertOptions{ RevertAddress: addr, }.GetBTCRevertAddress(chains.BitcoinTestnet.ChainId) diff --git a/x/observer/types/crosschain_flags.go b/x/observer/types/crosschain_flags.go index 5637debe0e..0f2c3e4cb0 100644 --- a/x/observer/types/crosschain_flags.go +++ b/x/observer/types/crosschain_flags.go @@ -4,8 +4,8 @@ import "time" var DefaultGasPriceIncreaseFlags = GasPriceIncreaseFlags{ // EpochLength is the number of blocks in an epoch before triggering a gas price increase - EpochLength: 100, + // RetryInterval is the number of blocks to wait before incrementing the gas price again RetryInterval: time.Minute * 10, From ef21aa2f3e82a07d148373384fc50738a8a15df1 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 17 Jan 2025 16:14:43 -0600 Subject: [PATCH 2/3] add changelog entry --- changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index 4aeca2aba5..f5ed8a9ccf 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,10 @@ ## unreleased +### Features + +* [3377](https://github.com/zeta-chain/node/pull/3377) - have zetacore feed latest gas price to pending Bitcoin cctxs + ### Refactor * [3332](https://github.com/zeta-chain/node/pull/3332) - implement orchestrator V2. Move BTC observer-signer to V2 From 0275db049308030ebbe42e46186c44de68d4c18f Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 17 Jan 2025 16:39:43 -0600 Subject: [PATCH 3/3] fix unit test compile error --- zetaclient/chains/bitcoin/observer/event_test.go | 6 +++--- zetaclient/chains/bitcoin/observer/inbound_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zetaclient/chains/bitcoin/observer/event_test.go b/zetaclient/chains/bitcoin/observer/event_test.go index ab78269527..ed3a50d881 100644 --- a/zetaclient/chains/bitcoin/observer/event_test.go +++ b/zetaclient/chains/bitcoin/observer/event_test.go @@ -32,7 +32,7 @@ func createTestBtcEvent( memoStd *memo.InboundMemo, ) observer.BTCInboundEvent { return observer.BTCInboundEvent{ - FromAddress: sample.BtcAddressP2WPKH(t, net), + FromAddress: sample.BtcAddressP2WPKH(t, net).String(), ToAddress: sample.EthAddress().Hex(), MemoBytes: memo, MemoStd: memoStd, @@ -249,7 +249,7 @@ func Test_ValidateStandardMemo(t *testing.T) { }, FieldsV0: memo.FieldsV0{ RevertOptions: crosschaintypes.RevertOptions{ - RevertAddress: sample.BtcAddressP2WPKH(t, &chaincfg.TestNet3Params), + RevertAddress: sample.BtcAddressP2WPKH(t, &chaincfg.TestNet3Params).String(), }, }, }, @@ -400,7 +400,7 @@ func Test_NewInboundVoteFromStdMemo(t *testing.T) { t.Run("should create new inbound vote msg with standard memo", func(t *testing.T) { // create revert options revertOptions := crosschaintypes.NewEmptyRevertOptions() - revertOptions.RevertAddress = sample.BtcAddressP2WPKH(t, &chaincfg.MainNetParams) + revertOptions.RevertAddress = sample.BtcAddressP2WPKH(t, &chaincfg.MainNetParams).String() // create test event receiver := sample.EthAddress() diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index 60ac90cb18..662fb7adbd 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -167,7 +167,7 @@ func Test_GetInboundVoteFromBtcEvent(t *testing.T) { { name: "should return vote for standard memo", event: &observer.BTCInboundEvent{ - FromAddress: sample.BtcAddressP2WPKH(t, &chaincfg.MainNetParams), + FromAddress: sample.BtcAddressP2WPKH(t, &chaincfg.MainNetParams).String(), // a deposit and call MemoBytes: testutil.HexToBytes( t,