Skip to content

Commit

Permalink
feat(btc): estimate txn size without fake sign.
Browse files Browse the repository at this point in the history
  • Loading branch information
Zhangguiguang committed May 27, 2024
1 parent 5a39dbe commit e75de45
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 29 deletions.
7 changes: 6 additions & 1 deletion core/btc/transaction_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ func (t *Transaction) TotalOutputValue() int64 {
}

func (t *Transaction) EstimateTransactionSize() int64 {
return virtualSize(ensureSignOrFakeSign(t.msgTx, t.prevOutFetcher))
if len(t.msgTx.TxIn) == 0 {
return virtualSize(t.msgTx)
} else {
out := t.prevOutFetcher.FetchPrevOutput(t.msgTx.TxIn[0].PreviousOutPoint)
return EstimateTxSizePkScript(t.msgTx, out.PkScript)
}
}

func (t *Transaction) AddInput(txId string, index int64, address string, value int64) error {
Expand Down
24 changes: 24 additions & 0 deletions core/btc/transaction_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"testing"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -72,6 +73,7 @@ func TestTransaction_Sign(t *testing.T) {
txHex, err := txn.SignWithAccount(from)
require.NoError(t, err)
require.Equal(t, txHex.Value, "0x010000000001023fb7a23c3b9070a01e586b9aa97a0179b2e49a44575bc653e1f3ae12303bd37a0100000000ffffffff13677517ca008dd58611522b9c9d672c6e14778a0c5c4e41cdcf740cf5409be60900000000ffffffff01dc5a0f0000000000225120f78430ddf1178c9c04bd32e3e51d0aa72760756934f95173cb7926083e115062014095001ab2628b184b396fa6daf0d17cee878dddca7822c7b2d69c540ed8572ef82c11fc4daa6d51c8afdb436a1413f081015107d798ec2f4fe62ea6fcc5f3bd9401400b602a8a4d9c979e18b15f202fc7d4eb4c1ff410005a38cbc48dd6a7ef363eeb0d1833fdbbf949d01aa83f6b6873b81f021a9b02126d0b375b512df604bb105700000000")
require.Equal(t, txn.EstimateTransactionSize(), int64(169))
// txn detail: https://mempool.space/signet/tx/f926ce5fd50735c30c1f4de0c0d61d0ee14d24a4e3fc2210f410abbf1335cacf
}

Expand All @@ -90,6 +92,7 @@ func TestTransaction_Sign(t *testing.T) {
txHex, err := txn.SignWithAccount(from)
require.NoError(t, err)
require.Equal(t, txHex.Value, "0x010000000001017eb6d9f5b89d085d974ebe07e199caf73a6ceb07cbe432a2f87b1ab21e6e846f0000000000ffffffff0258570f0000000000160014b2f9ed398433cd438d5e88d62b2e28b440901cc500000000000000001c6a1a436f6d696e67436861742057616c6c65742053444b20546573740140767a1c1b1ea639b92c59f9b49a38e794526c2f301cf9000ad0e58ebe2ea832d448d3711a494f933a95ed72bfa1642418eaa1dcc092c0530b6351a80c5ea995b600000000")
require.Equal(t, txn.EstimateTransactionSize(), int64(136))
// txn detail: https://mempool.space/signet/tx/6ba14977701bf831615d2ecca5d1e5b020eb6fe8b999e2f5f1a9b84f636c1705
}

Expand All @@ -109,6 +112,7 @@ func TestTransaction_Sign(t *testing.T) {
txHex, err := txn.SignWithAccount(from)
require.NoError(t, err)
require.Equal(t, txHex.Value, nativeSg_TxnHex)
require.Equal(t, txn.EstimateTransactionSize(), int64(149))
// txn detail: https://mempool.space/signet/tx/c0a4c2b41deadd4122a15c809079cf7d19c5b49dccb0247599427ff607295381
}

Expand All @@ -128,6 +132,7 @@ func TestTransaction_Sign(t *testing.T) {
txHex, err := txn.SignWithAccount(from)
require.NoError(t, err)
require.Equal(t, txHex.Value, "0x0100000000010181532907f67f42997524b0cc9db4c5197dcf7990805ca12241ddea1db4c2a4c0000000001716001441d432f2b3d3daa1df08e21fbdbbcdfb0908001bffffffff0240420f00000000001976a9147a494419776de8f4cb846882a15df3cc836299cf88ac24130000000000001976a9147a494419776de8f4cb846882a15df3cc836299cf88ac024730440220731dfff44876d17a9b9cbf08a33a238e61f79f09fb73bd3a7541a93a9df4b450022040ec119a288c042077e2dd219e94e23def37bb93fc4f932fd1a6805e7b6681440121026612352433fe00fed2c41213b79be357daeac671f51812918d057e35a7a36cc000000000")
require.Equal(t, txn.EstimateTransactionSize(), int64(170))
// txn detail: https://mempool.space/signet/tx/87db2ca3d888c74d96fef6a516b94b33be24e09e4a5e6418a96ecb5ab6cf959c
}

Expand All @@ -146,7 +151,26 @@ func TestTransaction_Sign(t *testing.T) {
txHex, err := txn.SignWithAccount(from)
require.NoError(t, err)
require.Equal(t, txHex.Value, "0x01000000029c95cfb65acb6ea918645e4a9ee024be334bb916a5f6fe964dc788d8a32cdb87000000006a47304402201d252ee0e20f82e0c13ee3294ad6cfd0ab6021c276724c41fec1a80f56ee1a1802202f65a9c4108c10fe5f481b63f1e464dfd14f5041fe617c37777c6ecf975e147d0121029b740120a2a4af5d751d5e3d67f6d2aa9f92792af6bc0df37d30584b6d65bb54ffffffff9c95cfb65acb6ea918645e4a9ee024be334bb916a5f6fe964dc788d8a32cdb87010000006a47304402200d6aabb1ff6eef5aa2b01fcd0e00bdbd0c1dda63a6842c0ad4b06b1e91ba46450220428bee9060569844b9d19206576ee648924817c1f2d42a379f22f7f2115122de0121029b740120a2a4af5d751d5e3d67f6d2aa9f92792af6bc0df37d30584b6d65bb54ffffffff01d4530f0000000000225120682aa8a7746b1ef87d5cc5b78664e48bbc8e6587b77404bb3f97f8a8ef7076a200000000")
require.Equal(t, txn.EstimateTransactionSize(), int64(349))
// txn detail: https://mempool.space/signet/tx/932bce660d7336aea21213a4d7a25c7fa82ae66a0e5cc9e1e8a82a88b7c40622
}

}

