From 66bd7b7050da1d5171eccb81b1aae95818d10f35 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 6 Apr 2022 14:33:13 +0200 Subject: [PATCH 1/8] mempool: export check Standardness func --- mempool/mempool.go | 2 +- mempool/policy.go | 4 ++-- mempool/policy_test.go | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mempool/mempool.go b/mempool/mempool.go index f6f73e9a..280937e1 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -1097,7 +1097,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, rejec // Don't allow non-standard transactions if the network parameters // forbid their acceptance. if !mp.cfg.Policy.AcceptNonStd { - err = checkTransactionStandard(tx, nextBlockHeight, + err = CheckTransactionStandard(tx, nextBlockHeight, medianTimePast, mp.cfg.Policy.MinRelayTxFee, mp.cfg.Policy.MaxTxVersion) if err != nil { diff --git a/mempool/policy.go b/mempool/policy.go index b73afcfc..60ccfc1f 100644 --- a/mempool/policy.go +++ b/mempool/policy.go @@ -268,14 +268,14 @@ func IsDust(txOut *wire.TxOut, minRelayTxFee btcutil.Amount) bool { return txOut.Value*1000/(3*int64(totalSize)) < int64(minRelayTxFee) } -// checkTransactionStandard performs a series of checks on a transaction to +// CheckTransactionStandard performs a series of checks on a transaction to // ensure it is a "standard" transaction. A standard transaction is one that // conforms to several additional limiting cases over what is considered a // "sane" transaction such as having a version in the supported range, being // finalized, conforming to more stringent size constraints, having scripts // of recognized forms, and not containing "dust" outputs (those that are // so small it costs more to process them than they are worth). -func checkTransactionStandard(tx *btcutil.Tx, height int32, +func CheckTransactionStandard(tx *btcutil.Tx, height int32, medianTimePast time.Time, minRelayTxFee btcutil.Amount, maxTxVersion int32) error { diff --git a/mempool/policy_test.go b/mempool/policy_test.go index 4b659b60..c87b633e 100644 --- a/mempool/policy_test.go +++ b/mempool/policy_test.go @@ -277,7 +277,7 @@ func TestDust(t *testing.T) { } } -// TestCheckTransactionStandard tests the checkTransactionStandard API. +// TestCheckTransactionStandard tests the CheckTransactionStandard API. func TestCheckTransactionStandard(t *testing.T) { // Create some dummy, but otherwise standard, data for transactions. prevOutHash, err := chainhash.NewHashFromStr("01") @@ -469,7 +469,7 @@ func TestCheckTransactionStandard(t *testing.T) { pastMedianTime := time.Now() for _, test := range tests { // Ensure standardness is as expected. - err := checkTransactionStandard(btcutil.NewTx(&test.tx), + err := CheckTransactionStandard(btcutil.NewTx(&test.tx), test.height, pastMedianTime, DefaultMinRelayTxFee, 1) if err == nil && test.isStandard { // Test passes since function returned standard for a @@ -477,12 +477,12 @@ func TestCheckTransactionStandard(t *testing.T) { continue } if err == nil && !test.isStandard { - t.Errorf("checkTransactionStandard (%s): standard when "+ + t.Errorf("CheckTransactionStandard (%s): standard when "+ "it should not be", test.name) continue } if err != nil && test.isStandard { - t.Errorf("checkTransactionStandard (%s): nonstandard "+ + t.Errorf("CheckTransactionStandard (%s): nonstandard "+ "when it should not be: %v", test.name, err) continue } @@ -490,20 +490,20 @@ func TestCheckTransactionStandard(t *testing.T) { // Ensure error type is a TxRuleError inside of a RuleError. rerr, ok := err.(RuleError) if !ok { - t.Errorf("checkTransactionStandard (%s): unexpected "+ + t.Errorf("CheckTransactionStandard (%s): unexpected "+ "error type - got %T", test.name, err) continue } txrerr, ok := rerr.Err.(TxRuleError) if !ok { - t.Errorf("checkTransactionStandard (%s): unexpected "+ + t.Errorf("CheckTransactionStandard (%s): unexpected "+ "error type - got %T", test.name, rerr.Err) continue } // Ensure the reject code is the expected one. if txrerr.RejectCode != test.code { - t.Errorf("checkTransactionStandard (%s): unexpected "+ + t.Errorf("CheckTransactionStandard (%s): unexpected "+ "error code - got %v, want %v", test.name, txrerr.RejectCode, test.code) continue From 9afb3df42818c9930716d1728b3cfb01bf7f1577 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 4 Nov 2023 07:40:47 +0800 Subject: [PATCH 2/8] mempool: add new method `checkMempoolAcceptance` This commit breaks the `maybeAcceptTransaction` into two parts - the first is reading the mempool to validate the transaction, and the relevant logic has been moved into the new method `checkMempoolAcceptance`. The second part is writing to the mempool, and is kept in the method `maybeAcceptTransaction`. --- mempool/mempool.go | 813 +++++++++++++++++++++++++++------------------ 1 file changed, 487 insertions(+), 326 deletions(-) diff --git a/mempool/mempool.go b/mempool/mempool.go index 280937e1..31d35e2a 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "github.com/davecgh/go-spew/spew" "github.com/utreexo/utreexod/blockchain" "github.com/utreexo/utreexod/blockchain/indexers" "github.com/utreexo/utreexod/btcjson" @@ -567,21 +568,7 @@ func (mp *TxPool) RemoveDoubleSpends(tx *btcutil.Tx) { // helper for maybeAcceptTransaction. // // This function MUST be called with the mempool lock held (for writes). -func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *btcutil.Tx, height int32, fee int64) (*TxDesc, error) { - if mp.cfg.IsUtreexoViewActive != nil && mp.cfg.IsUtreexoViewActive() { - // Ingest the proof. Shouldn't error out with the proof being invalid - // here since we've already verified it above. - err := mp.cfg.VerifyUData(tx.MsgTx().UData, tx.MsgTx().TxIn, true) - if err != nil { - return nil, fmt.Errorf("error while ingesting proof. %v", err) - } - - mp.poolLeaves[*tx.Hash()] = tx.MsgTx().UData.LeafDatas - } - - // Nil out uneeded udata for the mempool. - tx.MsgTx().UData = nil - +func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *btcutil.Tx, height int32, fee int64) *TxDesc { // Add the transaction to the pool and mark the referenced outpoints // as spent by the pool. txD := &TxDesc{ @@ -612,7 +599,7 @@ func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *btcutil mp.cfg.FeeEstimator.ObserveTransaction(txD) } - return txD, nil + return txD } // checkPoolDoubleSpend checks whether or not the passed transaction is @@ -1032,326 +1019,32 @@ func (mp *TxPool) validateReplacement(tx *btcutil.Tx, // more details. // // This function MUST be called with the mempool lock held (for writes). -func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, rejectDupOrphans bool) ([]*chainhash.Hash, *TxDesc, error) { - txHash := tx.Hash() - - // If a transaction has witness data, and segwit isn't active yet, If - // segwit isn't active yet, then we won't accept it into the mempool as - // it can't be mined yet. - if tx.MsgTx().HasWitness() { - segwitActive, err := mp.cfg.IsDeploymentActive(chaincfg.DeploymentSegwit) - if err != nil { - return nil, nil, err - } - - if !segwitActive { - simnetHint := "" - if mp.cfg.ChainParams.Net == wire.SimNet { - bestHeight := mp.cfg.BestHeight() - simnetHint = fmt.Sprintf(" (The threshold for segwit activation is 300 blocks on simnet, "+ - "current best height is %d)", bestHeight) - } - str := fmt.Sprintf("transaction %v has witness data, "+ - "but segwit isn't active yet%s", txHash, simnetHint) - return nil, nil, txRuleError(wire.RejectNonstandard, str) - } - } - - // Don't accept the transaction if it already exists in the pool. This - // applies to orphan transactions as well when the reject duplicate - // orphans flag is set. This check is intended to be a quick check to - // weed out duplicates. - if mp.isTransactionInPool(txHash) || (rejectDupOrphans && - mp.isOrphanInPool(txHash)) { - - str := fmt.Sprintf("already have transaction %v", txHash) - return nil, nil, txRuleError(wire.RejectDuplicate, str) - } - - // Perform preliminary sanity checks on the transaction. This makes - // use of blockchain which contains the invariant rules for what - // transactions are allowed into blocks. - err := blockchain.CheckTransactionSanity(tx) - if err != nil { - if cerr, ok := err.(blockchain.RuleError); ok { - return nil, nil, chainRuleError(cerr) - } - return nil, nil, err - } - - // A standalone transaction must not be a coinbase transaction. - if blockchain.IsCoinBase(tx) { - str := fmt.Sprintf("transaction %v is an individual coinbase", - txHash) - return nil, nil, txRuleError(wire.RejectInvalid, str) - } - - // Get the current height of the main chain. A standalone transaction - // will be mined into the next block at best, so its height is at least - // one more than the current height. - bestHeight := mp.cfg.BestHeight() - nextBlockHeight := bestHeight + 1 - - medianTimePast := mp.cfg.MedianTimePast() - - // Don't allow non-standard transactions if the network parameters - // forbid their acceptance. - if !mp.cfg.Policy.AcceptNonStd { - err = CheckTransactionStandard(tx, nextBlockHeight, - medianTimePast, mp.cfg.Policy.MinRelayTxFee, - mp.cfg.Policy.MaxTxVersion) - if err != nil { - // Attempt to extract a reject code from the error so - // it can be retained. When not possible, fall back to - // a non standard error. - rejectCode, found := extractRejectCode(err) - if !found { - rejectCode = wire.RejectNonstandard - } - str := fmt.Sprintf("transaction %v is not standard: %v", - txHash, err) - return nil, nil, txRuleError(rejectCode, str) - } - } - - // The transaction may not use any of the same outputs as other - // transactions already in the pool as that would ultimately result in a - // double spend, unless those transactions signal for RBF. This check is - // intended to be quick and therefore only detects double spends within - // the transaction pool itself. The transaction could still be double - // spending coins from the main chain at this point. There is a more - // in-depth check that happens later after fetching the referenced - // transaction inputs from the main chain which examines the actual - // spend data and prevents double spends. - isReplacement, err := mp.checkPoolDoubleSpend(tx) - if err != nil { - return nil, nil, err - } - - var utxoView *blockchain.UtxoViewpoint - // If utreexo accumulators are enabled, then we simply verify the - // included proof that was sent over with the transaction. - if mp.cfg.IsUtreexoViewActive != nil && mp.cfg.IsUtreexoViewActive() { - ud := tx.MsgTx().UData - - // First verify the proof to ensure that the proof the peer has - // sent was over valid. - err = mp.cfg.VerifyUData(ud, tx.MsgTx().TxIn, false) - if err != nil { - str := fmt.Sprintf("transaction %v failed the utreexo data verification. %v", - txHash, err) - return nil, nil, txRuleError(wire.RejectInvalid, str) - } - log.Debugf("VerifyUData passed for tx %s", txHash.String()) - - // After the validation passes, turn that proof into a utxoView. - utxoView = mp.fetchInputUtxosFromUData(tx, ud) - } else { - // Fetch all of the unspent transaction outputs referenced by the inputs - // to this transaction. This function also attempts to fetch the - // transaction itself to be used for detecting a duplicate transaction - // without needing to do a separate lookup. - utxoView, err = mp.fetchInputUtxos(tx) - if err != nil { - if cerr, ok := err.(blockchain.RuleError); ok { - return nil, nil, chainRuleError(cerr) - } - return nil, nil, err - } - } - - // Don't allow the transaction if it exists in the main chain and is not - // not already fully spent. - prevOut := wire.OutPoint{Hash: *txHash} - for txOutIdx := range tx.MsgTx().TxOut { - prevOut.Index = uint32(txOutIdx) - entry := utxoView.LookupEntry(prevOut) - if entry != nil && !entry.IsSpent() { - return nil, nil, txRuleError(wire.RejectDuplicate, - "transaction already exists") - } - utxoView.RemoveEntry(prevOut) - } - - // Transaction is an orphan if any of the referenced transaction outputs - // don't exist or are already spent. Adding orphans to the orphan pool - // is not handled by this function, and the caller should use - // maybeAddOrphan if this behavior is desired. - var missingParents []*chainhash.Hash - for outpoint, entry := range utxoView.Entries() { - if entry == nil || entry.IsSpent() { - // Must make a copy of the hash here since the iterator - // is replaced and taking its address directly would - // result in all of the entries pointing to the same - // memory location and thus all be the final hash. - hashCopy := outpoint.Hash - missingParents = append(missingParents, &hashCopy) - } - } - if len(missingParents) > 0 { - return missingParents, nil, nil - } - - // Don't allow the transaction into the mempool unless its sequence - // lock is active, meaning that it'll be allowed into the next block - // with respect to its defined relative lock times. - sequenceLock, err := mp.cfg.CalcSequenceLock(tx, utxoView) - if err != nil { - if cerr, ok := err.(blockchain.RuleError); ok { - return nil, nil, chainRuleError(cerr) - } - return nil, nil, err - } - if !blockchain.SequenceLockActive(sequenceLock, nextBlockHeight, - medianTimePast) { - return nil, nil, txRuleError(wire.RejectNonstandard, - "transaction's sequence locks on inputs not met") - } - - // Perform several checks on the transaction inputs using the invariant - // rules in blockchain for what transactions are allowed into blocks. - // Also returns the fees associated with the transaction which will be - // used later. - txFee, err := blockchain.CheckTransactionInputs(tx, nextBlockHeight, - utxoView, mp.cfg.ChainParams) - if err != nil { - if cerr, ok := err.(blockchain.RuleError); ok { - return nil, nil, chainRuleError(cerr) - } - return nil, nil, err - } - - // Don't allow transactions with non-standard inputs if the network - // parameters forbid their acceptance. - if !mp.cfg.Policy.AcceptNonStd { - err := checkInputsStandard(tx, utxoView) - if err != nil { - // Attempt to extract a reject code from the error so - // it can be retained. When not possible, fall back to - // a non standard error. - rejectCode, found := extractRejectCode(err) - if !found { - rejectCode = wire.RejectNonstandard - } - str := fmt.Sprintf("transaction %v has a non-standard "+ - "input: %v", txHash, err) - return nil, nil, txRuleError(rejectCode, str) - } - } +func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, + rejectDupOrphans bool) ([]*chainhash.Hash, *TxDesc, error) { - // NOTE: if you modify this code to accept non-standard transactions, - // you should add code here to check that the transaction does a - // reasonable number of ECDSA signature verifications. + txHash := tx.Hash() - // Don't allow transactions with an excessive number of signature - // operations which would result in making it impossible to mine. Since - // the coinbase address itself can contain signature operations, the - // maximum allowed signature operations per transaction is less than - // the maximum allowed signature operations per block. - // TODO(roasbeef): last bool should be conditional on segwit activation - sigOpCost, err := blockchain.GetSigOpCost(tx, false, utxoView, true, true) + // Check for mempool acceptance. + r, err := mp.checkMempoolAcceptance( + tx, isNew, rateLimit, rejectDupOrphans, + ) if err != nil { - if cerr, ok := err.(blockchain.RuleError); ok { - return nil, nil, chainRuleError(cerr) - } return nil, nil, err } - if sigOpCost > mp.cfg.Policy.MaxSigOpCostPerTx { - str := fmt.Sprintf("transaction %v sigop cost is too high: %d > %d", - txHash, sigOpCost, mp.cfg.Policy.MaxSigOpCostPerTx) - return nil, nil, txRuleError(wire.RejectNonstandard, str) - } - // Don't allow transactions with fees too low to get into a mined block. - // - // Most miners allow a free transaction area in blocks they mine to go - // alongside the area used for high-priority transactions as well as - // transactions with fees. A transaction size of up to 1000 bytes is - // considered safe to go into this section. Further, the minimum fee - // calculated below on its own would encourage several small - // transactions to avoid fees rather than one single larger transaction - // which is more desirable. Therefore, as long as the size of the - // transaction does not exceeed 1000 less than the reserved space for - // high-priority transactions, don't require a fee for it. - serializedSize := GetTxVirtualSize(tx) - minFee := calcMinRequiredTxRelayFee(serializedSize, - mp.cfg.Policy.MinRelayTxFee) - if serializedSize >= (DefaultBlockPrioritySize-1000) && txFee < minFee { - str := fmt.Sprintf("transaction %v has %d fees which is under "+ - "the required amount of %d", txHash, txFee, - minFee) - return nil, nil, txRuleError(wire.RejectInsufficientFee, str) - } - - // Require that free transactions have sufficient priority to be mined - // in the next block. Transactions which are being added back to the - // memory pool from blocks that have been disconnected during a reorg - // are exempted. - if isNew && !mp.cfg.Policy.DisableRelayPriority && txFee < minFee { - currentPriority := mining.CalcPriority(tx.MsgTx(), utxoView, - nextBlockHeight) - if currentPriority <= mining.MinHighPriority { - str := fmt.Sprintf("transaction %v has insufficient "+ - "priority (%g <= %g)", txHash, - currentPriority, mining.MinHighPriority) - return nil, nil, txRuleError(wire.RejectInsufficientFee, str) - } - } - - // Free-to-relay transactions are rate limited here to prevent - // penny-flooding with tiny transactions as a form of attack. - if rateLimit && txFee < minFee { - nowUnix := time.Now().Unix() - // Decay passed data with an exponentially decaying ~10 minute - // window - matches bitcoind handling. - mp.pennyTotal *= math.Pow(1.0-1.0/600.0, - float64(nowUnix-mp.lastPennyUnix)) - mp.lastPennyUnix = nowUnix - - // Are we still over the limit? - if mp.pennyTotal >= mp.cfg.Policy.FreeTxRelayLimit*10*1000 { - str := fmt.Sprintf("transaction %v has been rejected "+ - "by the rate limiter due to low fees", txHash) - return nil, nil, txRuleError(wire.RejectInsufficientFee, str) - } - oldTotal := mp.pennyTotal - - mp.pennyTotal += float64(serializedSize) - log.Tracef("rate limit: curTotal %v, nextTotal: %v, "+ - "limit %v", oldTotal, mp.pennyTotal, - mp.cfg.Policy.FreeTxRelayLimit*10*1000) - } - - // If the transaction has any conflicts and we've made it this far, then - // we're processing a potential replacement. - var conflicts map[chainhash.Hash]*btcutil.Tx - if isReplacement { - conflicts, err = mp.validateReplacement(tx, txFee) - if err != nil { - return nil, nil, err - } - } - - // Verify crypto signatures for each input and reject the transaction if - // any don't verify. - err = blockchain.ValidateTransactionScripts(tx, utxoView, - txscript.StandardVerifyFlags, mp.cfg.SigCache, - mp.cfg.HashCache) - if err != nil { - if cerr, ok := err.(blockchain.RuleError); ok { - return nil, nil, chainRuleError(cerr) - } - return nil, nil, err + // Exit early if this transaction is missing parents. + if len(r.MissingParents) > 0 { + return r.MissingParents, nil, nil } // Now that we've deemed the transaction as valid, we can add it to the // mempool. If it ended up replacing any transactions, we'll remove them // first. - for _, conflict := range conflicts { + for _, conflict := range r.Conflicts { log.Debugf("Replacing transaction %v (fee_rate=%v sat/kb) "+ "with %v (fee_rate=%v sat/kb)\n", conflict.Hash(), mp.pool[*conflict.Hash()].FeePerKB, tx.Hash(), - txFee*1000/serializedSize) + int64(r.TxFee)*1000/r.TxSize) // The conflict set should already include the descendants for // each one, so we don't need to remove the redeemers within @@ -1361,10 +1054,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, rejec // it for the ingestion. mp.removeTransaction(conflict, false, false) } - txD, err := mp.addTransaction(utxoView, tx, bestHeight, txFee) - if err != nil { - return nil, txD, err - } + txD := mp.addTransaction(r.utxoView, tx, r.bestHeight, int64(r.TxFee)) log.Debugf("Accepted transaction %v (pool size: %v)", txHash, len(mp.pool)) @@ -1679,6 +1369,477 @@ func (mp *TxPool) LastUpdated() time.Time { return time.Unix(atomic.LoadInt64(&mp.lastUpdated), 0) } +// MempoolAcceptResult holds the result from mempool acceptance check. +type MempoolAcceptResult struct { + // TxFee is the fees paid in satoshi. + TxFee btcutil.Amount + + // TxSize is the virtual size(vb) of the tx. + TxSize int64 + + // conflicts is a set of transactions whose inputs are spent by this + // transaction(RBF). + Conflicts map[chainhash.Hash]*btcutil.Tx + + // MissingParents is a set of outpoints that are used by this + // transaction which cannot be found. Transaction is an orphan if any + // of the referenced transaction outputs don't exist or are already + // spent. + // + // NOTE: this field is mutually exclusive with other fields. If this + // field is not nil, then other fields must be empty. + MissingParents []*chainhash.Hash + + // utxoView is a set of the unspent transaction outputs referenced by + // the inputs to this transaction. + utxoView *blockchain.UtxoViewpoint + + // bestHeight is the best known height by the mempool. + bestHeight int32 +} + +// CheckMempoolAcceptance behaves similarly to bitcoind's `testmempoolaccept` +// RPC method. It will perform a series of checks to decide whether this +// transaction can be accepted to the mempool. If not, the specific error is +// returned and the caller needs to take actions based on it. +func (mp *TxPool) CheckMempoolAcceptance(tx *btcutil.Tx) ( + *MempoolAcceptResult, error) { + + mp.mtx.RLock() + defer mp.mtx.RUnlock() + + // Call checkMempoolAcceptance with isNew=true and rateLimit=true, + // which has the effect that we always check the fee paid from this tx + // is greater than min relay fee. We also reject this tx if it's + // already an orphan. + result, err := mp.checkMempoolAcceptance(tx, true, true, true) + if err != nil { + log.Errorf("CheckMempoolAcceptance: %v", err) + return nil, err + } + + log.Tracef("Tx %v passed mempool acceptance check: %v", tx.Hash(), + spew.Sdump(result)) + + return result, nil +} + +// checkMempoolAcceptance performs a series of validations on the given +// transaction. It returns an error when the transaction fails to meet the +// mempool policy, otherwise a `mempoolAcceptResult` is returned. +func (mp *TxPool) checkMempoolAcceptance(tx *btcutil.Tx, + isNew, rateLimit, rejectDupOrphans bool) (*MempoolAcceptResult, error) { + + txHash := tx.Hash() + + // Check for segwit activeness. + if err := mp.validateSegWitDeployment(tx); err != nil { + return nil, err + } + + // Don't accept the transaction if it already exists in the pool. This + // applies to orphan transactions as well when the reject duplicate + // orphans flag is set. This check is intended to be a quick check to + // weed out duplicates. + if mp.isTransactionInPool(txHash) || (rejectDupOrphans && + mp.isOrphanInPool(txHash)) { + + str := fmt.Sprintf("already have transaction %v", txHash) + return nil, txRuleError(wire.RejectDuplicate, str) + } + + // Perform preliminary sanity checks on the transaction. This makes use + // of blockchain which contains the invariant rules for what + // transactions are allowed into blocks. + err := blockchain.CheckTransactionSanity(tx) + if err != nil { + if cerr, ok := err.(blockchain.RuleError); ok { + return nil, chainRuleError(cerr) + } + + return nil, err + } + + // A standalone transaction must not be a coinbase transaction. + if blockchain.IsCoinBase(tx) { + str := fmt.Sprintf("transaction %v is an individual coinbase", + txHash) + + return nil, txRuleError(wire.RejectInvalid, str) + } + + // Get the current height of the main chain. A standalone transaction + // will be mined into the next block at best, so its height is at least + // one more than the current height. + bestHeight := mp.cfg.BestHeight() + nextBlockHeight := bestHeight + 1 + + medianTimePast := mp.cfg.MedianTimePast() + + // The transaction may not use any of the same outputs as other + // transactions already in the pool as that would ultimately result in + // a double spend, unless those transactions signal for RBF. This check + // is intended to be quick and therefore only detects double spends + // within the transaction pool itself. The transaction could still be + // double spending coins from the main chain at this point. There is a + // more in-depth check that happens later after fetching the referenced + // transaction inputs from the main chain which examines the actual + // spend data and prevents double spends. + isReplacement, err := mp.checkPoolDoubleSpend(tx) + if err != nil { + return nil, err + } + + // Fetch all of the unspent transaction outputs referenced by the + // inputs to this transaction. This function also attempts to fetch the + // transaction itself to be used for detecting a duplicate transaction + // without needing to do a separate lookup. + utxoView, err := mp.fetchInputUtxos(tx) + if err != nil { + if cerr, ok := err.(blockchain.RuleError); ok { + return nil, chainRuleError(cerr) + } + + return nil, err + } + + // Don't allow the transaction if it exists in the main chain and is + // already fully spent. + prevOut := wire.OutPoint{Hash: *txHash} + for txOutIdx := range tx.MsgTx().TxOut { + prevOut.Index = uint32(txOutIdx) + + entry := utxoView.LookupEntry(prevOut) + if entry != nil && !entry.IsSpent() { + return nil, txRuleError(wire.RejectDuplicate, + "transaction already exists") + } + + utxoView.RemoveEntry(prevOut) + } + + // Transaction is an orphan if any of the referenced transaction + // outputs don't exist or are already spent. Adding orphans to the + // orphan pool is not handled by this function, and the caller should + // use maybeAddOrphan if this behavior is desired. + var missingParents []*chainhash.Hash + for outpoint, entry := range utxoView.Entries() { + if entry == nil || entry.IsSpent() { + // Must make a copy of the hash here since the iterator + // is replaced and taking its address directly would + // result in all the entries pointing to the same + // memory location and thus all be the final hash. + hashCopy := outpoint.Hash + missingParents = append(missingParents, &hashCopy) + } + } + + // Exit early if this transaction is missing parents. + if len(missingParents) > 0 { + log.Debugf("Tx %v is an orphan with missing parents: %v", + txHash, missingParents) + + return &MempoolAcceptResult{ + MissingParents: missingParents, + }, nil + } + + // Perform several checks on the transaction inputs using the invariant + // rules in blockchain for what transactions are allowed into blocks. + // Also returns the fees associated with the transaction which will be + // used later. + // + // NOTE: this check must be performed before `validateStandardness` to + // make sure a nil entry is not returned from `utxoView.LookupEntry`. + txFee, err := blockchain.CheckTransactionInputs( + tx, nextBlockHeight, utxoView, mp.cfg.ChainParams, + ) + if err != nil { + if cerr, ok := err.(blockchain.RuleError); ok { + return nil, chainRuleError(cerr) + } + return nil, err + } + + // Don't allow non-standard transactions or non-standard inputs if the + // network parameters forbid their acceptance. + err = mp.validateStandardness( + tx, nextBlockHeight, medianTimePast, utxoView, + ) + if err != nil { + return nil, err + } + + // Don't allow the transaction into the mempool unless its sequence + // lock is active, meaning that it'll be allowed into the next block + // with respect to its defined relative lock times. + sequenceLock, err := mp.cfg.CalcSequenceLock(tx, utxoView) + if err != nil { + if cerr, ok := err.(blockchain.RuleError); ok { + return nil, chainRuleError(cerr) + } + + return nil, err + } + + if !blockchain.SequenceLockActive( + sequenceLock, nextBlockHeight, medianTimePast, + ) { + + return nil, txRuleError(wire.RejectNonstandard, + "transaction's sequence locks on inputs not met") + } + + // Don't allow transactions with an excessive number of signature + // operations which would result in making it impossible to mine. + if err := mp.validateSigCost(tx, utxoView); err != nil { + return nil, err + } + + txSize := GetTxVirtualSize(tx) + + // Don't allow transactions with fees too low to get into a mined + // block. + err = mp.validateRelayFeeMet( + tx, txFee, txSize, utxoView, nextBlockHeight, isNew, rateLimit, + ) + if err != nil { + return nil, err + } + + // If the transaction has any conflicts, and we've made it this far, + // then we're processing a potential replacement. + var conflicts map[chainhash.Hash]*btcutil.Tx + if isReplacement { + conflicts, err = mp.validateReplacement(tx, txFee) + if err != nil { + return nil, err + } + } + + // Verify crypto signatures for each input and reject the transaction + // if any don't verify. + err = blockchain.ValidateTransactionScripts(tx, utxoView, + txscript.StandardVerifyFlags, mp.cfg.SigCache, + mp.cfg.HashCache) + if err != nil { + if cerr, ok := err.(blockchain.RuleError); ok { + return nil, chainRuleError(cerr) + } + return nil, err + } + + result := &MempoolAcceptResult{ + TxFee: btcutil.Amount(txFee), + TxSize: txSize, + Conflicts: conflicts, + utxoView: utxoView, + bestHeight: bestHeight, + } + + return result, nil +} + +// validateSegWitDeployment checks that when a transaction has witness data, +// segwit must be active. +func (mp *TxPool) validateSegWitDeployment(tx *btcutil.Tx) error { + // Exit early if this transaction doesn't have witness data. + if !tx.MsgTx().HasWitness() { + return nil + } + + // If a transaction has witness data, and segwit isn't active yet, then + // we won't accept it into the mempool as it can't be mined yet. + segwitActive, err := mp.cfg.IsDeploymentActive( + chaincfg.DeploymentSegwit, + ) + if err != nil { + return err + } + + // Exit early if segwit is active. + if segwitActive { + return nil + } + + simnetHint := "" + if mp.cfg.ChainParams.Net == wire.SimNet { + bestHeight := mp.cfg.BestHeight() + simnetHint = fmt.Sprintf(" (The threshold for segwit "+ + "activation is 300 blocks on simnet, current best "+ + "height is %d)", bestHeight) + } + str := fmt.Sprintf("transaction %v has witness data, "+ + "but segwit isn't active yet%s", tx.Hash(), simnetHint) + + return txRuleError(wire.RejectNonstandard, str) +} + +// validateStandardness checks the transaction passes both transaction standard +// and input standard. +func (mp *TxPool) validateStandardness(tx *btcutil.Tx, nextBlockHeight int32, + medianTimePast time.Time, utxoView *blockchain.UtxoViewpoint) error { + + // Exit early if we accept non-standard transactions. + // + // NOTE: if you modify this code to accept non-standard transactions, + // you should add code here to check that the transaction does a + // reasonable number of ECDSA signature verifications. + if mp.cfg.Policy.AcceptNonStd { + return nil + } + + // Check the transaction standard. + err := CheckTransactionStandard( + tx, nextBlockHeight, medianTimePast, + mp.cfg.Policy.MinRelayTxFee, mp.cfg.Policy.MaxTxVersion, + ) + if err != nil { + // Attempt to extract a reject code from the error so it can be + // retained. When not possible, fall back to a non standard + // error. + rejectCode, found := extractRejectCode(err) + if !found { + rejectCode = wire.RejectNonstandard + } + str := fmt.Sprintf("transaction %v is not standard: %v", + tx.Hash(), err) + + return txRuleError(rejectCode, str) + } + + // Check the inputs standard. + err = checkInputsStandard(tx, utxoView) + if err != nil { + // Attempt to extract a reject code from the error so it can be + // retained. When not possible, fall back to a non-standard + // error. + rejectCode, found := extractRejectCode(err) + if !found { + rejectCode = wire.RejectNonstandard + } + str := fmt.Sprintf("transaction %v has a non-standard "+ + "input: %v", tx.Hash(), err) + + return txRuleError(rejectCode, str) + } + + return nil +} + +// validateSigCost checks the cost to run the signature operations to make sure +// the number of singatures are sane. +func (mp *TxPool) validateSigCost(tx *btcutil.Tx, + utxoView *blockchain.UtxoViewpoint) error { + + // Since the coinbase address itself can contain signature operations, + // the maximum allowed signature operations per transaction is less + // than the maximum allowed signature operations per block. + // + // TODO(roasbeef): last bool should be conditional on segwit activation + sigOpCost, err := blockchain.GetSigOpCost( + tx, false, utxoView, true, true, + ) + if err != nil { + if cerr, ok := err.(blockchain.RuleError); ok { + return chainRuleError(cerr) + } + + return err + } + + // Exit early if the sig cost is under limit. + if sigOpCost <= mp.cfg.Policy.MaxSigOpCostPerTx { + return nil + } + + str := fmt.Sprintf("transaction %v sigop cost is too high: %d > %d", + tx.Hash(), sigOpCost, mp.cfg.Policy.MaxSigOpCostPerTx) + + return txRuleError(wire.RejectNonstandard, str) +} + +// validateRelayFeeMet checks that the min relay fee is covered by this +// transaction. +func (mp *TxPool) validateRelayFeeMet(tx *btcutil.Tx, txFee, txSize int64, + utxoView *blockchain.UtxoViewpoint, nextBlockHeight int32, + isNew, rateLimit bool) error { + + txHash := tx.Hash() + + // Most miners allow a free transaction area in blocks they mine to go + // alongside the area used for high-priority transactions as well as + // transactions with fees. A transaction size of up to 1000 bytes is + // considered safe to go into this section. Further, the minimum fee + // calculated below on its own would encourage several small + // transactions to avoid fees rather than one single larger transaction + // which is more desirable. Therefore, as long as the size of the + // transaction does not exceed 1000 less than the reserved space for + // high-priority transactions, don't require a fee for it. + minFee := calcMinRequiredTxRelayFee(txSize, mp.cfg.Policy.MinRelayTxFee) + + if txSize >= (DefaultBlockPrioritySize-1000) && txFee < minFee { + str := fmt.Sprintf("transaction %v has %d fees which is under "+ + "the required amount of %d", txHash, txFee, minFee) + + return txRuleError(wire.RejectInsufficientFee, str) + } + + // Exit early if the min relay fee is met. + if txFee >= minFee { + return nil + } + + // Exit early if this is neither a new tx or rate limited. + if !isNew && !rateLimit { + return nil + } + + // Require that free transactions have sufficient priority to be mined + // in the next block. Transactions which are being added back to the + // memory pool from blocks that have been disconnected during a reorg + // are exempted. + if isNew && !mp.cfg.Policy.DisableRelayPriority { + currentPriority := mining.CalcPriority( + tx.MsgTx(), utxoView, nextBlockHeight, + ) + if currentPriority <= mining.MinHighPriority { + str := fmt.Sprintf("transaction %v has insufficient "+ + "priority (%g <= %g)", txHash, + currentPriority, mining.MinHighPriority) + + return txRuleError(wire.RejectInsufficientFee, str) + } + } + + // We can only end up here when the rateLimit is true. Free-to-relay + // transactions are rate limited here to prevent penny-flooding with + // tiny transactions as a form of attack. + nowUnix := time.Now().Unix() + + // Decay passed data with an exponentially decaying ~10 minute window - + // matches bitcoind handling. + mp.pennyTotal *= math.Pow( + 1.0-1.0/600.0, float64(nowUnix-mp.lastPennyUnix), + ) + mp.lastPennyUnix = nowUnix + + // Are we still over the limit? + if mp.pennyTotal >= mp.cfg.Policy.FreeTxRelayLimit*10*1000 { + str := fmt.Sprintf("transaction %v has been rejected "+ + "by the rate limiter due to low fees", txHash) + + return txRuleError(wire.RejectInsufficientFee, str) + } + + oldTotal := mp.pennyTotal + mp.pennyTotal += float64(txSize) + log.Tracef("rate limit: curTotal %v, nextTotal: %v, limit %v", + oldTotal, mp.pennyTotal, mp.cfg.Policy.FreeTxRelayLimit*10*1000) + + return nil +} + // New returns a new memory pool for validating and storing standalone // transactions until they are mined into a block. func New(cfg *Config) *TxPool { From 32404c302bb768946dbd42ea5e3e70ee80bd96ca Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 29 Nov 2023 16:47:18 +0800 Subject: [PATCH 3/8] mempool+rpcserver: add interface `TxMempool` This commit adds a new interface `TxMempool` which defines how other subsystems interact with `TxPool`. --- electrum/server.go | 2 +- mempool/interface.go | 71 ++++++++++++++++++++++++++++++++++++++++++++ mempool/mempool.go | 34 ++++++--------------- netsync/manager.go | 4 +-- rpcserver.go | 6 ++-- 5 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 mempool/interface.go diff --git a/electrum/server.go b/electrum/server.go index 1e298df3..69091c42 100644 --- a/electrum/server.go +++ b/electrum/server.go @@ -577,7 +577,7 @@ func handleTransactionBroadcast(s *ElectrumServer, cmd *btcjson.Request, conn ne // Also, since an error is being returned to the caller, ensure the // transaction is removed from the memory pool. if len(acceptedTxs) == 0 || !acceptedTxs[0].Tx.Hash().IsEqual(tx.Hash()) { - s.cfg.Mempool.RemoveTransaction(tx, true, true) + s.cfg.Mempool.RemoveTransaction(tx, true) errStr := fmt.Errorf("transaction %v is not in accepted list", tx.Hash()) diff --git a/mempool/interface.go b/mempool/interface.go new file mode 100644 index 00000000..3b97c8aa --- /dev/null +++ b/mempool/interface.go @@ -0,0 +1,71 @@ +package mempool + +import ( + "time" + + "github.com/utreexo/utreexod/btcjson" + "github.com/utreexo/utreexod/btcutil" + "github.com/utreexo/utreexod/chaincfg/chainhash" + "github.com/utreexo/utreexod/wire" +) + +// TxMempool defines an interface that's used by other subsystems to interact +// with the mempool. +type TxMempool interface { + // LastUpdated returns the last time a transaction was added to or + // removed from the source pool. + LastUpdated() time.Time + + // TxDescs returns a slice of descriptors for all the transactions in + // the pool. + TxDescs() []*TxDesc + + // RawMempoolVerbose returns all the entries in the mempool as a fully + // populated btcjson result. + RawMempoolVerbose() map[string]*btcjson.GetRawMempoolVerboseResult + + // Count returns the number of transactions in the main pool. It does + // not include the orphan pool. + Count() int + + // FetchTransaction returns the requested transaction from the + // transaction pool. This only fetches from the main transaction pool + // and does not include orphans. + FetchTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) + + // HaveTransaction returns whether or not the passed transaction + // already exists in the main pool or in the orphan pool. + HaveTransaction(hash *chainhash.Hash) bool + + // ProcessTransaction is the main workhorse for handling insertion of + // new free-standing transactions into the memory pool. It includes + // functionality such as rejecting duplicate transactions, ensuring + // transactions follow all rules, orphan transaction handling, and + // insertion into the memory pool. + // + // It returns a slice of transactions added to the mempool. When the + // error is nil, the list will include the passed transaction itself + // along with any additional orphan transactions that were added as a + // result of the passed one being accepted. + ProcessTransaction(tx *btcutil.Tx, allowOrphan, + rateLimit bool, tag Tag) ([]*TxDesc, error) + + // RemoveTransaction removes the passed transaction from the mempool. + // When the removeRedeemers flag is set, any transactions that redeem + // outputs from the removed transaction will also be removed + // recursively from the mempool, as they would otherwise become + // orphans. + RemoveTransaction(tx *btcutil.Tx, removeRedeemers bool) + + // CheckMempoolAcceptance behaves similarly to bitcoind's + // `testmempoolaccept` RPC method. It will perform a series of checks + // to decide whether this transaction can be accepted to the mempool. + // If not, the specific error is returned and the caller needs to take + // actions based on it. + CheckMempoolAcceptance(tx *btcutil.Tx) (*MempoolAcceptResult, error) + + // CheckSpend checks whether the passed outpoint is already spent by + // a transaction in the mempool. If that's the case the spending + // transaction will be returned, if not nil will be returned. + CheckSpend(op wire.OutPoint) *btcutil.Tx +} diff --git a/mempool/mempool.go b/mempool/mempool.go index 31d35e2a..b566f26c 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -210,6 +210,9 @@ type TxPool struct { // Ensure the TxPool type implements the mining.TxSource interface. var _ mining.TxSource = (*TxPool)(nil) +// Ensure the TxPool type implements the TxMemPool interface. +var _ TxMempool = (*TxPool)(nil) + // removeOrphan is the internal function which implements the public // RemoveOrphan. See the comment for RemoveOrphan for more details. // @@ -481,14 +484,14 @@ func (mp *TxPool) HaveTransaction(hash *chainhash.Hash) bool { // RemoveTransaction. See the comment for RemoveTransaction for more details. // // This function MUST be called with the mempool lock held (for writes). -func (mp *TxPool) removeTransaction(tx *btcutil.Tx, removeRedeemers, uncacheUtreexo bool) { +func (mp *TxPool) removeTransaction(tx *btcutil.Tx, removeRedeemers bool) { txHash := tx.Hash() if removeRedeemers { // Remove any transactions which rely on this one. for i := uint32(0); i < uint32(len(tx.MsgTx().TxOut)); i++ { prevOut := wire.OutPoint{Hash: *txHash, Index: i} if txRedeemer, exists := mp.outpoints[prevOut]; exists { - mp.removeTransaction(txRedeemer, true, uncacheUtreexo) + mp.removeTransaction(txRedeemer, true) } } } @@ -501,25 +504,6 @@ func (mp *TxPool) removeTransaction(tx *btcutil.Tx, removeRedeemers, uncacheUtre mp.cfg.AddrIndex.RemoveUnconfirmedTx(txHash) } - // If the utreexo view is active, then remove the cached hashes from the - // accumulator. - if mp.cfg.IsUtreexoViewActive != nil && mp.cfg.IsUtreexoViewActive() && uncacheUtreexo { - leaves, found := mp.poolLeaves[*txHash] - if !found { - log.Debugf("missing the leaf hashes for tx %s from while "+ - "removing it from the pool", - tx.MsgTx().TxHash().String()) - } else { - delete(mp.poolLeaves, *txHash) - - err := mp.cfg.PruneFromAccumulator(leaves) - if err != nil { - log.Infof("err while pruning proof for inputs of tx %s: ", - err, tx.MsgTx().TxHash().String()) - } - } - } - // Mark the referenced outpoints as unspent by the pool. for _, txIn := range txDesc.Tx.MsgTx().TxIn { delete(mp.outpoints, txIn.PreviousOutPoint) @@ -536,10 +520,10 @@ func (mp *TxPool) removeTransaction(tx *btcutil.Tx, removeRedeemers, uncacheUtre // it'll uncache the utreexo proof for the given tx if it's cached. // // This function is safe for concurrent access. -func (mp *TxPool) RemoveTransaction(tx *btcutil.Tx, removeRedeemers, uncacheUtreexo bool) { +func (mp *TxPool) RemoveTransaction(tx *btcutil.Tx, removeRedeemers bool) { // Protect concurrent access. mp.mtx.Lock() - mp.removeTransaction(tx, removeRedeemers, uncacheUtreexo) + mp.removeTransaction(tx, removeRedeemers) mp.mtx.Unlock() } @@ -556,7 +540,7 @@ func (mp *TxPool) RemoveDoubleSpends(tx *btcutil.Tx) { for _, txIn := range tx.MsgTx().TxIn { if txRedeemer, ok := mp.outpoints[txIn.PreviousOutPoint]; ok { if !txRedeemer.Hash().IsEqual(tx.Hash()) { - mp.removeTransaction(txRedeemer, true, true) + mp.removeTransaction(txRedeemer, true) } } } @@ -1052,7 +1036,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit, // // Don't remove the cached utreexo proof either because we'll need // it for the ingestion. - mp.removeTransaction(conflict, false, false) + mp.removeTransaction(conflict, false) } txD := mp.addTransaction(r.utxoView, tx, r.bestHeight, int64(r.TxFee)) diff --git a/netsync/manager.go b/netsync/manager.go index b7e84863..486bfe72 100644 --- a/netsync/manager.go +++ b/netsync/manager.go @@ -1717,7 +1717,7 @@ func (sm *SyncManager) handleBlockchainNotification(notification *blockchain.Not // transaction are NOT removed recursively because they are still // valid. for _, tx := range block.Transactions()[1:] { - sm.txMemPool.RemoveTransaction(tx, false, true) + sm.txMemPool.RemoveTransaction(tx, false) sm.txMemPool.RemoveDoubleSpends(tx) sm.txMemPool.RemoveOrphan(tx) sm.peerNotifier.TransactionConfirmed(tx) @@ -1756,7 +1756,7 @@ func (sm *SyncManager) handleBlockchainNotification(notification *blockchain.Not // Remove the transaction and all transactions // that depend on it if it wasn't accepted into // the transaction pool. - sm.txMemPool.RemoveTransaction(tx, true, true) + sm.txMemPool.RemoveTransaction(tx, true) } } diff --git a/rpcserver.go b/rpcserver.go index 8ef91a3b..90a8cb3f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -3933,7 +3933,7 @@ func handleRebroadcastUnconfirmedBDKTxs(s *rpcServer, cmd interface{}, closeChan tx := unconfirmedTx.Tx // If it's already in the mempool, then we've already broadcasted it. - if s.cfg.TxMemPool.IsTransactionInPool(tx.Hash()) { + if s.cfg.TxMemPool.HaveTransaction(tx.Hash()) { continue } @@ -4322,7 +4322,7 @@ func (s *rpcServer) rpcProcessTx(tx *btcutil.Tx, allowOrphan, rateLimit bool) er // Also, since an error is being returned to the caller, ensure the // transaction is removed from the memory pool. if len(acceptedTxs) == 0 || !acceptedTxs[0].Tx.Hash().IsEqual(tx.Hash()) { - s.cfg.TxMemPool.RemoveTransaction(tx, true, true) + s.cfg.TxMemPool.RemoveTransaction(tx, true) errStr := fmt.Sprintf("transaction %v is not in accepted list", tx.Hash()) @@ -5633,7 +5633,7 @@ type rpcserverConfig struct { DB database.DB // TxMemPool defines the transaction memory pool to interact with. - TxMemPool *mempool.TxPool + TxMemPool mempool.TxMempool // These fields allow the RPC server to interface with mining. // From bf48a224140c3713451a034b83e2adbbeea32ea1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 5 Nov 2023 06:59:30 +0800 Subject: [PATCH 4/8] btcd: add new RPC method `testmempoolaccept` --- go.mod | 1 + go.sum | 1 + mempool/mocks.go | 121 ++++++++++++++ rpcserver.go | 124 ++++++++++++++ rpcserver_test.go | 413 ++++++++++++++++++++++++++++++++++++++++++++++ rpcserverhelp.go | 18 ++ 6 files changed, 678 insertions(+) create mode 100644 mempool/mocks.go create mode 100644 rpcserver_test.go diff --git a/go.mod b/go.mod index 99aa10aa..2440ecfc 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.1.0 // indirect golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 2d4e62a0..ed490dc4 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,7 @@ github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/mempool/mocks.go b/mempool/mocks.go new file mode 100644 index 00000000..1a762453 --- /dev/null +++ b/mempool/mocks.go @@ -0,0 +1,121 @@ +package mempool + +import ( + "time" + + "github.com/stretchr/testify/mock" + "github.com/utreexo/utreexod/btcjson" + "github.com/utreexo/utreexod/btcutil" + "github.com/utreexo/utreexod/chaincfg/chainhash" + "github.com/utreexo/utreexod/wire" +) + +// MockTxMempool is a mock implementation of the TxMempool interface. +type MockTxMempool struct { + mock.Mock +} + +// Ensure the MockTxMempool implements the TxMemPool interface. +var _ TxMempool = (*MockTxMempool)(nil) + +// LastUpdated returns the last time a transaction was added to or removed from +// the source pool. +func (m *MockTxMempool) LastUpdated() time.Time { + args := m.Called() + return args.Get(0).(time.Time) +} + +// TxDescs returns a slice of descriptors for all the transactions in the pool. +func (m *MockTxMempool) TxDescs() []*TxDesc { + args := m.Called() + return args.Get(0).([]*TxDesc) +} + +// RawMempoolVerbose returns all the entries in the mempool as a fully +// populated btcjson result. +func (m *MockTxMempool) RawMempoolVerbose() map[string]*btcjson. + GetRawMempoolVerboseResult { + + args := m.Called() + return args.Get(0).(map[string]*btcjson.GetRawMempoolVerboseResult) +} + +// Count returns the number of transactions in the main pool. It does not +// include the orphan pool. +func (m *MockTxMempool) Count() int { + args := m.Called() + return args.Get(0).(int) +} + +// FetchTransaction returns the requested transaction from the transaction +// pool. This only fetches from the main transaction pool and does not include +// orphans. +func (m *MockTxMempool) FetchTransaction( + txHash *chainhash.Hash) (*btcutil.Tx, error) { + + args := m.Called(txHash) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*btcutil.Tx), args.Error(1) +} + +// HaveTransaction returns whether or not the passed transaction already exists +// in the main pool or in the orphan pool. +func (m *MockTxMempool) HaveTransaction(hash *chainhash.Hash) bool { + args := m.Called(hash) + return args.Get(0).(bool) +} + +// ProcessTransaction is the main workhorse for handling insertion of new +// free-standing transactions into the memory pool. It includes functionality +// such as rejecting duplicate transactions, ensuring transactions follow all +// rules, orphan transaction handling, and insertion into the memory pool. +func (m *MockTxMempool) ProcessTransaction(tx *btcutil.Tx, allowOrphan, + rateLimit bool, tag Tag) ([]*TxDesc, error) { + + args := m.Called(tx, allowOrphan, rateLimit, tag) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).([]*TxDesc), args.Error(1) +} + +// RemoveTransaction removes the passed transaction from the mempool. When the +// removeRedeemers flag is set, any transactions that redeem outputs from the +// removed transaction will also be removed recursively from the mempool, as +// they would otherwise become orphans. +func (m *MockTxMempool) RemoveTransaction(tx *btcutil.Tx, + removeRedeemers bool) { + + m.Called(tx, removeRedeemers) +} + +// CheckMempoolAcceptance behaves similarly to bitcoind's `testmempoolaccept` +// RPC method. It will perform a series of checks to decide whether this +// transaction can be accepted to the mempool. If not, the specific error is +// returned and the caller needs to take actions based on it. +func (m *MockTxMempool) CheckMempoolAcceptance( + tx *btcutil.Tx) (*MempoolAcceptResult, error) { + + args := m.Called(tx) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*MempoolAcceptResult), args.Error(1) +} + +// CheckSpend checks whether the passed outpoint is already spent by a +// transaction in the mempool. If that's the case the spending transaction will +// be returned, if not nil will be returned. +func (m *MockTxMempool) CheckSpend(op wire.OutPoint) *btcutil.Tx { + args := m.Called(op) + + return args.Get(0).(*btcutil.Tx) +} diff --git a/rpcserver.go b/rpcserver.go index 90a8cb3f..9a7f7f0b 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -80,6 +80,10 @@ const ( // maxProtocolVersion is the max protocol version the server supports. maxProtocolVersion = 70002 + + // defaultMaxFeeRate is the default value to use(0.1 BTC/kvB) when the + // `MaxFee` field is not set when calling `testmempoolaccept`. + defaultMaxFeeRate = 0.1 ) var ( @@ -205,6 +209,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "verifymessage": handleVerifyMessage, "verifyutxochaintipinclusionproof": handleVerifyUtxoChainTipInclusionProof, "version": handleVersion, + "testmempoolaccept": handleTestMempoolAccept, } // list of commands that we recognize, but for which btcd has no support because @@ -4763,6 +4768,125 @@ func handleVerifyUtxoChainTipInclusionProof(s *rpcServer, cmd interface{}, close return true, nil } +// handleTestMempoolAccept implements the testmempoolaccept command. +func handleTestMempoolAccept(s *rpcServer, cmd interface{}, + closeChan <-chan struct{}) (interface{}, error) { + + c := cmd.(*btcjson.TestMempoolAcceptCmd) + + // Create txns to hold the decoded tx. + txns := make([]*btcutil.Tx, 0, len(c.RawTxns)) + + // Iterate the raw hex slice and decode them. + for _, rawTx := range c.RawTxns { + rawBytes, err := hex.DecodeString(rawTx) + if err != nil { + return nil, rpcDecodeHexError(rawTx) + } + + tx, err := btcutil.NewTxFromBytes(rawBytes) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDeserialization, + Message: "TX decode failed: " + err.Error(), + } + } + + txns = append(txns, tx) + } + + results := make([]*btcjson.TestMempoolAcceptResult, 0, len(txns)) + for _, tx := range txns { + // Create a test result item. + item := &btcjson.TestMempoolAcceptResult{ + Txid: tx.Hash().String(), + Wtxid: tx.WitnessHash().String(), + } + + // Check the mempool acceptance. + result, err := s.cfg.TxMemPool.CheckMempoolAcceptance(tx) + + // If an error is returned, this tx is not allow, hence we + // record the reason. + if err != nil { + item.Allowed = false + + // TODO(yy): differentiate the errors and put package + // error in `PackageError` field. + item.RejectReason = err.Error() + + results = append(results, item) + + // Move to the next transaction. + continue + } + + // If this transaction is an orphan, it's not allowed. + if result.MissingParents != nil { + item.Allowed = false + + // NOTE: "missing-inputs" is what bitcoind returns + // here, so we mimic the same error message. + item.RejectReason = "missing-inputs" + + results = append(results, item) + + // Move to the next transaction. + continue + } + + // Otherwise this tx is allowed if its fee rate is below the + // max fee rate, we now patch the fields in + // `TestMempoolAcceptItem` as much as possible. + // + // Calculate the fee field and validate its fee rate. + item.Fees, item.Allowed = validateFeeRate( + result.TxFee, result.TxSize, c.MaxFeeRate, + ) + + // If the fee rate check passed, assign the corresponding + // fields. + if item.Allowed { + item.Vsize = int32(result.TxSize) + } else { + // NOTE: "max-fee-exceeded" is what bitcoind returns + // here, so we mimic the same error message. + item.RejectReason = "max-fee-exceeded" + } + + results = append(results, item) + } + + return results, nil +} + +// validateFeeRate checks that the fee rate used by transaction doesn't exceed +// the max fee rate specified. +func validateFeeRate(feeSats btcutil.Amount, txSize int64, + maxFeeRate float64) (*btcjson.TestMempoolAcceptFees, bool) { + + // Calculate fee rate in sats/kvB. + feeRateSatsPerKVB := feeSats * 1e3 / btcutil.Amount(txSize) + + // Convert sats/vB to BTC/kvB. + feeRate := feeRateSatsPerKVB.ToBTC() + + // Get the max fee rate, if not provided, default to 0.1 BTC/kvB. + if maxFeeRate == 0 { + maxFeeRate = defaultMaxFeeRate + } + + // If the fee rate is above the max fee rate, this tx is not accepted. + if feeRate > maxFeeRate { + return nil, false + } + + return &btcjson.TestMempoolAcceptFees{ + Base: feeSats.ToBTC(), + EffectiveFeeRate: feeRate, + }, true +} + // rpcServer provides a concurrent safe RPC server to a chain server. type rpcServer struct { started int32 diff --git a/rpcserver_test.go b/rpcserver_test.go new file mode 100644 index 00000000..314fceb3 --- /dev/null +++ b/rpcserver_test.go @@ -0,0 +1,413 @@ +package main + +import ( + "encoding/hex" + "errors" + "testing" + + "github.com/stretchr/testify/require" + "github.com/utreexo/utreexod/btcjson" + "github.com/utreexo/utreexod/btcutil" + "github.com/utreexo/utreexod/chaincfg/chainhash" + "github.com/utreexo/utreexod/mempool" +) + +// TestHandleTestMempoolAcceptFailDecode checks that when invalid hex string is +// used as the raw txns, the corresponding error is returned. +func TestHandleTestMempoolAcceptFailDecode(t *testing.T) { + t.Parallel() + + require := require.New(t) + + // Create a testing server. + s := &rpcServer{} + + testCases := []struct { + name string + txns []string + expectedErrCode btcjson.RPCErrorCode + }{ + { + name: "hex decode fail", + txns: []string{"invalid"}, + expectedErrCode: btcjson.ErrRPCDecodeHexString, + }, + { + name: "tx decode fail", + txns: []string{"696e76616c6964"}, + expectedErrCode: btcjson.ErrRPCDeserialization, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Create a request that uses invalid raw txns. + cmd := btcjson.NewTestMempoolAcceptCmd(tc.txns, 0) + + // Call the method under test. + closeChan := make(chan struct{}) + result, err := handleTestMempoolAccept( + s, cmd, closeChan, + ) + + // Ensure the expected error is returned. + require.Error(err) + rpcErr, ok := err.(*btcjson.RPCError) + require.True(ok) + require.Equal(tc.expectedErrCode, rpcErr.Code) + + // No result should be returned. + require.Nil(result) + }) + } +} + +var ( + // TODO(yy): make a `btctest` package and move these testing txns there + // so they be used in other tests. + // + // txHex1 is taken from `txscript/data/tx_valid.json`. + txHex1 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" + + "49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3" + + "000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507a" + + "c48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0" + + "140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271a" + + "d504b88ac00000000" + + // txHex2 is taken from `txscript/data/tx_valid.json`. + txHex2 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" + + "49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3" + + "000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507a" + + "c48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01fffffff" + + "f0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c27" + + "1ad504b88ac00000000" + + // txHex3 is taken from `txscript/data/tx_valid.json`. + txHex3 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" + + "49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621e" + + "f3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc350" + + "7ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01fffffff" + + "f0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c27" + + "1ad504b88ac00000000" +) + +// decodeTxHex decodes the given hex string into a transaction. +func decodeTxHex(t *testing.T, txHex string) *btcutil.Tx { + rawBytes, err := hex.DecodeString(txHex) + require.NoError(t, err) + tx, err := btcutil.NewTxFromBytes(rawBytes) + require.NoError(t, err) + + return tx +} + +// TestHandleTestMempoolAcceptMixedResults checks that when different txns get +// different responses from calling the mempool method `CheckMempoolAcceptance` +// their results are correctly returned. +func TestHandleTestMempoolAcceptMixedResults(t *testing.T) { + t.Parallel() + + require := require.New(t) + + // Create a mock mempool. + mm := &mempool.MockTxMempool{} + + // Create a testing server with the mock mempool. + s := &rpcServer{cfg: rpcserverConfig{ + TxMemPool: mm, + }} + + // Decode the hex so we can assert the mock mempool is called with it. + tx1 := decodeTxHex(t, txHex1) + tx2 := decodeTxHex(t, txHex2) + tx3 := decodeTxHex(t, txHex3) + + // Create a slice to hold the expected results. We will use three txns + // so we expect threeresults. + expectedResults := make([]*btcjson.TestMempoolAcceptResult, 3) + + // We now mock the first call to `CheckMempoolAcceptance` to return an + // error. + dummyErr := errors.New("dummy error") + mm.On("CheckMempoolAcceptance", tx1).Return(nil, dummyErr).Once() + + // Since the call failed, we expect the first result to give us the + // error. + expectedResults[0] = &btcjson.TestMempoolAcceptResult{ + Txid: tx1.Hash().String(), + Wtxid: tx1.WitnessHash().String(), + Allowed: false, + RejectReason: dummyErr.Error(), + } + + // We mock the second call to `CheckMempoolAcceptance` to return a + // result saying the tx is missing inputs. + mm.On("CheckMempoolAcceptance", tx2).Return( + &mempool.MempoolAcceptResult{ + MissingParents: []*chainhash.Hash{}, + }, nil, + ).Once() + + // We expect the second result to give us the missing-inputs error. + expectedResults[1] = &btcjson.TestMempoolAcceptResult{ + Txid: tx2.Hash().String(), + Wtxid: tx2.WitnessHash().String(), + Allowed: false, + RejectReason: "missing-inputs", + } + + // We mock the third call to `CheckMempoolAcceptance` to return a + // result saying the tx allowed. + const feeSats = btcutil.Amount(1000) + mm.On("CheckMempoolAcceptance", tx3).Return( + &mempool.MempoolAcceptResult{ + TxFee: feeSats, + TxSize: 100, + }, nil, + ).Once() + + // We expect the third result to give us the fee details. + expectedResults[2] = &btcjson.TestMempoolAcceptResult{ + Txid: tx3.Hash().String(), + Wtxid: tx3.WitnessHash().String(), + Allowed: true, + Vsize: 100, + Fees: &btcjson.TestMempoolAcceptFees{ + Base: feeSats.ToBTC(), + EffectiveFeeRate: feeSats.ToBTC() * 1e3 / 100, + }, + } + + // Create a mock request with default max fee rate of 0.1 BTC/KvB. + cmd := btcjson.NewTestMempoolAcceptCmd( + []string{txHex1, txHex2, txHex3}, 0.1, + ) + + // Call the method handler and assert the expected results are + // returned. + closeChan := make(chan struct{}) + results, err := handleTestMempoolAccept(s, cmd, closeChan) + require.NoError(err) + require.Equal(expectedResults, results) + + // Assert the mocked method is called as expected. + mm.AssertExpectations(t) +} + +// TestValidateFeeRate checks that `validateFeeRate` behaves as expected. +func TestValidateFeeRate(t *testing.T) { + t.Parallel() + + const ( + // testFeeRate is in BTC/kvB. + testFeeRate = 0.1 + + // testTxSize is in vb. + testTxSize = 100 + + // testFeeSats is in sats. + // We have 0.1BTC/kvB = + // 0.1 * 1e8 sats/kvB = + // 0.1 * 1e8 / 1e3 sats/vb = 0.1 * 1e5 sats/vb. + testFeeSats = btcutil.Amount(testFeeRate * 1e5 * testTxSize) + ) + + testCases := []struct { + name string + feeSats btcutil.Amount + txSize int64 + maxFeeRate float64 + expectedFees *btcjson.TestMempoolAcceptFees + allowed bool + }{ + { + // When the fee rate(0.1) is above the max fee + // rate(0.01), we expect a nil result and false. + name: "fee rate above max", + feeSats: testFeeSats, + txSize: testTxSize, + maxFeeRate: testFeeRate / 10, + expectedFees: nil, + allowed: false, + }, + { + // When the fee rate(0.1) is no greater than the max + // fee rate(0.1), we expect a result and true. + name: "fee rate below max", + feeSats: testFeeSats, + txSize: testTxSize, + maxFeeRate: testFeeRate, + expectedFees: &btcjson.TestMempoolAcceptFees{ + Base: testFeeSats.ToBTC(), + EffectiveFeeRate: testFeeRate, + }, + allowed: true, + }, + { + // When the fee rate(1) is above the default max fee + // rate(0.1), we expect a nil result and false. + name: "fee rate above default max", + feeSats: testFeeSats, + txSize: testTxSize / 10, + expectedFees: nil, + allowed: false, + }, + { + // When the fee rate(0.1) is no greater than the + // default max fee rate(0.1), we expect a result and + // true. + name: "fee rate below default max", + feeSats: testFeeSats, + txSize: testTxSize, + expectedFees: &btcjson.TestMempoolAcceptFees{ + Base: testFeeSats.ToBTC(), + EffectiveFeeRate: testFeeRate, + }, + allowed: true, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + result, allowed := validateFeeRate( + tc.feeSats, tc.txSize, tc.maxFeeRate, + ) + + require.Equal(tc.expectedFees, result) + require.Equal(tc.allowed, allowed) + }) + } +} + +// TestHandleTestMempoolAcceptFees checks that the `Fees` field is correctly +// populated based on the max fee rate and the tx being checked. +func TestHandleTestMempoolAcceptFees(t *testing.T) { + t.Parallel() + + // Create a mock mempool. + mm := &mempool.MockTxMempool{} + + // Create a testing server with the mock mempool. + s := &rpcServer{cfg: rpcserverConfig{ + TxMemPool: mm, + }} + + const ( + // Set transaction's fee rate to be 0.2BTC/kvB. + feeRate = defaultMaxFeeRate * 2 + + // txSize is 100vb. + txSize = 100 + + // feeSats is 2e6 sats. + feeSats = feeRate * 1e8 * txSize / 1e3 + ) + + testCases := []struct { + name string + maxFeeRate float64 + txHex string + rejectReason string + allowed bool + }{ + { + // When the fee rate(0.2) used by the tx is below the + // max fee rate(2) specified, the result should allow + // it. + name: "below max fee rate", + maxFeeRate: feeRate * 10, + txHex: txHex1, + allowed: true, + }, + { + // When the fee rate(0.2) used by the tx is above the + // max fee rate(0.02) specified, the result should + // disallow it. + name: "above max fee rate", + maxFeeRate: feeRate / 10, + txHex: txHex1, + allowed: false, + rejectReason: "max-fee-exceeded", + }, + { + // When the max fee rate is not set, the default + // 0.1BTC/kvB is used and the fee rate(0.2) used by the + // tx is above it, the result should disallow it. + name: "above default max fee rate", + txHex: txHex1, + allowed: false, + rejectReason: "max-fee-exceeded", + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + // Decode the hex so we can assert the mock mempool is + // called with it. + tx := decodeTxHex(t, txHex1) + + // We mock the call to `CheckMempoolAcceptance` to + // return the result. + mm.On("CheckMempoolAcceptance", tx).Return( + &mempool.MempoolAcceptResult{ + TxFee: feeSats, + TxSize: txSize, + }, nil, + ).Once() + + // We expect the third result to give us the fee + // details. + expected := &btcjson.TestMempoolAcceptResult{ + Txid: tx.Hash().String(), + Wtxid: tx.WitnessHash().String(), + Allowed: tc.allowed, + } + + if tc.allowed { + expected.Vsize = txSize + expected.Fees = &btcjson.TestMempoolAcceptFees{ + Base: feeSats / 1e8, + EffectiveFeeRate: feeRate, + } + } else { + expected.RejectReason = tc.rejectReason + } + + // Create a mock request with specified max fee rate. + cmd := btcjson.NewTestMempoolAcceptCmd( + []string{txHex1}, tc.maxFeeRate, + ) + + // Call the method handler and assert the expected + // result is returned. + closeChan := make(chan struct{}) + r, err := handleTestMempoolAccept(s, cmd, closeChan) + require.NoError(err) + + // Check the interface type. + results, ok := r.([]*btcjson.TestMempoolAcceptResult) + require.True(ok) + + // Expect exactly one result. + require.Len(results, 1) + + // Check the result is returned as expected. + require.Equal(expected, results[0]) + + // Assert the mocked method is called as expected. + mm.AssertExpectations(t) + }) + } +} diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 6a6b2c9e..8c47badd 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -887,6 +887,23 @@ var helpDescsEnUS = map[string]string{ "versionresult-patch": "The patch component of the JSON-RPC API version", "versionresult-prerelease": "Prerelease info about the current build", "versionresult-buildmetadata": "Metadata about the current build", + + // TestMempoolAcceptCmd help. + "testmempoolaccept--synopsis": "Returns result of mempool acceptance tests indicating if raw transaction(s) would be accepted by mempool.", + "testmempoolaccept-rawtxns": "Serialized transactions to test.", + "testmempoolaccept-maxfeerate": "Maximum acceptable fee rate in BTC/kB", + + // TestMempoolAcceptCmd result help. + "testmempoolacceptresult-txid": "The transaction hash in hex.", + "testmempoolacceptresult-wtxid": "The transaction witness hash in hex.", + "testmempoolacceptresult-package-error": "Package validation error, if any (only possible if rawtxs had more than 1 transaction).", + "testmempoolacceptresult-allowed": "Whether the transaction would be accepted to the mempool.", + "testmempoolacceptresult-vsize": "Virtual transaction size as defined in BIP 141.(only present when 'allowed' is true)", + "testmempoolacceptresult-reject-reason": "Rejection string (only present when 'allowed' is false).", + "testmempoolacceptresult-fees": "Transaction fees (only present if 'allowed' is true).", + "testmempoolacceptfees-base": "Transaction fees (only present if 'allowed' is true).", + "testmempoolacceptfees-effective-feerate": "The effective feerate in BTC per KvB.", + "testmempoolacceptfees-effective-includes": "Transactions whose fees and vsizes are included in effective-feerate. Each item is a transaction wtxid in hex.", } // rpcResultTypes specifies the result types that each RPC command can return. @@ -963,6 +980,7 @@ var rpcResultTypes = map[string][]interface{}{ "verifymessage": {(*bool)(nil)}, "verifyutxochaintipinclusionproof": {(*bool)(nil)}, "version": {(*map[string]btcjson.VersionResult)(nil)}, + "testmempoolaccept": {(*[]btcjson.TestMempoolAcceptResult)(nil)}, // Websocket commands. "loadtxfilter": nil, From 87c51f1ebc9e869842ecd45090e45cde43eeda06 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 4 Nov 2023 02:08:38 +0800 Subject: [PATCH 5/8] btcjson: add `TestMempoolAcceptCmd` --- btcjson/chainsvrcmds.go | 22 ++++++++++++++++++++++ btcjson/chainsvrcmds_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/btcjson/chainsvrcmds.go b/btcjson/chainsvrcmds.go index d194d63a..f1305292 100644 --- a/btcjson/chainsvrcmds.go +++ b/btcjson/chainsvrcmds.go @@ -1267,6 +1267,27 @@ func NewVerifyUtxoChainTipInclusionProofCmd(proof string) *VerifyUtxoChainTipInc return &VerifyUtxoChainTipInclusionProofCmd{Proof: proof} } +// TestMempoolAcceptCmd defines the testmempoolaccept JSON-RPC command. +type TestMempoolAcceptCmd struct { + // An array of hex strings of raw transactions. + RawTxns []string + + // Reject transactions whose fee rate is higher than the specified + // value, expressed in BTC/kvB, optional, default="0.10". + MaxFeeRate float64 `json:"omitempty"` +} + +// NewTestMempoolAcceptCmd returns a new instance which can be used to issue a +// testmempoolaccept JSON-RPC command. +func NewTestMempoolAcceptCmd(rawTxns []string, + maxFeeRate float64) *TestMempoolAcceptCmd { + + return &TestMempoolAcceptCmd{ + RawTxns: rawTxns, + MaxFeeRate: maxFeeRate, + } +} + func init() { // No special flags for commands in this file. flags := UsageFlag(0) @@ -1346,4 +1367,5 @@ func init() { MustRegisterCmd("verifymessage", (*VerifyMessageCmd)(nil), flags) MustRegisterCmd("verifytxoutproof", (*VerifyTxOutProofCmd)(nil), flags) MustRegisterCmd("verifyutxochaintipinclusionproof", (*VerifyUtxoChainTipInclusionProofCmd)(nil), flags) + MustRegisterCmd("testmempoolaccept", (*TestMempoolAcceptCmd)(nil), flags) } diff --git a/btcjson/chainsvrcmds_test.go b/btcjson/chainsvrcmds_test.go index 17a50c31..e90fef8b 100644 --- a/btcjson/chainsvrcmds_test.go +++ b/btcjson/chainsvrcmds_test.go @@ -1490,6 +1490,34 @@ func TestChainSvrCmds(t *testing.T) { marshalled: `{"jsonrpc":"1.0","method":"getdescriptorinfo","params":["123"],"id":1}`, unmarshalled: &btcjson.GetDescriptorInfoCmd{Descriptor: "123"}, }, + { + name: "testmempoolaccept", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("testmempoolaccept", []string{"rawhex"}, 0.1) + }, + staticCmd: func() interface{} { + return btcjson.NewTestMempoolAcceptCmd([]string{"rawhex"}, 0.1) + }, + marshalled: `{"jsonrpc":"1.0","method":"testmempoolaccept","params":[["rawhex"],0.1],"id":1}`, + unmarshalled: &btcjson.TestMempoolAcceptCmd{ + RawTxns: []string{"rawhex"}, + MaxFeeRate: 0.1, + }, + }, + { + name: "testmempoolaccept with maxfeerate", + newCmd: func() (interface{}, error) { + return btcjson.NewCmd("testmempoolaccept", []string{"rawhex"}, 0.01) + }, + staticCmd: func() interface{} { + return btcjson.NewTestMempoolAcceptCmd([]string{"rawhex"}, 0.01) + }, + marshalled: `{"jsonrpc":"1.0","method":"testmempoolaccept","params":[["rawhex"],0.01],"id":1}`, + unmarshalled: &btcjson.TestMempoolAcceptCmd{ + RawTxns: []string{"rawhex"}, + MaxFeeRate: 0.01, + }, + }, } t.Logf("Running %d tests", len(tests)) From 511078b46d2def408ba80c4ffeda1e95c2b09584 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 4 Nov 2023 02:26:39 +0800 Subject: [PATCH 6/8] rpcclient: support `testmempoolaccept` for `bitcoind` --- btcjson/chainsvrresults.go | 60 +++++++++++++++- rpcclient/errors.go | 13 ++++ rpcclient/rawtransactions.go | 132 +++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 rpcclient/errors.go diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 79a3d6e2..06ce2a65 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -347,8 +347,8 @@ type GetBlockTemplateResult struct { NonceRange string `json:"noncerange,omitempty"` // Block proposal from BIP 0023. - Capabilities []string `json:"capabilities,omitempty"` - RejectReasion string `json:"reject-reason,omitempty"` + Capabilities []string `json:"capabilities,omitempty"` + RejectReason string `json:"reject-reason,omitempty"` } // GetMempoolEntryResult models the data returned from the getmempoolentry's @@ -966,3 +966,59 @@ type CreateTransactionFromBDKWalletResult struct { TxHash string `json:"txhash"` RawBytes string `json:"rawbytes"` } + +// TestMempoolAcceptResult models the data from the testmempoolaccept command. +// The result of the mempool acceptance test for each raw transaction in the +// input array. Returns results for each transaction in the same order they +// were passed in. Transactions that cannot be fully validated due to failures +// in other transactions will not contain an 'allowed' result. +type TestMempoolAcceptResult struct { + // Txid is the transaction hash in hex. + Txid string `json:"txid"` + + // Wtxid is the transaction witness hash in hex. + Wtxid string `json:"wtxid"` + + // PackageError is the package validation error, if any (only possible + // if rawtxs had more than 1 transaction). + PackageError string `json:"package-error"` + + // Allowed specifies whether this tx would be accepted to the mempool + // and pass client-specified maxfeerate. If not present, the tx was not + // fully validated due to a failure in another tx in the list. + Allowed bool `json:"allowed,omitempty"` + + // Vsize is the virtual transaction size as defined in BIP 141. This is + // different from actual serialized size for witness transactions as + // witness data is discounted (only present when 'allowed' is true) + Vsize int32 `json:"vsize,omitempty"` + + // Fees specifies the transaction fees (only present if 'allowed' is + // true). + Fees *TestMempoolAcceptFees `json:"fees,omitempty"` + + // RejectReason is the rejection string (only present when 'allowed' is + // false). + RejectReason string `json:"reject-reason,omitempty"` +} + +// TestMempoolAcceptFees models the `fees` section from the testmempoolaccept +// command. +type TestMempoolAcceptFees struct { + // Base is the transaction fee in BTC. + Base float64 `json:"base"` + + // EffectiveFeeRate specifies the effective feerate in BTC per KvB. May + // differ from the base feerate if, for example, there are modified + // fees from prioritisetransaction or a package feerate was used. + // + // NOTE: this field only exists in bitcoind v25.0 and above. + EffectiveFeeRate float64 `json:"effective-feerate"` + + // EffectiveIncludes specifies transactions whose fees and vsizes are + // included in effective-feerate. Each item is a transaction wtxid in + // hex. + // + // NOTE: this field only exists in bitcoind v25.0 and above. + EffectiveIncludes []string `json:"effective-includes"` +} diff --git a/rpcclient/errors.go b/rpcclient/errors.go new file mode 100644 index 00000000..62de9b38 --- /dev/null +++ b/rpcclient/errors.go @@ -0,0 +1,13 @@ +package rpcclient + +import "errors" + +var ( + // ErrBitcoindVersion is returned when running against a bitcoind that + // is older than the minimum version supported by the rpcclient. + ErrBitcoindVersion = errors.New("bitcoind version too low") + + // ErrInvalidParam is returned when the caller provides an invalid + // parameter to an RPC method. + ErrInvalidParam = errors.New("invalid param") +) diff --git a/rpcclient/rawtransactions.go b/rpcclient/rawtransactions.go index ac513b52..ccf11a2c 100644 --- a/rpcclient/rawtransactions.go +++ b/rpcclient/rawtransactions.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/hex" "encoding/json" + "fmt" "github.com/utreexo/utreexod/btcjson" "github.com/utreexo/utreexod/btcutil" @@ -882,3 +883,134 @@ func (c *Client) DecodeScriptAsync(serializedScript []byte) FutureDecodeScriptRe func (c *Client) DecodeScript(serializedScript []byte) (*btcjson.DecodeScriptResult, error) { return c.DecodeScriptAsync(serializedScript).Receive() } + +// FutureTestMempoolAcceptResult is a future promise to deliver the result +// of a TestMempoolAccept RPC invocation (or an applicable error). +type FutureTestMempoolAcceptResult chan *Response + +// Receive waits for the Response promised by the future and returns the +// response from TestMempoolAccept. +func (r FutureTestMempoolAcceptResult) Receive() ( + []*btcjson.TestMempoolAcceptResult, error) { + + response, err := ReceiveFuture(r) + if err != nil { + return nil, err + } + + // Unmarshal as an array of TestMempoolAcceptResult items. + var results []*btcjson.TestMempoolAcceptResult + + err = json.Unmarshal(response, &results) + if err != nil { + return nil, err + } + + return results, nil +} + +// TestMempoolAcceptAsync returns an instance of a type that can be used to get +// the result of the RPC at some future time by invoking the Receive function +// on the returned instance. +// +// See TestMempoolAccept for the blocking version and more details. +func (c *Client) TestMempoolAcceptAsync(txns []*wire.MsgTx, + maxFeeRate float64) FutureTestMempoolAcceptResult { + + // Due to differences in the testmempoolaccept API for different + // backends, we'll need to inspect our version and construct the + // appropriate request. + version, err := c.BackendVersion() + if err != nil { + return newFutureError(err) + } + + log.Debugf("TestMempoolAcceptAsync: backend version %s", version) + + // Exit early if the version is below 22.0.0. + // + // Based on the history of `testmempoolaccept` in bitcoind, + // - introduced in 0.17.0 + // - unchanged in 0.18.0 + // - allowhighfees(bool) param is changed to maxfeerate(numeric) in + // 0.19.0 + // - unchanged in 0.20.0 + // - added fees and vsize fields in its response in 0.21.0 + // - allow more than one txes in param rawtx and added package-error + // and wtxid fields in its response in 0.22.0 + // - unchanged in 0.23.0 + // - unchanged in 0.24.0 + // - added effective-feerate and effective-includes fields in its + // response in 0.25.0 + // + // We decide to not support this call for versions below 22.0.0. as the + // request/response formats are very different. + if version < BitcoindPre22 { + err := fmt.Errorf("%w: %v", ErrBitcoindVersion, version) + return newFutureError(err) + } + + // The maximum number of transactions allowed is 25. + if len(txns) > 25 { + err := fmt.Errorf("%w: too many transactions provided", + ErrInvalidParam) + return newFutureError(err) + } + + // Exit early if an empty array of transactions is provided. + if len(txns) == 0 { + err := fmt.Errorf("%w: no transactions provided", + ErrInvalidParam) + return newFutureError(err) + } + + // Iterate all the transactions and turn them into hex strings. + rawTxns := make([]string, 0, len(txns)) + for _, tx := range txns { + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + + // TODO(yy): add similar checks found in `BtcDecode` to + // `BtcEncode` - atm it just serializes bytes without any + // bitcoin-specific checks. + if err := tx.Serialize(buf); err != nil { + err = fmt.Errorf("%w: %v", ErrInvalidParam, err) + return newFutureError(err) + } + + rawTx := hex.EncodeToString(buf.Bytes()) + rawTxns = append(rawTxns, rawTx) + + // Sanity check the provided tx is valid, which can be removed + // once we have similar checks added in `BtcEncode`. + // + // NOTE: must be performed after buf.Bytes is copied above. + // + // TODO(yy): remove it once the above TODO is addressed. + if err := tx.Deserialize(buf); err != nil { + err = fmt.Errorf("%w: %v", ErrInvalidParam, err) + return newFutureError(err) + } + } + + cmd := btcjson.NewTestMempoolAcceptCmd(rawTxns, maxFeeRate) + + return c.SendCmd(cmd) +} + +// TestMempoolAccept returns result of mempool acceptance tests indicating if +// raw transaction(s) would be accepted by mempool. +// +// If multiple transactions are passed in, parents must come before children +// and package policies apply: the transactions cannot conflict with any +// mempool transactions or each other. +// +// If one transaction fails, other transactions may not be fully validated (the +// 'allowed' key will be blank). +// +// The maximum number of transactions allowed is 25. +func (c *Client) TestMempoolAccept(txns []*wire.MsgTx, + maxFeeRate float64) ([]*btcjson.TestMempoolAcceptResult, error) { + + return c.TestMempoolAcceptAsync(txns, maxFeeRate).Receive() +} From bbfa799f5130891fdb9632175bc254f5d01c3481 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 5 Jan 2024 19:12:09 +0800 Subject: [PATCH 7/8] multi: add more bitcoind versions to the `BackendVersion` This commit adds bitcoind version 22.0 and 25.0 to our `BackendVersion` set to handle the `testmempoolaccept` RPC calls. A unit test is added to make sure the parser works as expected. --- go.mod | 1 + go.sum | 16 +------- rpcclient/chain.go | 7 ++-- rpcclient/chain_test.go | 14 +++++-- rpcclient/infrastructure.go | 52 ++++++++++++++++++++++++-- rpcclient/infrastructure_test.go | 64 ++++++++++++++++++++++++++++++++ rpcclient/rawtransactions.go | 8 ++-- 7 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 rpcclient/infrastructure_test.go diff --git a/go.mod b/go.mod index 2440ecfc..0f52b564 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/decred/dcrd/lru v1.0.0 + github.com/gorilla/websocket v1.5.0 github.com/jessevdk/go-flags v1.4.0 github.com/jrick/logrotate v1.0.0 github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 diff --git a/go.sum b/go.sum index ed490dc4..14ab5af6 100644 --- a/go.sum +++ b/go.sum @@ -84,20 +84,6 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/utreexo/utreexo v0.0.0-20240130053814-4aa282fe3804 h1:9uJDD7lEbhQ/JQhdIPVJ2yk8tLh8nvwNz87shyUe+RQ= -github.com/utreexo/utreexo v0.0.0-20240130053814-4aa282fe3804/go.mod h1:RT9JpZADhLr2YJVBgp48tmUxVeAHaAbOSr6p6nAEJpI= -github.com/utreexo/utreexo v0.0.0-20240207085148-754fd0816976 h1:1r3hZAaf7zhLd/hT4wYB8PkYOLylgCDlJaxS8MRitZA= -github.com/utreexo/utreexo v0.0.0-20240207085148-754fd0816976/go.mod h1:RT9JpZADhLr2YJVBgp48tmUxVeAHaAbOSr6p6nAEJpI= -github.com/utreexo/utreexo v0.1.0 h1:ddbIk/yU7sqZ/7Q2XoZhaeWqMBRwhHXaoFRvKYx7uLo= -github.com/utreexo/utreexo v0.1.0/go.mod h1:RT9JpZADhLr2YJVBgp48tmUxVeAHaAbOSr6p6nAEJpI= -github.com/utreexo/utreexo v0.1.2 h1:KLC8rlwPep3w9sYapEgDROYUBNmLzsO5aTPTorBJhJc= -github.com/utreexo/utreexo v0.1.2/go.mod h1:RT9JpZADhLr2YJVBgp48tmUxVeAHaAbOSr6p6nAEJpI= -github.com/utreexo/utreexo v0.1.3 h1:Z7tj8DTZNHd927ZWT7NSKx14+Dk6BoMeDPhDOmFlibo= -github.com/utreexo/utreexo v0.1.3/go.mod h1:RT9JpZADhLr2YJVBgp48tmUxVeAHaAbOSr6p6nAEJpI= -github.com/utreexo/utreexo v0.1.4 h1:jygjZscJEzab7woP7aB6YWUKjhsV5Mrbjj873inhwR8= -github.com/utreexo/utreexo v0.1.4/go.mod h1:RT9JpZADhLr2YJVBgp48tmUxVeAHaAbOSr6p6nAEJpI= -github.com/utreexo/utreexo v0.1.5 h1:nnG2VvwDYPkXCSRicV15eAbh2vvTp/g4Pot3vlseGdQ= -github.com/utreexo/utreexo v0.1.5/go.mod h1:RT9JpZADhLr2YJVBgp48tmUxVeAHaAbOSr6p6nAEJpI= github.com/utreexo/utreexo v0.2.1 h1:xycdaHK+HhDZO5MY6Clq7YeXEDguzar1GrAYqIJ1gvs= github.com/utreexo/utreexo v0.2.1/go.mod h1:xIu3cTtT0jNdntc1qJhVyQV/RX03Sk9gFD3UAzALvuU= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -113,6 +99,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -129,6 +116,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= diff --git a/rpcclient/chain.go b/rpcclient/chain.go index 9b29218d..de9305b2 100644 --- a/rpcclient/chain.go +++ b/rpcclient/chain.go @@ -440,17 +440,16 @@ func unmarshalPartialGetBlockChainInfoResult(res []byte) (*btcjson.GetBlockChain func unmarshalGetBlockChainInfoResultSoftForks(chainInfo *btcjson.GetBlockChainInfoResult, version BackendVersion, res []byte) error { - switch version { // Versions of bitcoind on or after v0.19.0 use the unified format. - case BitcoindPost19: + if version > BitcoindPre19 { var softForks btcjson.UnifiedSoftForks if err := json.Unmarshal(res, &softForks); err != nil { return err } chainInfo.UnifiedSoftForks = &softForks + } else { - // All other versions use the original format. - default: + // All other versions use the original format. var softForks btcjson.SoftForks if err := json.Unmarshal(res, &softForks); err != nil { return err diff --git a/rpcclient/chain_test.go b/rpcclient/chain_test.go index e32d547c..4357f08b 100644 --- a/rpcclient/chain_test.go +++ b/rpcclient/chain_test.go @@ -1,6 +1,12 @@ package rpcclient -import "testing" +import ( + "testing" + + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{} // TestUnmarshalGetBlockChainInfoResult ensures that the SoftForks and // UnifiedSoftForks fields of GetBlockChainInfoResult are properly unmarshaled @@ -22,7 +28,7 @@ func TestUnmarshalGetBlockChainInfoResultSoftForks(t *testing.T) { }, { name: "bitcoind >= 0.19.0 with separate softforks", - version: BitcoindPost19, + version: BitcoindPre22, res: []byte(`{"softforks": [{"version": 2}]}`), compatible: false, }, @@ -34,7 +40,7 @@ func TestUnmarshalGetBlockChainInfoResultSoftForks(t *testing.T) { }, { name: "bitcoind >= 0.19.0 with unified softforks", - version: BitcoindPost19, + version: BitcoindPre22, res: []byte(`{"softforks": {"segwit": {"type": "bip9"}}}`), compatible: true, }, @@ -76,7 +82,7 @@ func TestUnmarshalGetBlockChainInfoResultSoftForks(t *testing.T) { // If the version is compatible with the response, we // should expect to see the proper softforks field set. - if test.version == BitcoindPost19 && + if test.version == BitcoindPre22 && info.SoftForks != nil { t.Fatal("expected SoftForks to be empty") } diff --git a/rpcclient/infrastructure.go b/rpcclient/infrastructure.go index f928fe58..2b9c0747 100644 --- a/rpcclient/infrastructure.go +++ b/rpcclient/infrastructure.go @@ -114,14 +114,45 @@ const ( // BitcoindPre19 represents a bitcoind version before 0.19.0. BitcoindPre19 BackendVersion = iota - // BitcoindPost19 represents a bitcoind version equal to or greater than - // 0.19.0. - BitcoindPost19 + // BitcoindPre22 represents a bitcoind version equal to or greater than + // 0.19.0 and smaller than 22.0.0. + BitcoindPre22 + + // BitcoindPre25 represents a bitcoind version equal to or greater than + // 22.0.0 and smaller than 25.0.0. + BitcoindPre25 + + // BitcoindPre25 represents a bitcoind version equal to or greater than + // 25.0.0. + BitcoindPost25 // Btcd represents a catch-all btcd version. Btcd ) +// String returns a human-readable backend version. +func (b BackendVersion) String() string { + switch b { + case BitcoindPre19: + return "bitcoind 0.19 and below" + + case BitcoindPre22: + return "bitcoind v0.19.0-v22.0.0" + + case BitcoindPre25: + return "bitcoind v22.0.0-v25.0.0" + + case BitcoindPost25: + return "bitcoind v25.0.0 and above" + + case Btcd: + return "btcd" + + default: + return "unknown" + } +} + // Client represents a Bitcoin RPC client which allows easy access to the // various RPC methods available on a Bitcoin RPC server. Each of the wrapper // functions handle the details of converting the passed and return types to and @@ -1544,6 +1575,12 @@ const ( // bitcoind19Str is the string representation of bitcoind v0.19.0. bitcoind19Str = "0.19.0" + // bitcoind22Str is the string representation of bitcoind v22.0.0. + bitcoind22Str = "22.0.0" + + // bitcoind25Str is the string representation of bitcoind v25.0.0. + bitcoind25Str = "25.0.0" + // bitcoindVersionPrefix specifies the prefix included in every bitcoind // version exposed through GetNetworkInfo. bitcoindVersionPrefix = "/Satoshi:" @@ -1565,8 +1602,15 @@ func parseBitcoindVersion(version string) BackendVersion { switch { case version < bitcoind19Str: return BitcoindPre19 + + case version < bitcoind22Str: + return BitcoindPre22 + + case version < bitcoind25Str: + return BitcoindPre25 + default: - return BitcoindPost19 + return BitcoindPost25 } } diff --git a/rpcclient/infrastructure_test.go b/rpcclient/infrastructure_test.go new file mode 100644 index 00000000..e97fa275 --- /dev/null +++ b/rpcclient/infrastructure_test.go @@ -0,0 +1,64 @@ +package rpcclient + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestParseBitcoindVersion checks that the correct version from bitcoind's +// `getnetworkinfo` RPC call is parsed. +func TestParseBitcoindVersion(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + rpcVersion string + parsedVersion BackendVersion + }{ + { + name: "parse version 0.19 and below", + rpcVersion: "/Satoshi:0.18.0/", + parsedVersion: BitcoindPre19, + }, + { + name: "parse version 0.19", + rpcVersion: "/Satoshi:0.19.0/", + parsedVersion: BitcoindPre22, + }, + { + name: "parse version 0.19 - 22.0", + rpcVersion: "/Satoshi:0.20.1/", + parsedVersion: BitcoindPre22, + }, + { + name: "parse version 22.0", + rpcVersion: "/Satoshi:22.0.0/", + parsedVersion: BitcoindPre25, + }, + { + name: "parse version 22.0 - 25.0", + rpcVersion: "/Satoshi:23.0.0/", + parsedVersion: BitcoindPre25, + }, + { + name: "parse version 25.0", + rpcVersion: "/Satoshi:25.0.0/", + parsedVersion: BitcoindPost25, + }, + { + name: "parse version 25.0 and above", + rpcVersion: "/Satoshi:26.0.0/", + parsedVersion: BitcoindPost25, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + version := parseBitcoindVersion(tc.rpcVersion) + require.Equal(t, tc.parsedVersion, version) + }) + } +} diff --git a/rpcclient/rawtransactions.go b/rpcclient/rawtransactions.go index ccf11a2c..0cad8c1f 100644 --- a/rpcclient/rawtransactions.go +++ b/rpcclient/rawtransactions.go @@ -359,9 +359,8 @@ func (c *Client) SendRawTransactionAsync(tx *wire.MsgTx, allowHighFees bool) Fut } var cmd *btcjson.SendRawTransactionCmd - switch version { // Starting from bitcoind v0.19.0, the MaxFeeRate field should be used. - case BitcoindPost19: + if version > BitcoindPre19 { // Using a 0 MaxFeeRate is interpreted as a maximum fee rate not // being enforced by bitcoind. var maxFeeRate int32 @@ -369,9 +368,8 @@ func (c *Client) SendRawTransactionAsync(tx *wire.MsgTx, allowHighFees bool) Fut maxFeeRate = defaultMaxFeeRate } cmd = btcjson.NewBitcoindSendRawTransactionCmd(txHex, maxFeeRate) - - // Otherwise, use the AllowHighFees field. - default: + } else { + // Otherwise, use the AllowHighFees field. cmd = btcjson.NewSendRawTransactionCmd(txHex, &allowHighFees) } From 6a52210c11bf264160266c056e1efaa12ee8eb1d Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 5 Nov 2024 14:40:57 +0900 Subject: [PATCH 8/8] mempool: don't fetch from the utxo set for csns Because Utreexo nodes don't have an initialized utxo set, attempting to fetch from them will result in a runtime panic. We check if we're a utreexo node before attempting any fetches. --- mempool/mempool.go | 47 +++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/mempool/mempool.go b/mempool/mempool.go index b566f26c..d61ec2a2 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -1314,10 +1314,13 @@ func (mp *TxPool) RawMempoolVerbose() map[string]*btcjson.GetRawMempoolVerboseRe // input transactions can't be found for some reason. tx := desc.Tx var currentPriority float64 - utxos, err := mp.fetchInputUtxos(tx) - if err == nil { - currentPriority = mining.CalcPriority(tx.MsgTx(), utxos, - bestHeight+1) + // Don't calculate for utreexo nodes yet. + if mp.cfg.IsUtreexoViewActive == nil && !mp.cfg.IsUtreexoViewActive() { + utxos, err := mp.fetchInputUtxos(tx) + if err == nil { + currentPriority = mining.CalcPriority(tx.MsgTx(), utxos, + bestHeight+1) + } } mpd := &btcjson.GetRawMempoolVerboseResult{ @@ -1474,17 +1477,35 @@ func (mp *TxPool) checkMempoolAcceptance(tx *btcutil.Tx, return nil, err } - // Fetch all of the unspent transaction outputs referenced by the - // inputs to this transaction. This function also attempts to fetch the - // transaction itself to be used for detecting a duplicate transaction - // without needing to do a separate lookup. - utxoView, err := mp.fetchInputUtxos(tx) - if err != nil { - if cerr, ok := err.(blockchain.RuleError); ok { - return nil, chainRuleError(cerr) + var utxoView *blockchain.UtxoViewpoint + if mp.cfg.IsUtreexoViewActive != nil && mp.cfg.IsUtreexoViewActive() { + ud := tx.MsgTx().UData + + // First verify the proof to ensure that the proof the peer has + // sent was over valid. + err = mp.cfg.VerifyUData(ud, tx.MsgTx().TxIn, false) + if err != nil { + str := fmt.Sprintf("transaction %v failed the utreexo data verification. %v", + txHash, err) + return nil, txRuleError(wire.RejectInvalid, str) } + log.Debugf("VerifyUData passed for tx %s", txHash.String()) + + // After the validation passes, turn that proof into a utxoView. + utxoView = mp.fetchInputUtxosFromUData(tx, ud) + } else { + // Fetch all of the unspent transaction outputs referenced by the + // inputs to this transaction. This function also attempts to fetch the + // transaction itself to be used for detecting a duplicate transaction + // without needing to do a separate lookup. + utxoView, err = mp.fetchInputUtxos(tx) + if err != nil { + if cerr, ok := err.(blockchain.RuleError); ok { + return nil, chainRuleError(cerr) + } - return nil, err + return nil, err + } } // Don't allow the transaction if it exists in the main chain and is