Skip to content

Commit

Permalink
implementation of bitcoin depositor fee V2
Browse files Browse the repository at this point in the history
  • Loading branch information
ws4charlie committed Aug 22, 2024
1 parent 1d5cee1 commit ad4fc25
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 185 deletions.
72 changes: 71 additions & 1 deletion zetaclient/chains/bitcoin/fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/rs/zerolog"

"github.com/zeta-chain/zetacore/pkg/chains"
"github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc"
"github.com/zeta-chain/zetacore/zetaclient/chains/interfaces"
clientcommon "github.com/zeta-chain/zetacore/zetaclient/common"
)

Expand All @@ -35,7 +37,11 @@ const (
OutboundBytesMax = uint64(1543) // 1543v == EstimateSegWitTxSize(21, 2, toP2TR)
OutboundBytesAvg = uint64(245) // 245vB is a suggested gas limit for zetacore

DynamicDepositorFeeHeight = 834500 // DynamicDepositorFeeHeight contains the starting height (Bitcoin mainnet) from which dynamic depositor fee will take effect
DynamicDepositorFeeHeight = 834500 // the mainnet height from which dynamic depositor fee V1 is applied
DynamicDepositorFeeHeightV2 = 863400 // the mainnet height from which dynamic depositor fee V2 is applied

feeRateCountBackBlocks = 2 // the default number of blocks to look back for fee rate estimation
defaultTestnetFeeRate = 10 // the default fee rate for testnet, 10 sat/vB
)