func TestEstimateTxSize(t *testing.T) {
signedTxHex := "0100000000010105176c634fb8a9f1f5e299b9e86feb20b0e5d1a5cc2e5d6131f81b707749a16b0000000000ffffffff0290560f000000000017a914fc910555e59c5449120f5ba0b5fdf75c5f7e3e718700000000000000001d6a1b436f6d696e67436861742057616c6c65742053444b20546573743202473044022037840ab4706872c141a60a065c1a0f65fd725bad1529d5149d1f241d5b44981d022047445550484ffe0b5db4316a3fd264437e2a9a7f8a6e26fdb2bc703044632d46012103c17dd7d56e18c38293a882dcd559f3e4e8ee8b5643f8d8e5841bb49447fb3a0f00000000"
data, err := hex.DecodeString(signedTxHex)
require.NoError(t, err)
tx, err := btcutil.NewTxFromBytes(data)
require.NoError(t, err)
msgTx := tx.MsgTx()
txSize := EstimateTxSize(msgTx, &btcutil.AddressWitnessPubKeyHash{})
require.Equal(t, txSize, int64(149))
// txn detail: https://mempool.space/signet/tx/c0a4c2b41deadd4122a15c809079cf7d19c5b49dccb0247599427ff607295381

// EstimateTxSize is a pure function, the msgTx should not be modified.
signedTxn := SignedTransaction{msgTx: msgTx}
txHex2, err := signedTxn.HexString()
require.NoError(t, err)
require.Equal(t, "0x"+signedTxHex, txHex2.Value)
}
90 changes: 62 additions & 28 deletions core/btc/transaction_decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import (
"fmt"
"strconv"

"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/coming-chat/wallet-SDK/core/base"
Expand Down Expand Up @@ -94,8 +93,7 @@ func DecodePsbtTransactionDetail(psbtHex string, chainnet string) (d *Transactio
return
}
feeFloat := txFee.ToUnit(btcutil.AmountSatoshi)
copyedTx := packet.UnsignedTx.Copy()
vSize := virtualSize(ensureSignOrFakeSign(copyedTx, nil))
vSize := EstimateTxSize(packet.UnsignedTx, nil)
feeRate := feeFloat / float64(vSize)

inputs := make([]*TxOut, len(packet.Inputs))
Expand Down Expand Up @@ -198,36 +196,72 @@ func txOutFromWireTxOut(txout *wire.TxOut, params *chaincfg.Params) (*TxOut, err
}, nil
}

