mirror of
https://github.com/btcsuite/btcd.git
synced 2025-02-22 22:25:45 +01:00
multi: map btcd
mempool acceptance errors to bitcoind's testmempoolaccept
This commit creates a `RejectReasonMap` to map the errors returned from `btcd` to bitcoind's `testmempoolaccept` so the `RejectReason` is unified at the RPC level. To make sure the map keys are unique, the error strings are modified in `btcd`.
This commit is contained in:
parent
6c9f7fe7b9
commit
ef54c49df4
6 changed files with 192 additions and 25 deletions
|
@ -243,9 +243,9 @@ func CheckTransactionSanity(tx *btcutil.Tx) error {
|
||||||
return ruleError(ErrBadTxOutValue, str)
|
return ruleError(ErrBadTxOutValue, str)
|
||||||
}
|
}
|
||||||
if satoshi > btcutil.MaxSatoshi {
|
if satoshi > btcutil.MaxSatoshi {
|
||||||
str := fmt.Sprintf("transaction output value of %v is "+
|
str := fmt.Sprintf("transaction output value is "+
|
||||||
"higher than max allowed value of %v", satoshi,
|
"higher than max allowed value: %v > %v ",
|
||||||
btcutil.MaxSatoshi)
|
satoshi, btcutil.MaxSatoshi)
|
||||||
return ruleError(ErrBadTxOutValue, str)
|
return ruleError(ErrBadTxOutValue, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -968,8 +968,8 @@ func CheckTransactionInputs(tx *btcutil.Tx, txHeight int32, utxoView *UtxoViewpo
|
||||||
return 0, ruleError(ErrBadTxOutValue, str)
|
return 0, ruleError(ErrBadTxOutValue, str)
|
||||||
}
|
}
|
||||||
if originTxSatoshi > btcutil.MaxSatoshi {
|
if originTxSatoshi > btcutil.MaxSatoshi {
|
||||||
str := fmt.Sprintf("transaction output value of %v is "+
|
str := fmt.Sprintf("transaction output value is "+
|
||||||
"higher than max allowed value of %v",
|
"higher than max allowed value: %v > %v ",
|
||||||
btcutil.Amount(originTxSatoshi),
|
btcutil.Amount(originTxSatoshi),
|
||||||
btcutil.MaxSatoshi)
|
btcutil.MaxSatoshi)
|
||||||
return 0, ruleError(ErrBadTxOutValue, str)
|
return 0, ruleError(ErrBadTxOutValue, str)
|
||||||
|
|
|
@ -592,9 +592,9 @@ func (mp *TxPool) checkPoolDoubleSpend(tx *btcutil.Tx) (bool, error) {
|
||||||
// transactions or if it doesn't signal replacement.
|
// transactions or if it doesn't signal replacement.
|
||||||
if mp.cfg.Policy.RejectReplacement ||
|
if mp.cfg.Policy.RejectReplacement ||
|
||||||
!mp.signalsReplacement(conflict, nil) {
|
!mp.signalsReplacement(conflict, nil) {
|
||||||
str := fmt.Sprintf("output %v already spent by "+
|
str := fmt.Sprintf("output already spent in mempool: "+
|
||||||
"transaction %v in the memory pool",
|
"output=%v, tx=%v", txIn.PreviousOutPoint,
|
||||||
txIn.PreviousOutPoint, conflict.Hash())
|
conflict.Hash())
|
||||||
return false, txRuleError(wire.RejectDuplicate, str)
|
return false, txRuleError(wire.RejectDuplicate, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -842,7 +842,7 @@ func (mp *TxPool) validateReplacement(tx *btcutil.Tx,
|
||||||
// exceed the maximum allowed.
|
// exceed the maximum allowed.
|
||||||
conflicts := mp.txConflicts(tx)
|
conflicts := mp.txConflicts(tx)
|
||||||
if len(conflicts) > MaxReplacementEvictions {
|
if len(conflicts) > MaxReplacementEvictions {
|
||||||
str := fmt.Sprintf("replacement transaction %v evicts more "+
|
str := fmt.Sprintf("%v: replacement transaction evicts more "+
|
||||||
"transactions than permitted: max is %v, evicts %v",
|
"transactions than permitted: max is %v, evicts %v",
|
||||||
tx.Hash(), MaxReplacementEvictions, len(conflicts))
|
tx.Hash(), MaxReplacementEvictions, len(conflicts))
|
||||||
return nil, txRuleError(wire.RejectNonstandard, str)
|
return nil, txRuleError(wire.RejectNonstandard, str)
|
||||||
|
@ -855,7 +855,7 @@ func (mp *TxPool) validateReplacement(tx *btcutil.Tx,
|
||||||
if _, ok := conflicts[ancestorHash]; !ok {
|
if _, ok := conflicts[ancestorHash]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
str := fmt.Sprintf("replacement transaction %v spends parent "+
|
str := fmt.Sprintf("%v: replacement transaction spends parent "+
|
||||||
"transaction %v", tx.Hash(), ancestorHash)
|
"transaction %v", tx.Hash(), ancestorHash)
|
||||||
return nil, txRuleError(wire.RejectInvalid, str)
|
return nil, txRuleError(wire.RejectInvalid, str)
|
||||||
}
|
}
|
||||||
|
@ -876,7 +876,7 @@ func (mp *TxPool) validateReplacement(tx *btcutil.Tx,
|
||||||
)
|
)
|
||||||
for hash, conflict := range conflicts {
|
for hash, conflict := range conflicts {
|
||||||
if txFeeRate <= mp.pool[hash].FeePerKB {
|
if txFeeRate <= mp.pool[hash].FeePerKB {
|
||||||
str := fmt.Sprintf("replacement transaction %v has an "+
|
str := fmt.Sprintf("%v: replacement transaction has an "+
|
||||||
"insufficient fee rate: needs more than %v, "+
|
"insufficient fee rate: needs more than %v, "+
|
||||||
"has %v", tx.Hash(), mp.pool[hash].FeePerKB,
|
"has %v", tx.Hash(), mp.pool[hash].FeePerKB,
|
||||||
txFeeRate)
|
txFeeRate)
|
||||||
|
@ -897,7 +897,7 @@ func (mp *TxPool) validateReplacement(tx *btcutil.Tx,
|
||||||
// which is determined by our minimum relay fee.
|
// which is determined by our minimum relay fee.
|
||||||
minFee := calcMinRequiredTxRelayFee(txSize, mp.cfg.Policy.MinRelayTxFee)
|
minFee := calcMinRequiredTxRelayFee(txSize, mp.cfg.Policy.MinRelayTxFee)
|
||||||
if txFee < conflictsFee+minFee {
|
if txFee < conflictsFee+minFee {
|
||||||
str := fmt.Sprintf("replacement transaction %v has an "+
|
str := fmt.Sprintf("%v: replacement transaction has an "+
|
||||||
"insufficient absolute fee: needs %v, has %v",
|
"insufficient absolute fee: needs %v, has %v",
|
||||||
tx.Hash(), conflictsFee+minFee, txFee)
|
tx.Hash(), conflictsFee+minFee, txFee)
|
||||||
return nil, txRuleError(wire.RejectInsufficientFee, str)
|
return nil, txRuleError(wire.RejectInsufficientFee, str)
|
||||||
|
@ -1350,7 +1350,8 @@ func (mp *TxPool) checkMempoolAcceptance(tx *btcutil.Tx,
|
||||||
if mp.isTransactionInPool(txHash) || (rejectDupOrphans &&
|
if mp.isTransactionInPool(txHash) || (rejectDupOrphans &&
|
||||||
mp.isOrphanInPool(txHash)) {
|
mp.isOrphanInPool(txHash)) {
|
||||||
|
|
||||||
str := fmt.Sprintf("already have transaction %v", txHash)
|
str := fmt.Sprintf("already have transaction in mempool %v",
|
||||||
|
txHash)
|
||||||
return nil, txRuleError(wire.RejectDuplicate, str)
|
return nil, txRuleError(wire.RejectDuplicate, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1368,7 +1369,7 @@ func (mp *TxPool) checkMempoolAcceptance(tx *btcutil.Tx,
|
||||||
|
|
||||||
// A standalone transaction must not be a coinbase transaction.
|
// A standalone transaction must not be a coinbase transaction.
|
||||||
if blockchain.IsCoinBase(tx) {
|
if blockchain.IsCoinBase(tx) {
|
||||||
str := fmt.Sprintf("transaction %v is an individual coinbase",
|
str := fmt.Sprintf("transaction is an individual coinbase %v",
|
||||||
txHash)
|
txHash)
|
||||||
|
|
||||||
return nil, txRuleError(wire.RejectInvalid, str)
|
return nil, txRuleError(wire.RejectInvalid, str)
|
||||||
|
@ -1418,7 +1419,7 @@ func (mp *TxPool) checkMempoolAcceptance(tx *btcutil.Tx,
|
||||||
entry := utxoView.LookupEntry(prevOut)
|
entry := utxoView.LookupEntry(prevOut)
|
||||||
if entry != nil && !entry.IsSpent() {
|
if entry != nil && !entry.IsSpent() {
|
||||||
return nil, txRuleError(wire.RejectDuplicate,
|
return nil, txRuleError(wire.RejectDuplicate,
|
||||||
"transaction already exists")
|
"transaction already exists in blockchain")
|
||||||
}
|
}
|
||||||
|
|
||||||
utxoView.RemoveEntry(prevOut)
|
utxoView.RemoveEntry(prevOut)
|
||||||
|
|
|
@ -1491,7 +1491,7 @@ func TestRBF(t *testing.T) {
|
||||||
|
|
||||||
return tx, nil
|
return tx, nil
|
||||||
},
|
},
|
||||||
err: "already spent by transaction",
|
err: "already spent in mempool",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// A transaction cannot replace another if we don't
|
// A transaction cannot replace another if we don't
|
||||||
|
@ -1522,7 +1522,7 @@ func TestRBF(t *testing.T) {
|
||||||
|
|
||||||
return tx, nil
|
return tx, nil
|
||||||
},
|
},
|
||||||
err: "already spent by transaction",
|
err: "already spent in mempool",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// A transaction cannot replace another if doing so
|
// A transaction cannot replace another if doing so
|
||||||
|
|
|
@ -308,8 +308,8 @@ func CheckTransactionStandard(tx *btcutil.Tx, height int32,
|
||||||
// attacks.
|
// attacks.
|
||||||
txWeight := blockchain.GetTransactionWeight(tx)
|
txWeight := blockchain.GetTransactionWeight(tx)
|
||||||
if txWeight > maxStandardTxWeight {
|
if txWeight > maxStandardTxWeight {
|
||||||
str := fmt.Sprintf("weight of transaction %v is larger than max "+
|
str := fmt.Sprintf("weight of transaction is larger than max "+
|
||||||
"allowed weight of %v", txWeight, maxStandardTxWeight)
|
"allowed: %v > %v", txWeight, maxStandardTxWeight)
|
||||||
return txRuleError(wire.RejectNonstandard, str)
|
return txRuleError(wire.RejectNonstandard, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,8 +320,8 @@ func CheckTransactionStandard(tx *btcutil.Tx, height int32,
|
||||||
sigScriptLen := len(txIn.SignatureScript)
|
sigScriptLen := len(txIn.SignatureScript)
|
||||||
if sigScriptLen > maxStandardSigScriptSize {
|
if sigScriptLen > maxStandardSigScriptSize {
|
||||||
str := fmt.Sprintf("transaction input %d: signature "+
|
str := fmt.Sprintf("transaction input %d: signature "+
|
||||||
"script size of %d bytes is large than max "+
|
"script size is larger than max allowed: "+
|
||||||
"allowed size of %d bytes", i, sigScriptLen,
|
"%d > %d bytes", i, sigScriptLen,
|
||||||
maxStandardSigScriptSize)
|
maxStandardSigScriptSize)
|
||||||
return txRuleError(wire.RejectNonstandard, str)
|
return txRuleError(wire.RejectNonstandard, str)
|
||||||
}
|
}
|
||||||
|
@ -359,8 +359,8 @@ func CheckTransactionStandard(tx *btcutil.Tx, height int32,
|
||||||
if scriptClass == txscript.NullDataTy {
|
if scriptClass == txscript.NullDataTy {
|
||||||
numNullDataOutputs++
|
numNullDataOutputs++
|
||||||
} else if IsDust(txOut, minRelayTxFee) {
|
} else if IsDust(txOut, minRelayTxFee) {
|
||||||
str := fmt.Sprintf("transaction output %d: payment "+
|
str := fmt.Sprintf("transaction output %d: payment is "+
|
||||||
"of %d is dust", i, txOut.Value)
|
"dust: %v", i, txOut.Value)
|
||||||
return txRuleError(wire.RejectDust, str)
|
return txRuleError(wire.RejectDust, str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package rpcclient
|
package rpcclient
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrBitcoindVersion is returned when running against a bitcoind that
|
// ErrBitcoindVersion is returned when running against a bitcoind that
|
||||||
|
@ -10,4 +13,164 @@ var (
|
||||||
// ErrInvalidParam is returned when the caller provides an invalid
|
// ErrInvalidParam is returned when the caller provides an invalid
|
||||||
// parameter to an RPC method.
|
// parameter to an RPC method.
|
||||||
ErrInvalidParam = errors.New("invalid param")
|
ErrInvalidParam = errors.New("invalid param")
|
||||||
|
|
||||||
|
// RejectReasonMap takes the error returned from
|
||||||
|
// `CheckMempoolAcceptance` in `btcd` and maps it to the reject reason
|
||||||
|
// that's returned from calling `testmempoolaccept` in `bitcoind`.
|
||||||
|
// references:
|
||||||
|
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/data/invalid_txs.py
|
||||||
|
// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_accept.py
|
||||||
|
// - https://github.com/bitcoin/bitcoin/blob/master/src/validation.cpp
|
||||||
|
//
|
||||||
|
// Errors not mapped in `btcd`:
|
||||||
|
// - deployment error from `validateSegWitDeployment`.
|
||||||
|
// - the error when total inputs is higher than max allowed value from
|
||||||
|
// `CheckTransactionInputs`.
|
||||||
|
// - the error when total outputs is higher than total inputs from
|
||||||
|
// `CheckTransactionInputs`.
|
||||||
|
// - errors from `CalcSequenceLock`.
|
||||||
|
//
|
||||||
|
// NOTE: This is not an exhaustive list of errors, but it covers the
|
||||||
|
// usage case of LND.
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
|
RejectReasonMap = map[string]string{
|
||||||
|
// BIP125 related errors.
|
||||||
|
//
|
||||||
|
// When fee rate used or fees paid doesn't meet the
|
||||||
|
// requirements.
|
||||||
|
"replacement transaction has an insufficient fee rate": "insufficient fee",
|
||||||
|
"replacement transaction has an insufficient absolute fee": "insufficient fee",
|
||||||
|
|
||||||
|
// When a transaction causes too many transactions being
|
||||||
|
// replaced. This is set by `MAX_REPLACEMENT_CANDIDATES` in
|
||||||
|
// `bitcoind` and defaults to 100.
|
||||||
|
"replacement transaction evicts more transactions than permitted": "too many potential replacements",
|
||||||
|
|
||||||
|
// When a transaction adds new unconfirmed inputs.
|
||||||
|
"replacement transaction spends new unconfirmed input": "replacement-adds-unconfirmed",
|
||||||
|
|
||||||
|
// A transaction that spends conflicting tx outputs that are
|
||||||
|
// rejected.
|
||||||
|
"replacement transaction spends parent transaction": "bad-txns-spends-conflicting-tx",
|
||||||
|
|
||||||
|
// A transaction that conflicts with an unconfirmed tx. Happens
|
||||||
|
// when RBF is not enabled.
|
||||||
|
"output already spent in mempool": "txn-mempool-conflict",
|
||||||
|
|
||||||
|
// A transaction with no outputs.
|
||||||
|
"transaction has no outputs": "bad-txns-vout-empty",
|
||||||
|
|
||||||
|
// A transaction with no inputs.
|
||||||
|
"transaction has no inputs": "bad-txns-vin-empty",
|
||||||
|
|
||||||
|
// A tiny transaction(in non-witness bytes) that is disallowed.
|
||||||
|
// TODO(yy): find/return this error in `btcd`.
|
||||||
|
// "": "tx-size-small",
|
||||||
|
|
||||||
|
// A transaction with duplicate inputs.
|
||||||
|
"transaction contains duplicate inputs": "bad-txns-inputs-duplicate",
|
||||||
|
|
||||||
|
// A non-coinbase transaction with coinbase-like outpoint.
|
||||||
|
"transaction input refers to previous output that is null": "bad-txns-prevout-null",
|
||||||
|
|
||||||
|
// A transaction pays too little fee.
|
||||||
|
"fees which is under the required amount": "bad-txns-in-belowout",
|
||||||
|
"has insufficient priority": "bad-txns-in-belowout",
|
||||||
|
"has been rejected by the rate limiter due to low fees": "bad-txns-in-belowout",
|
||||||
|
|
||||||
|
// A transaction with negative output value.
|
||||||
|
"transaction output has negative value": "bad-txns-vout-negative",
|
||||||
|
|
||||||
|
// A transaction with too large output value.
|
||||||
|
"transaction output value is higher than max allowed value": "bad-txns-vout-toolarge",
|
||||||
|
|
||||||
|
// A transaction with too large sum of output values.
|
||||||
|
"total value of all transaction outputs exceeds max allowed value": "bad-txns-txouttotal-toolarge",
|
||||||
|
|
||||||
|
// TODO(yy): find/return this error in `btcd`.
|
||||||
|
// "": "mandatory-script-verify-flag-failed (Invalid OP_IF construction)",
|
||||||
|
|
||||||
|
// A transaction with too many sigops.
|
||||||
|
"sigop cost is too hight": "bad-txns-too-many-sigops",
|
||||||
|
|
||||||
|
// A transaction with invalid OP codes.
|
||||||
|
// TODO(yy): find/return this error in `btcd`.
|
||||||
|
// "": "disabled opcode",
|
||||||
|
|
||||||
|
// A transaction already in the blockchain.
|
||||||
|
"database contains entry for spent tx output": "txn-already-known",
|
||||||
|
"transaction already exists in blockchain": "txn-already-known",
|
||||||
|
|
||||||
|
// A transaction in the mempool.
|
||||||
|
"already have transaction in mempool": "txn-already-in-mempool",
|
||||||
|
|
||||||
|
// A transaction with missing inputs, that never existed or
|
||||||
|
// only existed once in the past.
|
||||||
|
"either does not exist or has already been spent": "missing-inputs",
|
||||||
|
|
||||||
|
// A really large transaction.
|
||||||
|
"serialized transaction is too big": "bad-txns-oversize",
|
||||||
|
|
||||||
|
// A coinbase transaction.
|
||||||
|
"transaction is an invalid coinbase": "coinbase",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - a version currently
|
||||||
|
// non-standard.
|
||||||
|
"transaction version": "version",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - non-standard script.
|
||||||
|
"non-standard script form": "scriptpubkey",
|
||||||
|
"has a non-standard input": "scriptpubkey",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - bare multisig script
|
||||||
|
// (2-of-3).
|
||||||
|
"milti-signature script": "bare-multisig",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - not-pushonly scriptSig.
|
||||||
|
"signature script is not push only": "scriptsig-not-pushonly",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - too large scriptSig (>1650
|
||||||
|
// bytes).
|
||||||
|
"signature script size is larger than max allowed": "scriptsig-size",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - too large tx size.
|
||||||
|
"weight of transaction is larger than max allowed": "tx-size",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - output too small.
|
||||||
|
"payment is dust": "dust",
|
||||||
|
|
||||||
|
// Some nonstandard transactions - muiltiple OP_RETURNs.
|
||||||
|
"more than one transaction output in a nulldata script": "multi-op-return",
|
||||||
|
|
||||||
|
// A timelocked transaction.
|
||||||
|
"transaction is not finalized": "non-final",
|
||||||
|
"tried to spend coinbase transaction output": "non-final",
|
||||||
|
|
||||||
|
// A transaction that is locked by BIP68 sequence logic.
|
||||||
|
"transaction's sequence locks on inputs not met": "non-BIP68-final",
|
||||||
|
|
||||||
|
// Minimally-small transaction(in non-witness bytes) that is
|
||||||
|
// allowed.
|
||||||
|
// TODO(yy): find/return this error in `btcd`.
|
||||||
|
// "": "txn-same-nonwitness-data-in-mempools",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MapBtcdErrToRejectReason takes an error returned from
|
||||||
|
// `CheckMempoolAcceptance` and maps the error to a bitcoind reject reason.
|
||||||
|
func MapBtcdErrToRejectReason(err error) string {
|
||||||
|
// Get the error string and turn it into lower case.
|
||||||
|
btcErr := strings.ToLower(err.Error())
|
||||||
|
|
||||||
|
// Iterate the map and find the matching error.
|
||||||
|
for keyErr, rejectReason := range RejectReasonMap {
|
||||||
|
// Match the substring.
|
||||||
|
if strings.Contains(btcErr, keyErr) {
|
||||||
|
return rejectReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no match, return the error string directly.
|
||||||
|
return btcErr
|
||||||
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import (
|
||||||
"github.com/btcsuite/btcd/mining"
|
"github.com/btcsuite/btcd/mining"
|
||||||
"github.com/btcsuite/btcd/mining/cpuminer"
|
"github.com/btcsuite/btcd/mining/cpuminer"
|
||||||
"github.com/btcsuite/btcd/peer"
|
"github.com/btcsuite/btcd/peer"
|
||||||
|
"github.com/btcsuite/btcd/rpcclient"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/websocket"
|
"github.com/btcsuite/websocket"
|
||||||
|
@ -3856,7 +3857,9 @@ func handleTestMempoolAccept(s *rpcServer, cmd interface{},
|
||||||
|
|
||||||
// TODO(yy): differentiate the errors and put package
|
// TODO(yy): differentiate the errors and put package
|
||||||
// error in `PackageError` field.
|
// error in `PackageError` field.
|
||||||
item.RejectReason = err.Error()
|
item.RejectReason = rpcclient.MapBtcdErrToRejectReason(
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
results = append(results, item)
|
results = append(results, item)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue