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

feat: bitcoin depositor fee V2 to achieve better dev experience #2765

Merged
merged 9 commits into from
Aug 27, 2024
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing
* [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw
* [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription
* [2765](https://github.com/zeta-chain/node/pull/2765) - bitcoin depositor fee improvement

### Refactor

Expand Down
112 changes: 95 additions & 17 deletions zetaclient/chains/bitcoin/fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,41 @@
"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"
)

const (
bytesPerKB = 1000
bytesPerInput = 41 // each input is 41 bytes
bytesPerOutputP2TR = 43 // each P2TR output is 43 bytes
bytesPerOutputP2WSH = 43 // each P2WSH output is 43 bytes
bytesPerOutputP2WPKH = 31 // each P2WPKH output is 31 bytes
bytesPerOutputP2SH = 32 // each P2SH output is 32 bytes
bytesPerOutputP2PKH = 34 // each P2PKH output is 34 bytes
bytesPerOutputAvg = 37 // average size of all above types of outputs (36.6 bytes)
bytes1stWitness = 110 // the 1st witness incurs about 110 bytes and it may vary
bytesPerWitness = 108 // each additional witness incurs about 108 bytes and it may vary
defaultDepositorFeeRate = 20 // 20 sat/byte is the default depositor fee rate

OutboundBytesMin = uint64(239) // 239vB == EstimateSegWitTxSize(2, 2, toP2WPKH)
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
// constants related to transaction size calculations
bytesPerKB = 1000
bytesPerInput = 41 // each input is 41 bytes
bytesPerOutputP2TR = 43 // each P2TR output is 43 bytes
bytesPerOutputP2WSH = 43 // each P2WSH output is 43 bytes
bytesPerOutputP2WPKH = 31 // each P2WPKH output is 31 bytes
bytesPerOutputP2SH = 32 // each P2SH output is 32 bytes
bytesPerOutputP2PKH = 34 // each P2PKH output is 34 bytes
bytesPerOutputAvg = 37 // average size of all above types of outputs (36.6 bytes)
bytes1stWitness = 110 // the 1st witness incurs about 110 bytes and it may vary
bytesPerWitness = 108 // each additional witness incurs about 108 bytes and it may vary
OutboundBytesMin = uint64(239) // 239vB == EstimateSegWitTxSize(2, 2, toP2WPKH)
OutboundBytesMax = uint64(1543) // 1543v == EstimateSegWitTxSize(21, 2, toP2TR)
OutboundBytesAvg = uint64(245) // 245vB is a suggested gas limit for zetacore

// defaultDepositorFeeRate is the default fee rate for depositor fee, 20 sat/vB
defaultDepositorFeeRate = 20

// defaultTestnetFeeRate is the default fee rate for testnet, 10 sat/vB
defaultTestnetFeeRate = 10

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

// DynamicDepositorFeeHeight is the mainnet height from which dynamic depositor fee V1 is applied
DynamicDepositorFeeHeight = 834500

// DynamicDepositorFeeHeightV2 is the mainnet height from which dynamic depositor fee V2 is applied
DynamicDepositorFeeHeightV2 = 863400
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
)

var (
Expand Down Expand Up @@ -239,3 +253,67 @@

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) {

Check warning on line 262 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L262

Added line #L262 was not covered by tests
// use default fee for regnet
if netParams.Name == chaincfg.RegressionNetParams.Name {
return DefaultDepositorFee, nil

Check warning on line 265 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L264-L265

Added lines #L264 - L265 were not covered by tests
}

// 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)

Check warning on line 271 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L269-L271

Added lines #L269 - L271 were not covered by tests
}

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

Check warning on line 276 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L276

Added line #L276 was not covered by tests

return DepositorFee(feeRate), nil

Check warning on line 278 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L278

Added line #L278 was not covered by tests
}

// 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

Check warning on line 286 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L283-L286

Added lines #L283 - L286 were not covered by tests
}

// 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 291 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L290-L291

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

Check warning on line 295 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L293-L295

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

Check warning on line 299 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L297-L299

Added lines #L297 - L299 were not covered by tests
}

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

Check warning on line 305 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L303-L305

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

Check warning on line 308 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L307-L308

Added lines #L307 - L308 were not covered by tests
}
}

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

Check warning on line 314 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L313-L314

Added lines #L313 - L314 were not covered by tests
}

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

Check warning on line 318 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L318

Added line #L318 was not covered by tests
}
13 changes: 13 additions & 0 deletions zetaclient/chains/bitcoin/observer/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,19 @@
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
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
// https://github.com/zeta-chain/node/issues/2766
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)

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/observer/inbound.go#L431-L433

Added lines #L431 - L433 were not covered by tests
}
}

// 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 @@
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 @@
// 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)

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L630 was not covered by tests
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 @@
"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 @@
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)

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L58-L61

Added lines #L58 - L61 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L64-L66

Added lines #L64 - L66 were not covered by tests
}
return tx, nil

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L68 was not covered by tests
}

// GetBlockHeightByHash gets the block height by block hash
func GetBlockHeightByHash(
rpcClient interfaces.BTCRPCClient,
Expand Down Expand Up @@ -118,42 +123,48 @@
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
)

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L127-L131

Added lines #L127 - L131 were not covered by tests

// 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)

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L134 - L135 were not covered by tests
if err != nil {
return 0, err
return 0, 0, errors.Wrapf(err, "failed to get previous tx: %s", vin.Txid)

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L137 was not covered by tests
}
totalInputValue += prevTx.MsgTx().TxOut[vin.Vout].Value

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L139 was 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
}
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)

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L143-L145

Added lines #L143 - L145 were not covered by tests
}

// 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

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L149-L150

Added lines #L149 - L150 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L154-L156

Added lines #L154 - L156 were not covered by tests
}

// 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)

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L167 was not covered by tests

return fee, feeRate, nil

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L169 was not covered by tests
}
Loading
Loading