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
2 changes: 1 addition & 1 deletion e2e/runner/solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (r *E2ERunner) CreateSignedTransaction(
privateKey solana.PrivateKey,
) *solana.Transaction {
// get a recent blockhash
recent, err := r.SolanaClient.GetRecentBlockhash(r.Ctx, rpc.CommitmentFinalized)
recent, err := r.SolanaClient.GetLatestBlockhash(r.Ctx, rpc.CommitmentFinalized)
require.NoError(r, err)

// create the initialize transaction
Expand Down
119 changes: 102 additions & 17 deletions zetaclient/chains/bitcoin/fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,42 @@
"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
// Height 863400 is approximately a month away (2024-09-28) from the time of writing, allowing enough time for the upgrade
DynamicDepositorFeeHeightV2 = 863400
)

var (
Expand Down Expand Up @@ -239,3 +254,73 @@

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L263

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L265 - L266 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 272 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L270-L272

Added lines #L270 - L272 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L277

Added line #L277 was not covered by tests

return DepositorFee(feeRate), nil

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L279

Added line #L279 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) {

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L284

Added line #L284 was not covered by tests
// should avoid using this method for mainnet
if netParams.Name == chaincfg.MainNetParams.Name {
return 0, errors.New("GetRecentFeeRate should not be used for mainnet")

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

View check run for this annotation

Codecov / codecov/patch

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

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

// get the current block number
blockNumber, err := rpcClient.GetBlockCount()
if err != nil {
return 0, err

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L291 - L293 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 298 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L300-L302

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

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L304-L306

Added lines #L304 - L306 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 312 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L310-L312

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L314 - L315 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 321 in zetaclient/chains/bitcoin/fee.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L320-L321

Added lines #L320 - L321 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/fee.go#L325

Added line #L325 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
92 changes: 54 additions & 38 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,53 @@
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

// make sure the tx Vsize is not zero (should not happen)
if rawResult.Vsize <= 0 {
return 0, 0, fmt.Errorf("tx %s has non-positive Vsize: %d", rawResult.Txid, rawResult.Vsize)

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
}

// 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 140 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L139 - L140 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 142 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

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

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

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#L144

Added line #L144 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 150 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L148 - L150 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 155 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L154 - L155 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 161 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/rpc/rpc.go#L159-L161

Added lines #L159 - L161 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 172 in zetaclient/chains/bitcoin/rpc/rpc.go

View check run for this annotation

Codecov / codecov/patch

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

Added line #L172 was not covered by tests

return fee, feeRate, nil

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

View check run for this annotation

Codecov / codecov/patch

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

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