Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: special handle Bitcoin Testnet gas price estimator #2452

Merged
merged 4 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
* [2327](https://github.com/zeta-chain/node/pull/2327) - partially cherry picked the fix to Bitcoin outbound dust amount
* [2362](https://github.com/zeta-chain/node/pull/2362) - set 1000 satoshis as minimum BTC amount that can be withdrawn from zEVM
* [2382](https://github.com/zeta-chain/node/pull/2382) - add tx input and gas in rpc methods for synthetic eth txs
* [2396](https://github.com/zeta-chain/node/issues/2386) - special handle bitcoin testnet gas price estimator

### CI
* [2388](https://github.com/zeta-chain/node/pull/2388) - added GitHub attestations of binaries produced in the release workflow.
Expand Down
48 changes: 34 additions & 14 deletions zetaclient/chains/bitcoin/observer/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
observertypes "github.com/zeta-chain/zetacore/x/observer/types"
"github.com/zeta-chain/zetacore/zetaclient/chains/base"
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin"
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc"
"github.com/zeta-chain/zetacore/zetaclient/chains/interfaces"
"github.com/zeta-chain/zetacore/zetaclient/context"
"github.com/zeta-chain/zetacore/zetaclient/metrics"
Expand Down Expand Up @@ -341,20 +342,9 @@
// PostGasPrice posts gas price to zetacore
// TODO(revamp): move to gas price file
func (ob *Observer) PostGasPrice() error {
// hardcode gas price here since this RPC is not available on regtest
if chains.IsBitcoinRegnet(ob.Chain().ChainId) {
blockNumber, err := ob.btcClient.GetBlockCount()
if err != nil {
return err
}

// #nosec G701 always in range
_, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), 1, "100", uint64(blockNumber))
if err != nil {
ob.logger.GasPrice.Err(err).Msg("PostGasPrice:")
return err
}
return nil
// special handle regnet and testnet gas rate
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
if ob.Chain().NetworkType != chains.NetworkType_mainnet {
return ob.specialHandleFeeRate()

Check warning on line 347 in zetaclient/chains/bitcoin/observer/observer.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/observer.go#L346-L347

Added lines #L346 - L347 were not covered by tests
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}

// EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation
Expand Down Expand Up @@ -644,6 +634,36 @@
return nil
}

// specialHandleFeeRate handles the fee rate for regnet and testnet
func (ob *Observer) specialHandleFeeRate() error {
blockNumber, err := ob.btcClient.GetBlockCount()
if err != nil {
return errors.Wrapf(err, "specialHandleFeeRate: error GetBlockCount")

Check warning on line 641 in zetaclient/chains/bitcoin/observer/observer.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/observer.go#L638-L641

Added lines #L638 - L641 were not covered by tests
}

feeRateEstimated := uint64(0)
switch ob.Chain().NetworkType {
case chains.NetworkType_privnet:

Check warning on line 646 in zetaclient/chains/bitcoin/observer/observer.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/observer.go#L644-L646

Added lines #L644 - L646 were not covered by tests
// hardcode gas price here since RPC 'EstimateSmartFee' is not available on regtest (privnet)
feeRateEstimated = 1
case chains.NetworkType_testnet:
feeRateEstimated, err = rpc.GetRecentFeeRate(ob.btcClient, ob.netParams)
if err != nil {
return errors.Wrapf(err, "specialHandleFeeRate: error GetRecentFeeRate")

Check warning on line 652 in zetaclient/chains/bitcoin/observer/observer.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/observer.go#L648-L652

Added lines #L648 - L652 were not covered by tests
}
default:
return fmt.Errorf("specialHandleFeeRate: unsupported bitcoin network type %d", ob.Chain().NetworkType)

Check warning on line 655 in zetaclient/chains/bitcoin/observer/observer.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/observer.go#L654-L655

Added lines #L654 - L655 were not covered by tests
}

// #nosec G701 always in range
_, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRateEstimated, "100", uint64(blockNumber))
if err != nil {
return errors.Wrapf(err, "specialHandleFeeRate: error PostGasPrice")

Check warning on line 661 in zetaclient/chains/bitcoin/observer/observer.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/observer.go#L659-L661

Added lines #L659 - L661 were not covered by tests
}

return nil

Check warning on line 664 in zetaclient/chains/bitcoin/observer/observer.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/observer.go#L664

Added line #L664 was not covered by tests
}
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved

// isTssTransaction checks if a given transaction was sent by TSS itself.
// An unconfirmed transaction is safe to spend only if it was sent by TSS and verified by ourselves.
func (ob *Observer) isTssTransaction(txid string) bool {
Expand Down
49 changes: 49 additions & 0 deletions zetaclient/chains/bitcoin/rpc/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@
"fmt"

"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient"
"github.com/pkg/errors"

"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin"
"github.com/zeta-chain/zetacore/zetaclient/chains/interfaces"
"github.com/zeta-chain/zetacore/zetaclient/config"
)

const (
// feeRateCountBackBlocks is the default number of blocks to look back for fee rate estimation
feeRateCountBackBlocks = 2

// defaultTestnetFeeRate is the default fee rate for testnet, 10 sat/byte
defaultTestnetFeeRate = 10
kingpinXD marked this conversation as resolved.
Show resolved Hide resolved
)

// NewRPCClient creates a new RPC client by the given config.
func NewRPCClient(btcConfig config.BTCConfig) (*rpcclient.Client, error) {
connCfg := &rpcclient.ConnConfig{
Expand Down Expand Up @@ -107,3 +117,42 @@
// res.Confirmations < 0 (meaning not included)
return btcjson.TxRawResult{}, fmt.Errorf("GetRawTxResult: tx %s not included yet", hash)
}

// GetRecentFeeRate gets the highest fee rate from recent blocks
func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Params) (uint64, error) {
blockNumber, err := rpcClient.GetBlockCount()
if err != nil {
return 0, err

Check warning on line 125 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L122-L125

Added lines #L122 - L125 were not covered by tests
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}

// get the highest fee rate among recent 'countBack' blocks to avoid underestimation
highestRate := int64(0)
for i := int64(0); i < feeRateCountBackBlocks; i++ {

Check warning on line 130 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L129-L130

Added lines #L129 - L130 were not covered by tests
// get the block
hash, err := rpcClient.GetBlockHash(blockNumber - i)
if err != nil {
return 0, err

Check warning on line 134 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L132-L134

Added lines #L132 - L134 were not covered by tests
}
block, err := rpcClient.GetBlockVerboseTx(hash)
if err != nil {
return 0, err

Check warning on line 138 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L136-L138

Added lines #L136 - L138 were not covered by tests
}

// computes the average fee rate of the block and take the higher rate
avgFeeRate, err := bitcoin.CalcBlockAvgFeeRate(block, netParams)
if err != nil {
return 0, err

Check warning on line 144 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L142-L144

Added lines #L142 - L144 were not covered by tests
}
if avgFeeRate > highestRate {
highestRate = avgFeeRate

Check warning on line 147 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L146-L147

Added lines #L146 - L147 were not covered by tests
}
}

// use 10 sat/byte as default estimation when fee rate is 0
if highestRate == 0 {
highestRate = defaultTestnetFeeRate

Check warning on line 153 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L152-L153

Added lines #L152 - L153 were not covered by tests
}

// #nosec G701 always in range
return uint64(highestRate), nil

Check warning on line 157 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L157

Added line #L157 was not covered by tests
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}
34 changes: 24 additions & 10 deletions zetaclient/chains/bitcoin/rpc/rpc_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (suite *BitcoinObserverTestSuite) SetupTest() {
base.DefaultLogger(), nil)
suite.Require().NoError(err)
suite.Require().NotNil(ob)
suite.rpcClient, err = getRPCClient(18332)
suite.rpcClient, err = createRPCClient(18332)
suite.Require().NoError(err)
skBytes, err := hex.DecodeString(skHex)
suite.Require().NoError(err)
Expand Down Expand Up @@ -91,13 +91,14 @@ func (suite *BitcoinObserverTestSuite) SetupTest() {
func (suite *BitcoinObserverTestSuite) TearDownSuite() {
}

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

// mainnet
if chainID == 8332 {
if chainID == chains.BitcoinMainnet.ChainId {
connCfg = &rpcclient.ConnConfig{
Host: rpcMainnet, // mainnet endpoint goes here
User: "user",
Expand All @@ -108,7 +109,7 @@ func getRPCClient(chainID int64) (*rpcclient.Client, error) {
}
}
// testnet3
if chainID == 18332 {
if chainID == chains.BitcoinTestnet.ChainId {
connCfg = &rpcclient.ConnConfig{
Host: rpcTestnet, // testnet endpoint goes here
User: "user",
Expand Down Expand Up @@ -218,6 +219,7 @@ func TestBitcoinObserverLive(t *testing.T) {
// LiveTestBitcoinFeeRate(t)
// LiveTestAvgFeeRateMainnetMempoolSpace(t)
// LiveTestAvgFeeRateTestnetMempoolSpace(t)
// LiveTestGetRecentFeeRate(t)
// LiveTestGetSenderByVin(t)
}

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

// the block hashes to test
Expand All @@ -265,7 +267,7 @@ func LiveTestGetBlockHeightByHash(t *testing.T) {
// and compares Conservative and Economical fee rates for different block targets (1 and 2)
func LiveTestBitcoinFeeRate(t *testing.T) {
// setup Bitcoin client
client, err := getRPCClient(8332)
client, err := createRPCClient(chains.BitcoinMainnet.ChainId)
require.NoError(t, err)
bn, err := client.GetBlockCount()
if err != nil {
Expand Down Expand Up @@ -390,7 +392,7 @@ func compareAvgFeeRate(t *testing.T, client *rpcclient.Client, startBlock int, e
// LiveTestAvgFeeRateMainnetMempoolSpace compares calculated fee rate with mempool.space fee rate for mainnet
func LiveTestAvgFeeRateMainnetMempoolSpace(t *testing.T) {
// setup Bitcoin client
client, err := getRPCClient(8332)
client, err := createRPCClient(chains.BitcoinMainnet.ChainId)
require.NoError(t, err)

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

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

// LiveTestGetRecentFeeRate gets the highest fee rate from recent blocks
func LiveTestGetRecentFeeRate(t *testing.T) {
// setup Bitcoin testnet client
client, err := createRPCClient(chains.BitcoinTestnet.ChainId)
require.NoError(t, err)

// get fee rate from recent blocks
feeRate, err := rpc.GetRecentFeeRate(client, &chaincfg.TestNet3Params)
require.NoError(t, err)
require.Greater(t, feeRate, uint64(0))
}

// LiveTestGetSenderByVin gets sender address for each vin and compares with mempool.space sender address
func LiveTestGetSenderByVin(t *testing.T) {
// setup Bitcoin client
chainID := int64(8332)
client, err := getRPCClient(chainID)
chainID := chains.BitcoinMainnet.ChainId
client, err := createRPCClient(chainID)
require.NoError(t, err)

// net params
Expand Down
Loading