// calculation reference:
// https://github.com/btcsuite/btcd/blob/569155bc6a502f45b4a514bc6b9d5f814a980b6c/mempool/policy.go#L382
func virtualSize(tx *wire.MsgTx) int64 {
// vSize := (((baseSize * 3) + totalSize) + 3) / 4
baseSize := int64(tx.SerializeSizeStripped())
totalSize := int64(tx.SerializeSize())
weight := (baseSize * (blockchain.WitnessScaleFactor - 1)) + totalSize
return (weight + blockchain.WitnessScaleFactor - 1) / blockchain.WitnessScaleFactor
return mempool.GetTxVirtualSize(btcutil.NewTx(tx))
}

var _fakePrivatekey *btcec.PrivateKey

func ensureSignOrFakeSign(tx *wire.MsgTx, fetcher txscript.PrevOutputFetcher) *wire.MsgTx {
if len(tx.TxIn) == 0 || len(tx.TxOut) == 0 {
return tx // cannot sign
// This is a pure function; it does not change the tx parameter.
func EstimateTxSizePkScript(tx *wire.MsgTx, pkScript []byte) int64 {
witnessSize := 0
signatureSize := 0
if pkScript == nil { // average: NestedSegwit
witnessSize = 108
signatureSize = 23
} else if txscript.IsPayToTaproot(pkScript) { // Taproot
witnessSize = 64
} else if txscript.IsPayToPubKeyHash(pkScript) {
signatureSize = 106 // Legacy
} else {
witnessSize = 108 // NativeSegwit
if txscript.IsPayToScriptHash(pkScript) { // NestedSegwit
signatureSize = 23
}
}
if len(tx.TxIn[0].SignatureScript) != 0 || len(tx.TxIn[0].Witness) != 0 {
return tx // no need sign
return estimateTxSize(tx, witnessSize, signatureSize)
}

// This is a pure function; it does not change the tx parameter.
func EstimateTxSize(tx *wire.MsgTx, sendAddr btcutil.Address) int64 {
witnessSize := 0
signatureSize := 0
switch sendAddr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
witnessSize = 108 // P2WPKH_WINETSS_SIZE
case *btcutil.AddressTaproot:
witnessSize = 64 // SCHNORR_SIGNATURE_SIZE
case *btcutil.AddressScriptHash:
witnessSize = 108
signatureSize = 23
case *btcutil.AddressPubKeyHash:
signatureSize = 106
default: // average: AddressScriptHash
witnessSize = 108
signatureSize = 23
}
return estimateTxSize(tx, witnessSize, signatureSize)
}

var err error
if _fakePrivatekey == nil {
if _fakePrivatekey, err = btcec.NewPrivateKey(); err != nil {
return tx // sign failed
}
// This is a pure function; it does not change the tx parameter.
func estimateTxSize(tx *wire.MsgTx, witnessSize int, signatureSize int) int64 {
signatureBackup := make([]*wire.TxIn, len(tx.TxIn))
copySignature := func(from, to *wire.TxIn) {
to.Witness = from.Witness
to.Sequence = from.Sequence
to.SignatureScript = from.SignatureScript
}
if fetcher == nil {
fetcher = txscript.NewCannedPrevOutputFetcher(tx.TxOut[0].PkScript, 100000000)
defer func() {
for idx, in := range tx.TxIn { // restore signature
copySignature(signatureBackup[idx], in)
}
}()

for idx, in := range tx.TxIn {
signatureBackup[idx] = &wire.TxIn{}
copySignature(in, signatureBackup[idx]) // backup signature
in.Witness = [][]byte{make([]byte, witnessSize)}
in.Sequence = wire.MaxTxInSequenceNum - 1
in.SignatureScript = make([]byte, signatureSize)
}
// fake sign
_ = Sign(tx, _fakePrivatekey, fetcher, false)
return tx
txSize := mempool.GetTxVirtualSize(btcutil.NewTx(tx))
return txSize
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/99designs/keyring v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/aead/siphash v1.0.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
Expand Down Expand Up @@ -110,6 +111,7 @@ require (
github.com/huandu/xstrings v1.3.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bw
github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o=
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c=
github.com/alecthomas/participle/v2 v2.0.0-alpha7/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA=
Expand Down Expand Up @@ -529,6 +530,7 @@ github.com/kilic/bls12-381 v0.0.0-20200607163746-32e1441c8a9f/go.mod h1:XXfR6YFC
github.com/kilic/bls12-381 v0.0.0-20200731194930-64c428e1bff5/go.mod h1:XXfR6YFCRSrkEXbNlIyDsgXVNJWVUV30m/ebkVy9n6s=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
Expand Down

0 comments on commit e75de45

Please sign in to comment.