var (
Expand Down Expand Up @@ -239,3 +245,67 @@ func CalcDepositorFee(

return DepositorFee(feeRate)
}

// CalcDepositorFeeV2 calculates the depositor fee for a given tx result
func CalcDepositorFeeV2(
rpcClient interfaces.BTCRPCClient,
rawResult *btcjson.TxRawResult,
netParams *chaincfg.Params,
) (float64, error) {
// use default fee for regnet
if netParams.Name == chaincfg.RegressionNetParams.Name {
return DefaultDepositorFee, nil
}

// get fee rate of the transaction
_, feeRate, err := rpc.GetTransactionFeeAndRate(rpcClient, rawResult)
if err != nil {
return 0, errors.Wrapf(err, "error getting fee rate for tx %s", rawResult.Txid)
}

// apply gas price multiplier
// #nosec G115 always in range
feeRate = int64(float64(feeRate) * clientcommon.BTCOutboundGasPriceMultiplier)

return DepositorFee(feeRate), nil
}

// GetRecentFeeRate gets the highest fee rate from recent blocks
// Note: this method should be used for testnet ONLY
func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Params) (uint64, error) {
blockNumber, err := rpcClient.GetBlockCount()
if err != nil {
return 0, err
}

// get the highest fee rate among recent 'countBack' blocks to avoid underestimation
highestRate := int64(0)
for i := int64(0); i < feeRateCountBackBlocks; i++ {
// get the block
hash, err := rpcClient.GetBlockHash(blockNumber - i)
if err != nil {
return 0, err
}
block, err := rpcClient.GetBlockVerboseTx(hash)
if err != nil {
return 0, err
}

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

// use 10 sat/byte as default estimation if recent fee rate drops to 0
if highestRate == 0 {
highestRate = defaultTestnetFeeRate
}

// #nosec G115 always in range
return uint64(highestRate), nil
}
12 changes: 12 additions & 0 deletions zetaclient/chains/bitcoin/observer/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,18 @@ func GetBtcEvent(
return nil, nil
}

// switch to depositor fee V2 if
// 1. it is bitcoin testnet, or
// 2. it is bitcoin mainnet and upgrade height is reached
// TODO: remove CalcDepositorFeeV1 and below conditions after the upgrade height
if netParams.Name == chaincfg.TestNet3Params.Name ||
(netParams.Name == chaincfg.MainNetParams.Name && blockNumber >= bitcoin.DynamicDepositorFeeHeightV2) {
depositorFee, err = bitcoin.CalcDepositorFeeV2(rpcClient, &tx, netParams)
if err != nil {
return nil, errors.Wrapf(err, "error calculating depositor fee V2 for inbound: %s", tx.Txid)
}
}

// deposit amount has to be no less than the minimum depositor fee
if vout0.Value < depositorFee {
logger.Info().
Expand Down
3 changes: 1 addition & 2 deletions zetaclient/chains/bitcoin/observer/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
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/db"
"github.com/zeta-chain/zetacore/zetaclient/metrics"
Expand Down Expand Up @@ -628,7 +627,7 @@ func (ob *Observer) specialHandleFeeRate() (uint64, error) {
// hardcode gas price for regnet
return 1, nil
case chains.NetworkType_testnet:
feeRateEstimated, err := rpc.GetRecentFeeRate(ob.btcClient, ob.netParams)
feeRateEstimated, err := bitcoin.GetRecentFeeRate(ob.btcClient, ob.netParams)
if err != nil {
return 0, errors.Wrapf(err, "error GetRecentFeeRate")
}
Expand Down
89 changes: 50 additions & 39 deletions zetaclient/chains/bitcoin/rpc/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,15 @@ import (
"fmt"

"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcutil"
"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
)

// 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 @@ -63,6 +54,20 @@ func GetTxResultByHash(
return hash, txResult, nil
}

// GetTXRawResultByHash gets the raw transaction by hash
func GetRawTxByHash(rpcClient interfaces.BTCRPCClient, txID string) (*btcutil.Tx, error) {
hash, err := chainhash.NewHashFromStr(txID)
if err != nil {
return nil, errors.Wrapf(err, "GetRawTxByHash: error NewHashFromStr: %s", txID)
}

tx, err := rpcClient.GetRawTransaction(hash)
if err != nil {
return nil, errors.Wrapf(err, "GetRawTxByHash: error GetRawTransaction %s", txID)
}
return tx, nil
}

// GetBlockHeightByHash gets the block height by block hash
func GetBlockHeightByHash(
rpcClient interfaces.BTCRPCClient,
Expand Down Expand Up @@ -118,42 +123,48 @@ func GetRawTxResult(
return btcjson.TxRawResult{}, fmt.Errorf("GetRawTxResult: tx %s not included yet", hash)
}

// GetRecentFeeRate gets the highest fee rate from recent blocks
// Note: this method is only used for testnet
func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Params) (uint64, error) {
blockNumber, err := rpcClient.GetBlockCount()
if err != nil {
return 0, err
}
// GetTransactionFeeAndRate gets the transaction fee and rate for a given tx result
func GetTransactionFeeAndRate(rpcClient interfaces.BTCRPCClient, rawResult *btcjson.TxRawResult) (int64, int64, error) {
var (
totalInputValue int64
totalOutputValue int64
)

// get the highest fee rate among recent 'countBack' blocks to avoid underestimation
highestRate := int64(0)
for i := int64(0); i < feeRateCountBackBlocks; i++ {
// get the block
hash, err := rpcClient.GetBlockHash(blockNumber - i)
if err != nil {
return 0, err
}
block, err := rpcClient.GetBlockVerboseTx(hash)
// sum up total input value
for _, vin := range rawResult.Vin {
prevTx, err := GetRawTxByHash(rpcClient, vin.Txid)
if err != nil {
return 0, err
return 0, 0, errors.Wrapf(err, "failed to get previous tx: %s", vin.Txid)
}
totalInputValue += prevTx.MsgTx().TxOut[vin.Vout].Value
}

// 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
}
if avgFeeRate > highestRate {
highestRate = avgFeeRate
}
// query the raw tx
tx, err := GetRawTxByHash(rpcClient, rawResult.Txid)
if err != nil {
return 0, 0, errors.Wrapf(err, "failed to get tx: %s", rawResult.Txid)
}

// use 10 sat/byte as default estimation if recent fee rate drops to 0
if highestRate == 0 {
highestRate = defaultTestnetFeeRate
// sum up total output value
for _, vout := range tx.MsgTx().TxOut {
totalOutputValue += vout.Value
}

// calculate the transaction fee in satoshis
fee := totalInputValue - totalOutputValue
if fee < 0 { // never happens
return 0, 0, fmt.Errorf("got negative fee: %d", fee)
}

// Note: the calculation uses 'Vsize' returned by RPC to simplify dev experience:
// - 1. the devs could use the same value returned by their RPC endpoints to estimate deposit fee.
// - 2. the devs don't have to bother 'Vsize' calculation, even though there is more accurate formula.
// Moreoever, the accurate 'Vsize' is usually an adjusted size (float value) by Bitcoin Core.
// - 3. the 'Vsize' calculation could depend on program language and the library used.
//
// calculate the fee rate in satoshis/vByte
// #nosec G115 always in range
return uint64(highestRate), nil
feeRate := fee / int64(rawResult.Vsize)

return fee, feeRate, nil
}
Loading

0 comments on commit ad4fc25

Please sign in to comment.