Skip to content

Commit fb63e13

Browse files
authored
Merge pull request #6 from BoostyLabs/vs/estimation_upgrade
bitcoin/txbuilder: added variable estimation params, estimation logic updated
2 parents 021a44a + cd09614 commit fb63e13

4 files changed

Lines changed: 571 additions & 172 deletions

File tree

bitcoin/txbuilder/fees.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (C) 2025 Creditor Corp. Group.
2+
// See LICENSE for copying information.
3+
4+
package txbuilder
5+
6+
import (
7+
"errors"
8+
"math/big"
9+
10+
"github.com/btcsuite/btcd/btcutil"
11+
"github.com/btcsuite/btcd/chaincfg"
12+
)
13+
14+
var (
15+
// headerSizeVBytes defined rough tx header size in vBytes.
16+
headerSizeVBytes = big.NewInt(11)
17+
)
18+
19+
// PaymentDataFees holds additional info to build more accurate fee estimation.
20+
type PaymentDataFees struct {
21+
InputSizeVBytes *big.Int // defines input size in virtual bytes.
22+
WitnessSizeVBytes *big.Int // defines witness size in virtual bytes.
23+
OutputSizeVBytes *big.Int // defines output size in virtual bytes.
24+
}
25+
26+
// NewDefaultPaymentDataFees builds default estimation data for provided address.
27+
// NOTE: Default estimation data assumes the next type to script mapping:
28+
//
29+
// P2PKH - single signature
30+
// P2SH - 2-of-3 multisig
31+
// P2WPKH - single signature
32+
// P2WSH - 2-of-3 multisig
33+
// P2TR - key-spend path (single signature)
34+
//
35+
// INFO: Sizes were taken from this calculator: https://bitcoinops.org/en/tools/calc-size/.
36+
func NewDefaultPaymentDataFees(address btcutil.Address) (*PaymentDataFees, error) {
37+
var inputSizeVByte, witnessSizeVBytes, outputSizeVBytes int64
38+
switch address.(type) {
39+
case *btcutil.AddressPubKey:
40+
return nil, errors.New("unsupported address type: too old (P2PK)")
41+
case *btcutil.AddressPubKeyHash:
42+
inputSizeVByte, witnessSizeVBytes, outputSizeVBytes = 41, 107, 34
43+
case *btcutil.AddressScriptHash:
44+
inputSizeVByte, witnessSizeVBytes, outputSizeVBytes = 43, 254, 32
45+
case *btcutil.AddressWitnessPubKeyHash:
46+
inputSizeVByte, witnessSizeVBytes, outputSizeVBytes = 41, 27, 31
47+
case *btcutil.AddressWitnessScriptHash:
48+
inputSizeVByte, witnessSizeVBytes, outputSizeVBytes = 41, 64, 43
49+
case *btcutil.AddressTaproot:
50+
inputSizeVByte, witnessSizeVBytes, outputSizeVBytes = 41, 17, 43
51+
default:
52+
return nil, errors.New("unsupported address type")
53+
}
54+
55+
return &PaymentDataFees{
56+
InputSizeVBytes: big.NewInt(inputSizeVByte),
57+
WitnessSizeVBytes: big.NewInt(witnessSizeVBytes),
58+
OutputSizeVBytes: big.NewInt(outputSizeVBytes),
59+
}, nil
60+
}
61+
62+
// Filled returns true is struct was properly initialized and all fields are filled.
63+
func (pdf *PaymentDataFees) Filled() bool {
64+
return pdf != nil && pdf.InputSizeVBytes != nil && pdf.WitnessSizeVBytes != nil && pdf.OutputSizeVBytes != nil
65+
}
66+
67+
// RoughTxSize holds data to make rough estimation of tx in vBytes.
68+
type RoughTxSize struct {
69+
IncludeHeader bool // to include or not tx header vBytes size into a count.
70+
Elements []*EstimationElement // estimation elements.
71+
ExtraExpenses *big.Int // defines extra expenses (in vBytes) from previous estimation or for another logic needs. optional.
72+
}
73+
74+
// EstimationElement holds data needed to estimate fee part for PaymentData group.
75+
type EstimationElement struct {
76+
*PaymentDataFees
77+
InputsNumber int // number of inputs of PaymentData address.
78+
OutputsNumber int // number of outputs of PaymentData address.
79+
}
80+
81+
// Estimate returns rough estimation of tx in vBytes.
82+
func (params *RoughTxSize) Estimate() *big.Int {
83+
size := big.NewInt(0)
84+
if params.IncludeHeader {
85+
size.Add(size, headerSizeVBytes)
86+
}
87+
88+
for _, element := range params.Elements {
89+
signedInputSize := new(big.Int).Add(element.InputSizeVBytes, element.WitnessSizeVBytes)
90+
inputsFee := new(big.Int).Mul(signedInputSize, big.NewInt(int64(element.InputsNumber)))
91+
size.Add(size, inputsFee)
92+
93+
outputsFee := new(big.Int).Mul(element.OutputSizeVBytes, big.NewInt(int64(element.OutputsNumber)))
94+
size.Add(size, outputsFee)
95+
}
96+
97+
if params.ExtraExpenses != nil {
98+
size.Add(size, params.ExtraExpenses)
99+
}
100+
101+
return size
102+
}
103+
104+
// NewEstimationElementFromStringAddress is a constructor for EstimationElement that additionally parses address from string.
105+
func NewEstimationElementFromStringAddress(address string, inputs, outputs int, networkParams *chaincfg.Params) (*EstimationElement, error) {
106+
addr, err := btcutil.DecodeAddress(address, networkParams)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
addressDataFees, err := NewDefaultPaymentDataFees(addr)
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
if inputs < 0 || outputs < 0 {
117+
return nil, errors.New("inputs and outputs must be >= 0")
118+
}
119+
120+
return &EstimationElement{
121+
PaymentDataFees: addressDataFees,
122+
InputsNumber: inputs,
123+
OutputsNumber: outputs,
124+
}, nil
125+
}
126+
127+
// CalculateTxFee calculates tx network fee from tx size in vBytes and sat/kVb price.
128+
func CalculateTxFee(txSizeVBytes, satoshiPerKVByte *big.Int) (fee *big.Int) {
129+
fee = new(big.Int).Mul(txSizeVBytes, satoshiPerKVByte)
130+
fee.Quo(fee, big.NewInt(1000))
131+
132+
return fee
133+
}

