Skip to content

Commit ce8afec

Browse files
ws4charlielumtis
andauthored
fix: special handle Bitcoin Testnet gas price estimator (#2452)
* special handle bitcoin testnet gas price estimator * let specialHandleFeeRate return the fee rate number and calls PostGasPrice in single spot * added some explanations for specialHandleFeeRate --------- Co-authored-by: Lucas Bertrand <[email protected]>
1 parent d0db41c commit ce8afec

File tree

4 files changed

+116
-33
lines changed

4 files changed

+116
-33
lines changed

changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
* [2327](https://github.com/zeta-chain/node/pull/2327) - partially cherry picked the fix to Bitcoin outbound dust amount
9191
* [2362](https://github.com/zeta-chain/node/pull/2362) - set 1000 satoshis as minimum BTC amount that can be withdrawn from zEVM
9292
* [2382](https://github.com/zeta-chain/node/pull/2382) - add tx input and gas in rpc methods for synthetic eth txs
93+
* [2396](https://github.com/zeta-chain/node/issues/2386) - special handle bitcoin testnet gas price estimator
9394
* [2434](https://github.com/zeta-chain/node/pull/2434) - the default database when running `zetacored init` is now pebbledb
9495

9596
### CI

zetaclient/chains/bitcoin/observer/observer.go

+41-23
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
observertypes "github.com/zeta-chain/zetacore/x/observer/types"
2424
"github.com/zeta-chain/zetacore/zetaclient/chains/base"
2525
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin"
26+
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc"
2627
"github.com/zeta-chain/zetacore/zetaclient/chains/interfaces"
2728
"github.com/zeta-chain/zetacore/zetaclient/context"
2829
"github.com/zeta-chain/zetacore/zetaclient/metrics"
@@ -341,44 +342,44 @@ func (ob *Observer) WatchGasPrice() {
341342
// PostGasPrice posts gas price to zetacore
342343
// TODO(revamp): move to gas price file
343344
func (ob *Observer) PostGasPrice() error {
344-
// hardcode gas price here since this RPC is not available on regtest
345-
if chains.IsBitcoinRegnet(ob.Chain().ChainId) {
346-
blockNumber, err := ob.btcClient.GetBlockCount()
345+
var err error
346+
feeRateEstimated := uint64(0)
347+
348+
// special handle regnet and testnet gas rate
349+
// regnet: RPC 'EstimateSmartFee' is not available
350+
// testnet: RPC 'EstimateSmartFee' returns unreasonable high gas rate
351+
if ob.Chain().NetworkType != chains.NetworkType_mainnet {
352+
feeRateEstimated, err = ob.specialHandleFeeRate()
347353
if err != nil {
354+
ob.logger.GasPrice.Err(err).Msg("error specialHandleFeeRate")
348355
return err
349356
}
350-
351-
// #nosec G701 always in range
352-
_, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), 1, "100", uint64(blockNumber))
357+
} else {
358+
// EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation
359+
feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical)
353360
if err != nil {
354-
ob.logger.GasPrice.Err(err).Msg("PostGasPrice:")
361+
ob.logger.GasPrice.Err(err).Msg("error EstimateSmartFee")
355362
return err
356363
}
357-
return nil
358-
}
359-
360-
// EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation
361-
feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical)
362-
if err != nil {
363-
return err
364-
}
365-
if feeResult.Errors != nil || feeResult.FeeRate == nil {
366-
return fmt.Errorf("error getting gas price: %s", feeResult.Errors)
367-
}
368-
if *feeResult.FeeRate > math.MaxInt64 {
369-
return fmt.Errorf("gas price is too large: %f", *feeResult.FeeRate)
364+
if feeResult.Errors != nil || feeResult.FeeRate == nil {
365+
return fmt.Errorf("error getting gas price: %s", feeResult.Errors)
366+
}
367+
if *feeResult.FeeRate > math.MaxInt64 {
368+
return fmt.Errorf("gas price is too large: %f", *feeResult.FeeRate)
369+
}
370+
feeRateEstimated = bitcoin.FeeRateToSatPerByte(*feeResult.FeeRate).Uint64()
370371
}
371-
feeRatePerByte := bitcoin.FeeRateToSatPerByte(*feeResult.FeeRate)
372372

373+
// query the current block number
373374
blockNumber, err := ob.btcClient.GetBlockCount()
374375
if err != nil {
375376
return err
376377
}
377378

378379
// #nosec G701 always positive
379-
_, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRatePerByte.Uint64(), "100", uint64(blockNumber))
380+
_, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRateEstimated, "100", uint64(blockNumber))
380381
if err != nil {
381-
ob.logger.GasPrice.Err(err).Msg("PostGasPrice:")
382+
ob.logger.GasPrice.Err(err).Msg("err PostGasPrice")
382383
return err
383384
}
384385

@@ -644,6 +645,23 @@ func (ob *Observer) LoadBroadcastedTxMap() error {
644645
return nil
645646
}
646647

648+
// specialHandleFeeRate handles the fee rate for regnet and testnet
649+
func (ob *Observer) specialHandleFeeRate() (uint64, error) {
650+
switch ob.Chain().NetworkType {
651+
case chains.NetworkType_privnet:
652+
// hardcode gas price for regnet
653+
return 1, nil
654+
case chains.NetworkType_testnet:
655+
feeRateEstimated, err := rpc.GetRecentFeeRate(ob.btcClient, ob.netParams)
656+
if err != nil {
657+
return 0, errors.Wrapf(err, "error GetRecentFeeRate")
658+
}
659+
return feeRateEstimated, nil
660+
default:
661+
return 0, fmt.Errorf(" unsupported bitcoin network type %d", ob.Chain().NetworkType)
662+
}
663+
}
664+
647665
// isTssTransaction checks if a given transaction was sent by TSS itself.
648666
// An unconfirmed transaction is safe to spend only if it was sent by TSS and verified by ourselves.
649667
func (ob *Observer) isTssTransaction(txid string) bool {

zetaclient/chains/bitcoin/rpc/rpc.go

+50
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@ import (
44
"fmt"
55

66
"github.com/btcsuite/btcd/btcjson"
7+
"github.com/btcsuite/btcd/chaincfg"
78
"github.com/btcsuite/btcd/chaincfg/chainhash"
89
"github.com/btcsuite/btcd/rpcclient"
910
"github.com/pkg/errors"
1011

12+
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin"
1113
"github.com/zeta-chain/zetacore/zetaclient/chains/interfaces"
1214
"github.com/zeta-chain/zetacore/zetaclient/config"
1315
)
1416

17+
const (
18+
// feeRateCountBackBlocks is the default number of blocks to look back for fee rate estimation
19+
feeRateCountBackBlocks = 2
20+
21+
// defaultTestnetFeeRate is the default fee rate for testnet, 10 sat/byte
22+
defaultTestnetFeeRate = 10
23+
)
24+
1525
// NewRPCClient creates a new RPC client by the given config.
1626
func NewRPCClient(btcConfig config.BTCConfig) (*rpcclient.Client, error) {
1727
connCfg := &rpcclient.ConnConfig{
@@ -107,3 +117,43 @@ func GetRawTxResult(
107117
// res.Confirmations < 0 (meaning not included)
108118
return btcjson.TxRawResult{}, fmt.Errorf("GetRawTxResult: tx %s not included yet", hash)
109119
}
120+
121+
// GetRecentFeeRate gets the highest fee rate from recent blocks
122+
// Note: this method is only used for testnet
123+
func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Params) (uint64, error) {
124+
blockNumber, err := rpcClient.GetBlockCount()
125+
if err != nil {
126+
return 0, err
127+
}
128+
129+
// get the highest fee rate among recent 'countBack' blocks to avoid underestimation
130+
highestRate := int64(0)
131+
for i := int64(0); i < feeRateCountBackBlocks; i++ {
132+
// get the block
133+
hash, err := rpcClient.GetBlockHash(blockNumber - i)
134+
if err != nil {
135+
return 0, err
136+
}
137+
block, err := rpcClient.GetBlockVerboseTx(hash)
138+
if err != nil {
139+
return 0, err
140+
}
141+
142+
// computes the average fee rate of the block and take the higher rate
143+
avgFeeRate, err := bitcoin.CalcBlockAvgFeeRate(block, netParams)
144+
if err != nil {
145+
return 0, err
146+
}
147+
if avgFeeRate > highestRate {
148+
highestRate = avgFeeRate
149+
}
150+
}
151+
152+
// use 10 sat/byte as default estimation if recent fee rate drops to 0
153+
if highestRate == 0 {
154+
highestRate = defaultTestnetFeeRate
155+
}
156+
157+
// #nosec G701 always in range
158+
return uint64(highestRate), nil
159+
}

zetaclient/chains/bitcoin/rpc/rpc_live_test.go

+24-10
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (suite *BitcoinObserverTestSuite) SetupTest() {
5959
base.DefaultLogger(), nil)
6060
suite.Require().NoError(err)
6161
suite.Require().NotNil(ob)
62-
suite.rpcClient, err = getRPCClient(18332)
62+
suite.rpcClient, err = createRPCClient(18332)
6363
suite.Require().NoError(err)
6464
skBytes, err := hex.DecodeString(skHex)
6565
suite.Require().NoError(err)
@@ -91,13 +91,14 @@ func (suite *BitcoinObserverTestSuite) SetupTest() {
9191
func (suite *BitcoinObserverTestSuite) TearDownSuite() {
9292
}
9393

94-
func getRPCClient(chainID int64) (*rpcclient.Client, error) {
94+
// createRPCClient creates a new Bitcoin RPC client for given chainID
95+
func createRPCClient(chainID int64) (*rpcclient.Client, error) {
9596
var connCfg *rpcclient.ConnConfig
9697
rpcMainnet := os.Getenv("BTC_RPC_MAINNET")
9798
rpcTestnet := os.Getenv("BTC_RPC_TESTNET")
9899

99100
// mainnet
100-
if chainID == 8332 {
101+
if chainID == chains.BitcoinMainnet.ChainId {
101102
connCfg = &rpcclient.ConnConfig{
102103
Host: rpcMainnet, // mainnet endpoint goes here
103104
User: "user",
@@ -108,7 +109,7 @@ func getRPCClient(chainID int64) (*rpcclient.Client, error) {
108109
}
109110
}
110111
// testnet3
111-
if chainID == 18332 {
112+
if chainID == chains.BitcoinTestnet.ChainId {
112113
connCfg = &rpcclient.ConnConfig{
113114
Host: rpcTestnet, // testnet endpoint goes here
114115
User: "user",
@@ -218,6 +219,7 @@ func TestBitcoinObserverLive(t *testing.T) {
218219
// LiveTestBitcoinFeeRate(t)
219220
// LiveTestAvgFeeRateMainnetMempoolSpace(t)
220221
// LiveTestAvgFeeRateTestnetMempoolSpace(t)
222+
// LiveTestGetRecentFeeRate(t)
221223
// LiveTestGetSenderByVin(t)
222224
}
223225

@@ -243,7 +245,7 @@ func LiveTestNewRPCClient(t *testing.T) {
243245
// LiveTestGetBlockHeightByHash queries Bitcoin block height by hash
244246
func LiveTestGetBlockHeightByHash(t *testing.T) {
245247
// setup Bitcoin client
246-
client, err := getRPCClient(8332)
248+
client, err := createRPCClient(chains.BitcoinMainnet.ChainId)
247249
require.NoError(t, err)
248250

249251
// the block hashes to test
@@ -265,7 +267,7 @@ func LiveTestGetBlockHeightByHash(t *testing.T) {
265267
// and compares Conservative and Economical fee rates for different block targets (1 and 2)
266268
func LiveTestBitcoinFeeRate(t *testing.T) {
267269
// setup Bitcoin client
268-
client, err := getRPCClient(8332)
270+
client, err := createRPCClient(chains.BitcoinMainnet.ChainId)
269271
require.NoError(t, err)
270272
bn, err := client.GetBlockCount()
271273
if err != nil {
@@ -390,7 +392,7 @@ func compareAvgFeeRate(t *testing.T, client *rpcclient.Client, startBlock int, e
390392
// LiveTestAvgFeeRateMainnetMempoolSpace compares calculated fee rate with mempool.space fee rate for mainnet
391393
func LiveTestAvgFeeRateMainnetMempoolSpace(t *testing.T) {
392394
// setup Bitcoin client
393-
client, err := getRPCClient(8332)
395+
client, err := createRPCClient(chains.BitcoinMainnet.ChainId)
394396
require.NoError(t, err)
395397

396398
// test against mempool.space API for 10000 blocks
@@ -404,7 +406,7 @@ func LiveTestAvgFeeRateMainnetMempoolSpace(t *testing.T) {
404406
// LiveTestAvgFeeRateTestnetMempoolSpace compares calculated fee rate with mempool.space fee rate for testnet
405407
func LiveTestAvgFeeRateTestnetMempoolSpace(t *testing.T) {
406408
// setup Bitcoin client
407-
client, err := getRPCClient(18332)
409+
client, err := createRPCClient(chains.BitcoinTestnet.ChainId)
408410
require.NoError(t, err)
409411

410412
// test against mempool.space API for 10000 blocks
@@ -415,11 +417,23 @@ func LiveTestAvgFeeRateTestnetMempoolSpace(t *testing.T) {
415417
compareAvgFeeRate(t, client, startBlock, endBlock, true)
416418
}
417419

420+
// LiveTestGetRecentFeeRate gets the highest fee rate from recent blocks
421+
func LiveTestGetRecentFeeRate(t *testing.T) {
422+
// setup Bitcoin testnet client
423+
client, err := createRPCClient(chains.BitcoinTestnet.ChainId)
424+
require.NoError(t, err)
425+
426+
// get fee rate from recent blocks
427+
feeRate, err := rpc.GetRecentFeeRate(client, &chaincfg.TestNet3Params)
428+
require.NoError(t, err)
429+
require.Greater(t, feeRate, uint64(0))
430+
}
431+
418432
// LiveTestGetSenderByVin gets sender address for each vin and compares with mempool.space sender address
419433
func LiveTestGetSenderByVin(t *testing.T) {
420434
// setup Bitcoin client
421-
chainID := int64(8332)
422-
client, err := getRPCClient(chainID)
435+
chainID := chains.BitcoinMainnet.ChainId
436+
client, err := createRPCClient(chainID)
423437
require.NoError(t, err)
424438

425439
// net params

0 commit comments

Comments
 (0)