Skip to content

Commit 6e660cc

Browse files
authored
fix: fix SegWit tx size estimation (in vByte) and gas fee calculation (#1669)
* fix SegWit tx size and gas fee calculation * define outTxBytesMin and outTxBytesMax as constants
1 parent be1c040 commit 6e660cc

File tree

4 files changed

+113
-76
lines changed

4 files changed

+113
-76
lines changed

changelog.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
## Unreleased
44

5-
### Fixes
65
### Fixes
76

87
* [1642](https://github.com/zeta-chain/node/pull/1642) - Change WhitelistERC20 authorization from group1 to group2
98
* [1610](https://github.com/zeta-chain/node/issues/1610) - add pending outtx hash to tracker after monitoring for 10 minutes
109
* [1656](https://github.com/zeta-chain/node/issues/1656) - schedule bitcoin keysign with intervals to avoid keysign failures
10+
* [1661](https://github.com/zeta-chain/node/issues/1661) - use estimated SegWit tx size for Bitcoin gas fee calculation
11+
* [1667](https://github.com/zeta-chain/node/issues/1667) - estimate SegWit tx size in uinit of vByte
1112

1213
## Version: v12.1.0
1314

zetaclient/btc_signer.go

+6-14
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,11 @@ import (
2323

2424
const (
2525
maxNoOfInputsPerTx = 20
26-
consolidationRank = 10 // the rank below (or equal to) which we consolidate UTXOs
26+
consolidationRank = 10 // the rank below (or equal to) which we consolidate UTXOs
27+
outTxBytesMin = uint64(239) // 239vB == EstimateSegWitTxSize(2, 3)
28+
outTxBytesMax = uint64(1531) // 1531v == EstimateSegWitTxSize(21, 3)
2729
)
2830

29-
var (
30-
outTxBytesMin uint64
31-
outTxBytesMax uint64
32-
)
33-
34-
func init() {
35-
outTxBytesMin = EstimateSegWitTxSize(2, 3) // 403B, estimated size for a 2-input, 3-output SegWit tx
36-
outTxBytesMax = EstimateSegWitTxSize(21, 3) // 3234B, estimated size for a 21-input, 3-output SegWit tx
37-
}
38-
3931
type BTCSigner struct {
4032
tssSigner TSSSigner
4133
rpcClient BTCRPCClient
@@ -114,9 +106,9 @@ func (signer *BTCSigner) SignWithdrawTx(
114106

115107
// size checking
116108
// #nosec G701 always positive
117-
txSize := uint64(tx.SerializeSize())
118-
if txSize > sizeLimit { // ZRC20 'withdraw' charged less fee from end user
119-
signer.logger.Info().Msgf("sizeLimit %d is less than txSize %d for nonce %d", sizeLimit, txSize, nonce)
109+
txSize := EstimateSegWitTxSize(uint64(len(prevOuts)), 3)
110+
if sizeLimit < BtcOutTxBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user
111+
signer.logger.Info().Msgf("sizeLimit %d is less than BtcOutTxBytesWithdrawer %d for nonce %d", sizeLimit, txSize, nonce)
120112
}
121113
if txSize < outTxBytesMin { // outbound shouldn't be blocked a low sizeLimit
122114
signer.logger.Warn().Msgf("txSize %d is less than outTxBytesMin %d; use outTxBytesMin", txSize, outTxBytesMin)

zetaclient/btc_signer_test.go

+80-47
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sync"
1010
"testing"
1111

12+
"github.com/btcsuite/btcd/blockchain"
1213
"github.com/btcsuite/btcd/btcec"
1314
"github.com/btcsuite/btcd/btcjson"
1415
"github.com/btcsuite/btcd/chaincfg"
@@ -31,6 +32,31 @@ type BTCSignerSuite struct {
3132

3233
var _ = Suite(&BTCSignerSuite{})
3334

35+
// 21 example UTXO txids to use in the test.
36+
var exampleTxids = []string{
37+
"c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e",
38+
"54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa",
39+
"b18a55a34319cfbedebfcfe1a80fef2b92ad8894d06caf8293a0344824c2cfbc",
40+
"969fb309a4df7c299972700da788b5d601c0c04bab4ab46fff79d0335a7d75de",
41+
"6c71913061246ffc20e268c1b0e65895055c36bfbf1f8faf92dcad6f8242121e",
42+
"ba6d6e88cb5a97556684a1232719a3ffe409c5c9501061e1f59741bc412b3585",
43+
"69b56c3c8c5d1851f9eaec256cd49f290b477a5d43e2aef42ef25d3c1d9f4b33",
44+
"b87effd4cb46fe1a575b5b1ba0289313dc9b4bc9e615a3c6cbc0a14186921fdf",
45+
"3135433054523f5e220621c9e3d48efbbb34a6a2df65635c2a3e7d462d3e1cda",
46+
"8495c22a9ce6359ab53aa048c13b41c64fdf5fe141f516ba2573cc3f9313f06e",
47+
"f31583544b475370d7b9187c9a01b92e44fb31ac5fcfa7fc55565ac64043aa9a",
48+
"c03d55f9f717c1df978623e2e6b397b720999242f9ead7db9b5988fee3fb3933",
49+
"ee55688439b47a5410cdc05bac46be0094f3af54d307456fdfe6ba8caf336e0b",
50+
"61895f86c70f0bc3eef55d9a00347b509fa90f7a344606a9774be98a3ee9e02a",
51+
"ffabb401a19d04327bd4a076671d48467dbcde95459beeab23df21686fd01525",
52+
"b7e1c03b9b73e4e90fc06da893072c5604203c49e66699acbb2f61485d822981",
53+
"185614d21973990138e478ce10e0a4014352df58044276d4e4c0093aa140f482",
54+
"4a2800f13d15dc0c82308761d6fe8f6d13b65e42d7ca96a42a3a7048830e8c55",
55+
"fb98f52e91db500735b185797cebb5848afbfe1289922d87e03b98c3da5b85ef",
56+
"7901c5e36d9e8456ac61b29b82048650672a889596cbd30a9f8910a589ffc5b3",
57+
"6bcd0850fd2fa1404290ed04d78d4ae718414f16d4fbfd344951add8dcf60326",
58+
}
59+
3460
func (s *BTCSignerSuite) SetUpTest(c *C) {
3561
// test private key with EVM address
3662
//// EVM: 0x236C7f53a90493Bb423411fe4117Cb4c2De71DfB
@@ -219,11 +245,10 @@ func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, []b
219245

220246
func addTxInputs(t *testing.T, tx *wire.MsgTx, txids []string) {
221247
preTxSize := tx.SerializeSize()
222-
require.Equal(t, bytesEmptyTx, preTxSize)
223-
for i, txid := range txids {
248+
for _, txid := range txids {
224249
hash, err := chainhash.NewHashFromStr(txid)
225250
require.Nil(t, err)
226-
outpoint := wire.NewOutPoint(hash, uint32(i%3))
251+
outpoint := wire.NewOutPoint(hash, uint32(rand.Intn(100)))
227252
txIn := wire.NewTxIn(outpoint, nil, nil)
228253
tx.AddTxIn(txIn)
229254
require.Equal(t, bytesPerInput, tx.SerializeSize()-preTxSize)
@@ -307,63 +332,71 @@ func TestP2WPHSize2In3Out(t *testing.T) {
307332
// Payer sign the redeeming transaction.
308333
signTx(t, tx, payerScript, privateKey)
309334

310-
// Estimate the tx size
335+
// Estimate the tx size in vByte
311336
// #nosec G701 always positive
312-
txSize := uint64(tx.SerializeSize())
313-
sizeEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3)
314-
require.Equal(t, outTxBytesMin, sizeEstimated)
315-
require.True(t, outTxBytesMin >= txSize)
316-
require.True(t, outTxBytesMin-txSize <= 2) // 2 witness may vary
337+
vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor)
338+
vBytesEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3)
339+
require.Equal(t, vBytes, vBytesEstimated)
340+
require.Equal(t, vBytes, outTxBytesMin)
317341
}
318342

319343
func TestP2WPHSize21In3Out(t *testing.T) {
320344
// Generate payer/payee private keys and P2WPKH addresss
321345
privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params)
322346
_, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params)
323347

324-
// 21 example UTXO txids to use in the test.
325-
utxosTxids := []string{
326-
"c1729638e1c9b6bfca57d11bf93047d98b65594b0bf75d7ee68bf7dc80dc164e",
327-
"54f9ebbd9e3ad39a297da54bf34a609b6831acbea0361cb5b7b5c8374f5046aa",
328-
"b18a55a34319cfbedebfcfe1a80fef2b92ad8894d06caf8293a0344824c2cfbc",
329-
"969fb309a4df7c299972700da788b5d601c0c04bab4ab46fff79d0335a7d75de",
330-
"6c71913061246ffc20e268c1b0e65895055c36bfbf1f8faf92dcad6f8242121e",
331-
"ba6d6e88cb5a97556684a1232719a3ffe409c5c9501061e1f59741bc412b3585",
332-
"69b56c3c8c5d1851f9eaec256cd49f290b477a5d43e2aef42ef25d3c1d9f4b33",
333-
"b87effd4cb46fe1a575b5b1ba0289313dc9b4bc9e615a3c6cbc0a14186921fdf",
334-
"3135433054523f5e220621c9e3d48efbbb34a6a2df65635c2a3e7d462d3e1cda",
335-
"8495c22a9ce6359ab53aa048c13b41c64fdf5fe141f516ba2573cc3f9313f06e",
336-
"f31583544b475370d7b9187c9a01b92e44fb31ac5fcfa7fc55565ac64043aa9a",
337-
"c03d55f9f717c1df978623e2e6b397b720999242f9ead7db9b5988fee3fb3933",
338-
"ee55688439b47a5410cdc05bac46be0094f3af54d307456fdfe6ba8caf336e0b",
339-
"61895f86c70f0bc3eef55d9a00347b509fa90f7a344606a9774be98a3ee9e02a",
340-
"ffabb401a19d04327bd4a076671d48467dbcde95459beeab23df21686fd01525",
341-
"b7e1c03b9b73e4e90fc06da893072c5604203c49e66699acbb2f61485d822981",
342-
"185614d21973990138e478ce10e0a4014352df58044276d4e4c0093aa140f482",
343-
"4a2800f13d15dc0c82308761d6fe8f6d13b65e42d7ca96a42a3a7048830e8c55",
344-
"fb98f52e91db500735b185797cebb5848afbfe1289922d87e03b98c3da5b85ef",
345-
"7901c5e36d9e8456ac61b29b82048650672a889596cbd30a9f8910a589ffc5b3",
346-
"6bcd0850fd2fa1404290ed04d78d4ae718414f16d4fbfd344951add8dcf60326",
347-
}
348-
349348
// Create a new transaction and add inputs
350349
tx := wire.NewMsgTx(wire.TxVersion)
351-
require.Equal(t, bytesEmptyTx, tx.SerializeSize())
352-
addTxInputs(t, tx, utxosTxids)
350+
addTxInputs(t, tx, exampleTxids)
353351

354352
// Add P2WPKH outputs
355353
addTxOutputs(t, tx, payerScript, payeeScript)
356354

357355
// Payer sign the redeeming transaction.
358356
signTx(t, tx, payerScript, privateKey)
359357

360-
// Estimate the tx size
358+
// Estimate the tx size in vByte
361359
// #nosec G701 always positive
362-
txSize := uint64(tx.SerializeSize())
363-
sizeEstimated := EstimateSegWitTxSize(uint64(len(utxosTxids)), 3)
364-
require.Equal(t, outTxBytesMax, sizeEstimated)
365-
require.True(t, outTxBytesMax >= txSize)
366-
require.True(t, outTxBytesMax-txSize <= 21) // 21 witness may vary
360+
vError := uint64(21 / 4) // 5 vBytes error tolerance
361+
vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor)
362+
vBytesEstimated := EstimateSegWitTxSize(uint64(len(exampleTxids)), 3)
363+
require.Equal(t, vBytesEstimated, outTxBytesMax)
364+
if vBytes > vBytesEstimated {
365+
require.True(t, vBytes-vBytesEstimated <= vError)
366+
} else {
367+
require.True(t, vBytesEstimated-vBytes <= vError)
368+
}
369+
}
370+
371+
func TestP2WPHSizeXIn3Out(t *testing.T) {
372+
// Generate payer/payee private keys and P2WPKH addresss
373+
privateKey, payerScript := generateKeyPair(t, &chaincfg.TestNet3Params)
374+
_, payeeScript := generateKeyPair(t, &chaincfg.TestNet3Params)
375+
376+
// Create new transactions with X (2 <= X <= 21) inputs and 3 outputs respectively
377+
for x := 2; x <= 21; x++ {
378+
tx := wire.NewMsgTx(wire.TxVersion)
379+
addTxInputs(t, tx, exampleTxids[:x])
380+
381+
// Add P2WPKH outputs
382+
addTxOutputs(t, tx, payerScript, payeeScript)
383+
384+
// Payer sign the redeeming transaction.
385+
signTx(t, tx, payerScript, privateKey)
386+
387+
// Estimate the tx size
388+
// #nosec G701 always positive
389+
vError := uint64(0.25 + float64(x)/4) // 1st witness incur 0.25 vByte error, other witness incur 1/4 vByte error tolerance,
390+
vBytes := uint64(blockchain.GetTransactionWeight(btcutil.NewTx(tx)) / blockchain.WitnessScaleFactor)
391+
vBytesEstimated := EstimateSegWitTxSize(uint64(len(exampleTxids[:x])), 3)
392+
if vBytes > vBytesEstimated {
393+
require.True(t, vBytes-vBytesEstimated <= vError)
394+
//fmt.Printf("%d error percentage: %.2f%%\n", float64(vBytes-vBytesEstimated)/float64(vBytes)*100)
395+
} else {
396+
require.True(t, vBytesEstimated-vBytes <= vError)
397+
//fmt.Printf("error percentage: %.2f%%\n", float64(vBytesEstimated-vBytes)/float64(vBytes)*100)
398+
}
399+
}
367400
}
368401

369402
func TestP2WPHSizeBreakdown(t *testing.T) {
@@ -374,14 +407,14 @@ func TestP2WPHSizeBreakdown(t *testing.T) {
374407
fmt.Printf("1 input, 1 output: %d\n", sz)
375408

376409
txSizeDepositor := SegWitTxSizeDepositor()
377-
require.Equal(t, uint64(149), txSizeDepositor)
410+
require.Equal(t, uint64(68), txSizeDepositor)
378411

379412
txSizeWithdrawer := SegWitTxSizeWithdrawer()
380-
require.Equal(t, uint64(254), txSizeWithdrawer)
381-
require.Equal(t, txSize2In3Out, txSizeDepositor+txSizeWithdrawer) // 403 = 149 + 254
413+
require.Equal(t, uint64(171), txSizeWithdrawer)
414+
require.Equal(t, txSize2In3Out, txSizeDepositor+txSizeWithdrawer) // 239 = 68 + 171
382415

383-
depositFee := DepositorFee(5)
384-
require.Equal(t, depositFee, 0.00000745)
416+
depositFee := DepositorFee(20)
417+
require.Equal(t, depositFee, 0.00001360)
385418
}
386419

387420
// helper function to create a new BitcoinChainClient

zetaclient/utils.go

+25-14
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
"time"
1414

1515
sdkmath "cosmossdk.io/math"
16+
"github.com/btcsuite/btcd/blockchain"
1617
"github.com/btcsuite/btcd/txscript"
18+
"github.com/btcsuite/btcd/wire"
1719
ethcommon "github.com/ethereum/go-ethereum/common"
1820
ethtypes "github.com/ethereum/go-ethereum/core/types"
1921
"github.com/pkg/errors"
@@ -36,22 +38,18 @@ const (
3638
)
3739

3840
var (
39-
BtcOutTxBytesMin uint64
40-
BtcOutTxBytesMax uint64
4141
BtcOutTxBytesDepositor uint64
4242
BtcOutTxBytesWithdrawer uint64
4343
BtcDepositorFeeMin float64
4444
)
4545

4646
func init() {
47-
BtcOutTxBytesMin = EstimateSegWitTxSize(2, 3) // 403B, estimated size for a 2-input, 3-output SegWit tx
48-
BtcOutTxBytesMax = EstimateSegWitTxSize(21, 3) // 3234B, estimated size for a 21-input, 3-output SegWit tx
49-
BtcOutTxBytesDepositor = SegWitTxSizeDepositor() // 149B, the outtx size incurred by the depositor
50-
BtcOutTxBytesWithdrawer = SegWitTxSizeWithdrawer() // 254B, the outtx size incurred by the withdrawer
47+
BtcOutTxBytesDepositor = SegWitTxSizeDepositor() // 68vB, the outtx size incurred by the depositor
48+
BtcOutTxBytesWithdrawer = SegWitTxSizeWithdrawer() // 171vB, the outtx size incurred by the withdrawer
5149

52-
// depositor fee calculation is based on a fixed fee rate of 5 sat/byte just for simplicity.
50+
// depositor fee calculation is based on a fixed fee rate of 20 sat/byte just for simplicity.
5351
// In reality, the fee rate on UTXO deposit is different from the fee rate when the UTXO is spent.
54-
BtcDepositorFeeMin = DepositorFee(5) // 0.00000745 (5 * 149B / 100000000), the minimum deposit fee in BTC for 5 sat/byte
52+
BtcDepositorFeeMin = DepositorFee(20) // 0.00001360 (20 * 68vB / 100000000), the minimum deposit fee in BTC for 20 sat/byte
5553
}
5654

5755
func PrettyPrintStruct(val interface{}) (string, error) {
@@ -73,27 +71,40 @@ func FeeRateToSatPerByte(rate float64) *big.Int {
7371
return new(big.Int).Div(satPerKB, big.NewInt(bytesPerKB))
7472
}
7573

76-
// EstimateSegWitTxSize estimates SegWit tx size
74+
// WiredTxSize calculates the wired tx size in bytes
75+
func WiredTxSize(numInputs uint64, numOutputs uint64) uint64 {
76+
// Version 4 bytes + LockTime 4 bytes + Serialized varint size for the
77+
// number of transaction inputs and outputs.
78+
// #nosec G701 always positive
79+
return uint64(8 + wire.VarIntSerializeSize(numInputs) + wire.VarIntSerializeSize(numOutputs))
80+
}
81+
82+
// EstimateSegWitTxSize estimates SegWit tx size in vByte
7783
func EstimateSegWitTxSize(numInputs uint64, numOutputs uint64) uint64 {
7884
if numInputs == 0 {
7985
return 0
8086
}
87+
bytesWiredTx := WiredTxSize(numInputs, numOutputs)
8188
bytesInput := numInputs * bytesPerInput
8289
bytesOutput := numOutputs * bytesPerOutput
8390
bytesWitness := bytes1stWitness + (numInputs-1)*bytesPerWitness
84-
return bytesEmptyTx + bytesInput + bytesOutput + bytesWitness
91+
92+
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
93+
// Calculation for signed SegWit tx: blockchain.GetTransactionWeight(tx) / 4
94+
return bytesWiredTx + bytesInput + bytesOutput + bytesWitness/blockchain.WitnessScaleFactor
8595
}
8696

87-
// SegWitTxSizeDepositor returns SegWit tx size (149B) incurred by the depositor
97+
// SegWitTxSizeDepositor returns SegWit tx size (68vB) incurred by the depositor
8898
func SegWitTxSizeDepositor() uint64 {
89-
return bytesPerInput + bytesPerWitness
99+
return bytesPerInput + bytesPerWitness/blockchain.WitnessScaleFactor
90100
}
91101

92-
// SegWitTxSizeWithdrawer returns SegWit tx size (254B) incurred by the withdrawer
102+
// SegWitTxSizeWithdrawer returns SegWit tx size (171vB) incurred by the withdrawer (1 input, 3 outputs)
93103
func SegWitTxSizeWithdrawer() uint64 {
104+
bytesWiredTx := WiredTxSize(1, 3)
94105
bytesInput := uint64(1) * bytesPerInput // nonce mark
95106
bytesOutput := uint64(3) * bytesPerOutput // 3 outputs: new nonce mark, payment, change
96-
return bytesEmptyTx + bytesInput + bytesOutput + bytes1stWitness
107+
return bytesWiredTx + bytesInput + bytesOutput + bytes1stWitness/blockchain.WitnessScaleFactor
97108
}
98109

99110
// DepositorFee calculates the depositor fee in BTC for a given sat/byte fee rate

0 commit comments

Comments
 (0)