From e75de45fedbae4b626bfdb20e2b2476a9c354b23 Mon Sep 17 00:00:00 2001 From: GG <863867759@qq.com> Date: Mon, 27 May 2024 17:57:53 +0800 Subject: [PATCH] feat(btc): estimate txn size without fake sign. --- core/btc/transaction_build.go | 7 ++- core/btc/transaction_build_test.go | 24 ++++++++ core/btc/transaction_decode.go | 90 ++++++++++++++++++++---------- go.mod | 2 + go.sum | 2 + 5 files changed, 96 insertions(+), 29 deletions(-) diff --git a/core/btc/transaction_build.go b/core/btc/transaction_build.go index f95614f..72aa6dd 100644 --- a/core/btc/transaction_build.go +++ b/core/btc/transaction_build.go @@ -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 { diff --git a/core/btc/transaction_build_test.go b/core/btc/transaction_build_test.go index 52bc4c1..790ea06 100644 --- a/core/btc/transaction_build_test.go +++ b/core/btc/transaction_build_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "testing" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" "github.com/stretchr/testify/require" ) @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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) +} diff --git a/core/btc/transaction_decode.go b/core/btc/transaction_decode.go index 8adaa0a..6688aa2 100644 --- a/core/btc/transaction_decode.go +++ b/core/btc/transaction_decode.go @@ -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" @@ -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)) @@ -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 } diff --git a/go.mod b/go.mod index 9500ff0..58f1a3f 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index f86ff88..0f05e2b 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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=