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: WIP (DON'T review, code will change a lot) bitcoin transaction replace by fee #3306

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ require (
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
Expand Down
16 changes: 14 additions & 2 deletions x/crosschain/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ func (k Keeper) IterateAndUpdateCctxGasPrice(

IterateChains:
for _, chain := range chains {
// support only external evm chains
if zetachains.IsEVMChain(chain.ChainId, additionalChains) && !zetachains.IsZetaChain(chain.ChainId, additionalChains) {
// support only external evm chains and bitcoin chain
if IsGasStabilityPoolEnabledChain(chain.ChainId, additionalChains) {
res, err := k.ListPendingCctx(sdk.UnwrapSDKContext(ctx), &types.QueryListPendingCctxRequest{
ChainId: chain.ChainId,
Limit: gasPriceIncreaseFlags.MaxPendingCctxs,
Expand Down Expand Up @@ -175,3 +175,15 @@ func CheckAndUpdateCctxGasPrice(

return gasPriceIncrease, additionalFees, nil
}

// IsGasStabilityPoolEnabledChain returns true if given chainID is enabled for gas stability pool
func IsGasStabilityPoolEnabledChain(chainID int64, additionalChains []zetachains.Chain) bool {
switch {
case zetachains.IsEVMChain(chainID, additionalChains):
return !zetachains.IsZetaChain(chainID, additionalChains)
case zetachains.IsBitcoinChain(chainID, additionalChains):
return true
default:
return false
}
}
46 changes: 46 additions & 0 deletions x/crosschain/keeper/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/zeta-chain/node/pkg/chains"
zetachains "github.com/zeta-chain/node/pkg/chains"
testkeeper "github.com/zeta-chain/node/testutil/keeper"
"github.com/zeta-chain/node/testutil/sample"
"github.com/zeta-chain/node/x/crosschain/keeper"
Expand Down Expand Up @@ -449,3 +450,48 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) {
})
}
}

func TestIsGasStabilityPoolEnabledChain(t *testing.T) {
tests := []struct {
name string
chainID int64
expected bool
}{
{
name: "Ethereum is enabled",
chainID: chains.Ethereum.ChainId,
expected: true,
},
{
name: "Binance Smart Chain is enabled",
chainID: chains.BscMainnet.ChainId,
expected: true,
},
{
name: "Bitcoin is enabled",
chainID: chains.BitcoinMainnet.ChainId,
expected: true,
},
{
name: "ZetaChain is not enabled",
chainID: chains.ZetaChainMainnet.ChainId,
expected: false,
},
{
name: "Solana is not enabled",
chainID: chains.SolanaMainnet.ChainId,
expected: false,
},
{
name: "TON is not enabled",
chainID: chains.TONMainnet.ChainId,
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, keeper.IsGasStabilityPoolEnabledChain(tt.chainID, []zetachains.Chain{}))
})
}
}
2 changes: 1 addition & 1 deletion x/observer/types/crosschain_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import "time"

var DefaultGasPriceIncreaseFlags = GasPriceIncreaseFlags{
// EpochLength is the number of blocks in an epoch before triggering a gas price increase

EpochLength: 100,

// RetryInterval is the number of blocks to wait before incrementing the gas price again
RetryInterval: time.Minute * 10,

Expand Down
62 changes: 26 additions & 36 deletions zetaclient/chains/bitcoin/fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/hex"
"fmt"
"math"
"math/big"

"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcjson"
Expand All @@ -20,19 +19,17 @@ import (

const (
// 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
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 = int64(239) // 239vB == EstimateSegWitTxSize(2, 2, toP2WPKH)
OutboundBytesMax = int64(1543) // 1543v == EstimateSegWitTxSize(21, 2, toP2TR)

// defaultDepositorFeeRate is the default fee rate for depositor fee, 20 sat/vB
defaultDepositorFeeRate = 20
Expand All @@ -49,6 +46,7 @@ var (
BtcOutboundBytesDepositor = OutboundSizeDepositor()

// BtcOutboundBytesWithdrawer is the outbound size incurred by the withdrawer: 177vB
// This will be the suggested gas limit used for zetacore
BtcOutboundBytesWithdrawer = OutboundSizeWithdrawer()

// DefaultDepositorFee is the default depositor fee is 0.00001360 BTC (20 * 68vB / 100000000)
Expand All @@ -59,34 +57,27 @@ var (
// DepositorFeeCalculator is a function type to calculate the Bitcoin depositor fee
type DepositorFeeCalculator func(interfaces.BTCRPCClient, *btcjson.TxRawResult, *chaincfg.Params) (float64, error)

// FeeRateToSatPerByte converts a fee rate in BTC/KB to sat/byte.
func FeeRateToSatPerByte(rate float64) *big.Int {
// #nosec G115 always in range
satPerKB := new(big.Int).SetInt64(int64(rate * btcutil.SatoshiPerBitcoin))
return new(big.Int).Div(satPerKB, big.NewInt(bytesPerKB))
}

// WiredTxSize calculates the wired tx size in bytes
func WiredTxSize(numInputs uint64, numOutputs uint64) uint64 {
func WiredTxSize(numInputs uint64, numOutputs uint64) int64 {
// Version 4 bytes + LockTime 4 bytes + Serialized varint size for the
// number of transaction inputs and outputs.
// #nosec G115 always positive
return uint64(8 + wire.VarIntSerializeSize(numInputs) + wire.VarIntSerializeSize(numOutputs))
return int64(8 + wire.VarIntSerializeSize(numInputs) + wire.VarIntSerializeSize(numOutputs))
}

// EstimateOutboundSize estimates the size of an outbound in vBytes
func EstimateOutboundSize(numInputs uint64, payees []btcutil.Address) (uint64, error) {
func EstimateOutboundSize(numInputs int64, payees []btcutil.Address) (int64, error) {
if numInputs == 0 {
return 0, nil
}
// #nosec G115 always positive
numOutputs := 2 + uint64(len(payees))
bytesWiredTx := WiredTxSize(numInputs, numOutputs)
bytesWiredTx := WiredTxSize(uint64(numInputs), numOutputs)
bytesInput := numInputs * bytesPerInput
bytesOutput := uint64(2) * bytesPerOutputP2WPKH // new nonce mark, change
bytesOutput := int64(2) * bytesPerOutputP2WPKH // new nonce mark, change

// calculate the size of the outputs to payees
bytesToPayees := uint64(0)
bytesToPayees := int64(0)
for _, to := range payees {
sizeOutput, err := GetOutputSizeByAddress(to)
if err != nil {
Expand All @@ -104,7 +95,7 @@ func EstimateOutboundSize(numInputs uint64, payees []btcutil.Address) (uint64, e
}

// GetOutputSizeByAddress returns the size of a tx output in bytes by the given address
func GetOutputSizeByAddress(to btcutil.Address) (uint64, error) {
func GetOutputSizeByAddress(to btcutil.Address) (int64, error) {
switch addr := to.(type) {
case *btcutil.AddressTaproot:
if addr == nil {
Expand Down Expand Up @@ -137,16 +128,16 @@ func GetOutputSizeByAddress(to btcutil.Address) (uint64, error) {
}

// OutboundSizeDepositor returns outbound size (68vB) incurred by the depositor
func OutboundSizeDepositor() uint64 {
func OutboundSizeDepositor() int64 {
return bytesPerInput + bytesPerWitness/blockchain.WitnessScaleFactor
}

// OutboundSizeWithdrawer returns outbound size (177vB) incurred by the withdrawer (1 input, 3 outputs)
func OutboundSizeWithdrawer() uint64 {
func OutboundSizeWithdrawer() int64 {
bytesWiredTx := WiredTxSize(1, 3)
bytesInput := uint64(1) * bytesPerInput // nonce mark
bytesOutput := uint64(2) * bytesPerOutputP2WPKH // 2 P2WPKH outputs: new nonce mark, change
bytesOutput += bytesPerOutputAvg // 1 output to withdrawer's address
bytesInput := int64(1) * bytesPerInput // nonce mark
bytesOutput := int64(2) * bytesPerOutputP2WPKH // 2 P2WPKH outputs: new nonce mark, change
bytesOutput += bytesPerOutputAvg // 1 output to withdrawer's address

return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor
}
Expand Down Expand Up @@ -246,7 +237,7 @@ func CalcDepositorFee(

// 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) {
func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Params) (int64, error) {
// should avoid using this method for mainnet
if netParams.Name == chaincfg.MainNetParams.Name {
return 0, errors.New("GetRecentFeeRate should not be used for mainnet")
Expand Down Expand Up @@ -286,6 +277,5 @@ func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Par
highestRate = defaultTestnetFeeRate
}

// #nosec G115 always in range
return uint64(highestRate), nil
return highestRate, nil
}
23 changes: 11 additions & 12 deletions zetaclient/chains/bitcoin/fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ func TestOutboundSize2In3Out(t *testing.T) {

// Estimate the tx size in vByte
// #nosec G115 always positive
vError := uint64(1) // 1 vByte error tolerance
vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor)
vBytesEstimated, err := EstimateOutboundSize(uint64(len(utxosTxids)), []btcutil.Address{payee})
vError := int64(1) // 1 vByte error tolerance
vBytes := blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor
vBytesEstimated, err := EstimateOutboundSize(int64(len(utxosTxids)), []btcutil.Address{payee})
require.NoError(t, err)
if vBytes > vBytesEstimated {
require.True(t, vBytes-vBytesEstimated <= vError)
Expand All @@ -219,9 +219,9 @@ func TestOutboundSize21In3Out(t *testing.T) {

// Estimate the tx size in vByte
// #nosec G115 always positive
vError := uint64(21 / 4) // 5 vBytes error tolerance
vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor)
vBytesEstimated, err := EstimateOutboundSize(uint64(len(exampleTxids)), []btcutil.Address{payee})
vError := int64(21 / 4) // 5 vBytes error tolerance
vBytes := blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor
vBytesEstimated, err := EstimateOutboundSize(int64(len(exampleTxids)), []btcutil.Address{payee})
require.NoError(t, err)
if vBytes > vBytesEstimated {
require.True(t, vBytes-vBytesEstimated <= vError)
Expand All @@ -243,11 +243,11 @@ func TestOutboundSizeXIn3Out(t *testing.T) {

// Estimate the tx size
// #nosec G115 always positive
vError := uint64(
vError := int64(
0.25 + float64(x)/4,
) // 1st witness incurs 0.25 more vByte error than others (which incurs 1/4 vByte per witness)
vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor)
vBytesEstimated, err := EstimateOutboundSize(uint64(len(exampleTxids[:x])), []btcutil.Address{payee})
vBytes := blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor
vBytesEstimated, err := EstimateOutboundSize(int64(len(exampleTxids[:x])), []btcutil.Address{payee})
require.NoError(t, err)
if vBytes > vBytesEstimated {
require.True(t, vBytes-vBytesEstimated <= vError)
Expand Down Expand Up @@ -413,14 +413,14 @@ func TestOutboundSizeBreakdown(t *testing.T) {
}

// add all outbound sizes paying to each address
txSizeTotal := uint64(0)
txSizeTotal := int64(0)
for _, payee := range payees {
sizeOutput, err := EstimateOutboundSize(2, []btcutil.Address{payee})
require.NoError(t, err)
txSizeTotal += sizeOutput
}

// calculate the average outbound size
// calculate the average outbound size (245 vByte)
// #nosec G115 always in range
txSizeAverage := uint64((float64(txSizeTotal))/float64(len(payees)) + 0.5)

Expand All @@ -433,7 +433,6 @@ func TestOutboundSizeBreakdown(t *testing.T) {
require.Equal(t, uint64(177), txSizeWithdrawer)

// total outbound size == (deposit fee + withdrawer fee), 245 = 68 + 177
require.Equal(t, OutboundBytesAvg, txSizeAverage)
require.Equal(t, txSizeAverage, txSizeDepositor+txSizeWithdrawer)

// check default depositor fee
Expand Down
65 changes: 65 additions & 0 deletions zetaclient/chains/bitcoin/observer/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package observer

import (
"github.com/pkg/errors"

"github.com/zeta-chain/node/pkg/chains"
clienttypes "github.com/zeta-chain/node/zetaclient/types"
)

// SaveBroadcastedTx saves successfully broadcasted transaction
func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) {
outboundID := ob.OutboundID(nonce)
ob.Mu().Lock()
ob.broadcastedTx[outboundID] = txHash
ob.Mu().Unlock()

broadcastEntry := clienttypes.ToOutboundHashSQLType(txHash, outboundID)
if err := ob.DB().Client().Save(&broadcastEntry).Error; err != nil {
ob.logger.Outbound.Error().
Err(err).
Msgf("SaveBroadcastedTx: error saving broadcasted txHash %s for outbound %s", txHash, outboundID)
}
ob.logger.Outbound.Info().Msgf("SaveBroadcastedTx: saved broadcasted txHash %s for outbound %s", txHash, outboundID)
}

// LoadLastBlockScanned loads the last scanned block from the database
func (ob *Observer) LoadLastBlockScanned() error {
err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain)
if err != nil {
return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId)
}

// observer will scan from the last block when 'lastBlockScanned == 0', this happens when:
// 1. environment variable is set explicitly to "latest"
// 2. environment variable is empty and last scanned block is not found in DB
if ob.LastBlockScanned() == 0 {
blockNumber, err := ob.btcClient.GetBlockCount()
if err != nil {
return errors.Wrapf(err, "error GetBlockCount for chain %d", ob.Chain().ChainId)
}
// #nosec G115 always positive
ob.WithLastBlockScanned(uint64(blockNumber))
}

// bitcoin regtest starts from hardcoded block 100
if chains.IsBitcoinRegnet(ob.Chain().ChainId) {
ob.WithLastBlockScanned(RegnetStartBlock)
}
ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned())

return nil
}

// LoadBroadcastedTxMap loads broadcasted transactions from the database
func (ob *Observer) LoadBroadcastedTxMap() error {
var broadcastedTransactions []clienttypes.OutboundHashSQLType
if err := ob.DB().Client().Find(&broadcastedTransactions).Error; err != nil {
ob.logger.Chain.Error().Err(err).Msgf("error iterating over db for chain %d", ob.Chain().ChainId)
return err
}
for _, entry := range broadcastedTransactions {
ob.broadcastedTx[entry.Key] = entry.Hash
}
return nil
}
Loading
Loading