bitcoin/txbuilder/fees_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (C) 2025 Creditor Corp. Group.
2+
// See LICENSE for copying information.
3+
4+
package txbuilder_test
5+
6+
import (
7+
"math/big"
8+
"testing"
9+
10+
"github.com/btcsuite/btcd/btcutil"
11+
"github.com/btcsuite/btcd/chaincfg"
12+
"github.com/stretchr/testify/require"
13+
14+
"github.com/BoostyLabs/blockchain/bitcoin/txbuilder"
15+
)
16+
17+
func TestNewDefaultPaymentDataFees(t *testing.T) {
18+
type unknownAddressType struct {
19+
btcutil.Address
20+
}
21+
chainParams := &chaincfg.MainNetParams
22+
tests := []struct {
23+
name string
24+
address btcutil.Address
25+
sum int64
26+
errStr string
27+
}{
28+
{
29+
name: "P2PK",
30+
address: btcutilAddress(t, "034cf21794aef30e95631c1e43b91412cd6e7d4734a44eec91eb45518aa81f6390", chainParams),
31+
errStr: "unsupported address type: too old (P2PK)",
32+
},
33+
{
34+
name: "P2PKH",
35+
address: btcutilAddress(t, "13yQfEYjcDZt72pyoyhAZgr3qe8vvwrwgH", chainParams),
36+
sum: 182,
37+
},
38+
{
39+
name: "P2SH",
40+
address: btcutilAddress(t, "33qK7XqdJ3v5ySvnN9PugSRZsK2ahL5U1B", chainParams),
41+
sum: 329,
42+
},
43+
{
44+
name: "P2WPKH",
45+
address: btcutilAddress(t, "bc1qhl00zlummcd9zc5ppu3klmnp4m7slkuvuq4lz0", chainParams),
46+
sum: 99,
47+
},
48+
{
49+
name: "P2WSH",
50+
address: btcutilAddress(t, "bc1q6qj9wcc5lqshlyk427437e9rkevlcsuuypsukaqhmckjf8exdqmsa0pqxh", chainParams),
51+
sum: 148,
52+
},
53+
{
54+
name: "P2TR",
55+
address: btcutilAddress(t, "bc1pvep2j3hym7tp69zyd3pwhe7a7exztrtg6wwhdd66fwx2ewfwag7qgawmug", chainParams),
56+
sum: 101,
57+
},
58+
{
59+
name: "UNKNOWN",
60+
address: unknownAddressType{btcutilAddress(t, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", chainParams)},
61+
errStr: "unsupported address type",
62+
},
63+
}
64+
for _, test := range tests {
65+
t.Run(test.name, func(t *testing.T) {
66+
got, err := txbuilder.NewDefaultPaymentDataFees(test.address)
67+
if test.errStr != "" {
68+
require.Error(t, err)
69+
require.Contains(t, err.Error(), test.errStr)
70+
return
71+
}
72+
require.NoError(t, err)
73+
require.NotNil(t, got)
74+
75+
sum := big.NewInt(0)
76+
sum.Add(sum, got.InputSizeVBytes)
77+
sum.Add(sum, got.WitnessSizeVBytes)
78+
sum.Add(sum, got.OutputSizeVBytes)
79+
require.EqualValues(t, test.sum, sum.Int64())
80+
})
81+
}
82+
}
83+
84+
func btcutilAddress(t *testing.T, address string, params *chaincfg.Params) btcutil.Address {
85+
addr, err := btcutil.DecodeAddress(address, params)
86+
require.NoError(t, err)
87+
88+
return addr
89+
}

0 commit comments

Comments
 (0)