From cb31db36088becad8f795325651aa0890e74b151 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 3 Jul 2024 22:25:02 +0800 Subject: [PATCH 1/3] chain: add errors for `sendrawtransaction` and `testmempoolaccept` This commit moves the defined errors from [btcd/rpcclient/errors.go](https://github.com/btcsuite/btcd/blob/d881c686e61db35e332fb0309178152dac589b03/rpcclient/errors.go) to here so it's easier to manage error matching for different backends. --- chain/errors.go | 495 +++++++++++++++++++++++++++++++++++++++++++ chain/errors_test.go | 87 ++++++++ 2 files changed, 582 insertions(+) create mode 100644 chain/errors.go create mode 100644 chain/errors_test.go diff --git a/chain/errors.go b/chain/errors.go new file mode 100644 index 0000000000..b71241d681 --- /dev/null +++ b/chain/errors.go @@ -0,0 +1,495 @@ +package chain + +import ( + "errors" + "strings" +) + +var ( + // ErrBackendVersion is returned when running against a bitcoind or + // btcd that is older than the minimum version supported by the + // rpcclient. + ErrBackendVersion = errors.New("backend version too low") + + // ErrInvalidParam is returned when the caller provides an invalid + // parameter to an RPC method. + ErrInvalidParam = errors.New("invalid param") + + // ErrUndefined is used when an error returned is not recognized. We + // should gradually increase our error types to avoid returning this + // error. + ErrUndefined = errors.New("undefined") +) + +// RPCErr represents an error returned by bitcoind's RPC server. +type RPCErr uint32 + +// This section defines all possible errors or reject reasons returned from +// bitcoind's `sendrawtransaction` or `testmempoolaccept` RPC. +const ( + // ErrMissingInputsOrSpent is returned when calling + // `sendrawtransaction` with missing inputs. + ErrMissingInputsOrSpent RPCErr = iota + + // ErrMaxBurnExceeded is returned when calling `sendrawtransaction` + // with exceeding, falling short of, and equaling maxburnamount. + ErrMaxBurnExceeded + + // ErrMaxFeeExceeded can happen when passing a signed tx to + // `testmempoolaccept`, but the tx pays more fees than specified. + ErrMaxFeeExceeded + + // ErrTxAlreadyKnown is used in the `reject-reason` field of + // `testmempoolaccept` when a transaction is already in the blockchain. + ErrTxAlreadyKnown + + // ErrTxAlreadyConfirmed is returned as an error from + // `sendrawtransaction` when a transaction is already in the + // blockchain. + ErrTxAlreadyConfirmed + + // ErrMempoolConflict happens when RBF is not enabled yet the + // transaction conflicts with an unconfirmed tx. . + // + // NOTE: RBF rule 1. + ErrMempoolConflict + + // ErrReplacementAddsUnconfirmed is returned when a transaction adds + // new unconfirmed inputs. + // + // NOTE: RBF rule 2. + ErrReplacementAddsUnconfirmed + + // ErrInsufficientFee is returned when fee rate used or fees paid + // doesn't meet the requirements. + // + // NOTE: RBF rule 3 or 4. + ErrInsufficientFee + + // ErrTooManyReplacements is returned when a transaction causes too + // many transactions being replaced. This is set by + // `MAX_REPLACEMENT_CANDIDATES` in `bitcoind` and defaults to 100. + // + // NOTE: RBF rule 5. + ErrTooManyReplacements + + // ErrMempoolMinFeeNotMet is returned when the transaction doesn't meet + // the minimum relay fee. + ErrMempoolMinFeeNotMet + + // ErrConflictingTx is returned when a transaction that spends + // conflicting tx outputs that are rejected. + ErrConflictingTx + + // ErrEmptyOutput is returned when a transaction has no outputs. + ErrEmptyOutput + + // ErrEmptyInput is returned when a transaction has no inputs. + ErrEmptyInput + + // ErrTxTooSmall is returned when spending a tiny transaction(in + // non-witness bytes) that is disallowed. + // + // NOTE: ErrTxTooLarge must be put after ErrTxTooSmall because it's a + // subset of ErrTxTooSmall. Otherwise, if bitcoind returns + // `tx-size-small`, it will be matched to ErrTxTooLarge. + ErrTxTooSmall + + // ErrDuplicateInput is returned when a transaction has duplicate + // inputs. + ErrDuplicateInput + + // ErrEmptyPrevOut is returned when a non-coinbase transaction has + // coinbase-like outpoint. + ErrEmptyPrevOut + + // ErrBelowOutValue is returned when a transaction's output value is + // greater than its input value. + ErrBelowOutValue + + // ErrNegativeOutput is returned when a transaction has negative output + // value. + ErrNegativeOutput + + // ErrLargeOutput is returned when a transaction has too large output + // value. + ErrLargeOutput + + // ErrLargeTotalOutput is returned when a transaction has too large sum + // of output values. + ErrLargeTotalOutput + + // ErrScriptVerifyFlag is returned when there is invalid OP_IF + // construction. + ErrScriptVerifyFlag + + // ErrTooManySigOps is returned when a transaction has too many sigops. + ErrTooManySigOps + + // ErrInvalidOpcode is returned when a transaction has invalid OP + // codes. + ErrInvalidOpcode + + // ErrTxAlreadyInMempool is returned when a transaction is in the + // mempool. + ErrTxAlreadyInMempool + + // ErrMissingInputs is returned when a transaction has missing inputs, + // that never existed or only existed once in the past. + ErrMissingInputs + + // ErrOversizeTx is returned when a transaction is too large. + ErrOversizeTx + + // ErrCoinbaseTx is returned when the transaction is coinbase tx. + ErrCoinbaseTx + + // ErrNonStandardVersion is returned when the transactions are not + // standard - a version currently non-standard. + ErrNonStandardVersion + + // ErrNonStandardScript is returned when the transactions are not + // standard - non-standard script. + ErrNonStandardScript + + // ErrBareMultiSig is returned when the transactions are not standard - + // bare multisig script (2-of-3). + ErrBareMultiSig + + // ErrScriptSigNotPushOnly is returned when the transactions are not + // standard - not-pushonly scriptSig. + ErrScriptSigNotPushOnly + + // ErrScriptSigSize is returned when the transactions are not standard + // - too large scriptSig (>1650 bytes). + ErrScriptSigSize + + // ErrTxTooLarge is returned when the transactions are not standard - + // too large tx size. + ErrTxTooLarge + + // ErrDust is returned when the transactions are not standard - output + // too small. + ErrDust + + // ErrMultiOpReturn is returned when the transactions are not standard + // - muiltiple OP_RETURNs. + ErrMultiOpReturn + + // ErrNonFinal is returned when spending a timelocked transaction that + // hasn't expired yet. + ErrNonFinal + + // ErrNonBIP68Final is returned when a transaction that is locked by + // BIP68 sequence logic and not expired yet. + ErrNonBIP68Final + + // ErrSameNonWitnessData is returned when another tx with the same + // non-witness data is already in the mempool. For instance, these two + // txns share the same `txid` but different `wtxid`. + ErrSameNonWitnessData + + // ErrNonMandatoryScriptVerifyFlag is returned when passing a raw tx to + // `testmempoolaccept`, which gives the error followed by (Witness + // program hash mismatch). + ErrNonMandatoryScriptVerifyFlag + + // errSentinel is used to indicate the end of the error list. This + // should always be the last error code. + errSentinel +) + +// Error implements the error interface. It returns the error message defined +// in `bitcoind`. + +// Some of the dashes used in the original error string is removed, e.g. +// "missing-inputs" is now "missing inputs". This is ok since we will normalize +// the errors before matching. +// +// references: +// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/rpc_rawtransaction.py#L342 +// - 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/test/functional/mempool_accept_wtxid.py +// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_dust.py +// - https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_limit.py +// - https://github.com/bitcoin/bitcoin/blob/master/src/validation.cpp +func (r RPCErr) Error() string { + switch r { + case ErrMissingInputsOrSpent: + return "bad txns inputs missingorspent" + + case ErrMaxBurnExceeded: + return "Unspendable output exceeds maximum configured by user (maxburnamount)" + + case ErrMaxFeeExceeded: + return "max fee exceeded" + + case ErrTxAlreadyKnown: + return "txn already known" + + case ErrTxAlreadyConfirmed: + return "Transaction already in block chain" + + case ErrMempoolConflict: + return "txn mempool conflict" + + case ErrReplacementAddsUnconfirmed: + return "replacement adds unconfirmed" + + case ErrInsufficientFee: + return "insufficient fee" + + case ErrTooManyReplacements: + return "too many potential replacements" + + case ErrMempoolMinFeeNotMet: + return "mempool min fee not met" + + case ErrConflictingTx: + return "bad txns spends conflicting tx" + + case ErrEmptyOutput: + return "bad txns vout empty" + + case ErrEmptyInput: + return "bad txns vin empty" + + case ErrTxTooSmall: + return "tx size small" + + case ErrDuplicateInput: + return "bad txns inputs duplicate" + + case ErrEmptyPrevOut: + return "bad txns prevout null" + + case ErrBelowOutValue: + return "bad txns in belowout" + + case ErrNegativeOutput: + return "bad txns vout negative" + + case ErrLargeOutput: + return "bad txns vout toolarge" + + case ErrLargeTotalOutput: + return "bad txns txouttotal toolarge" + + case ErrScriptVerifyFlag: + return "mandatory script verify flag failed" + + case ErrTooManySigOps: + return "bad txns too many sigops" + + case ErrInvalidOpcode: + return "disabled opcode" + + case ErrTxAlreadyInMempool: + return "txn already in mempool" + + case ErrMissingInputs: + return "missing inputs" + + case ErrOversizeTx: + return "bad txns oversize" + + case ErrCoinbaseTx: + return "coinbase" + + case ErrNonStandardVersion: + return "version" + + case ErrNonStandardScript: + return "scriptpubkey" + + case ErrBareMultiSig: + return "bare multisig" + + case ErrScriptSigNotPushOnly: + return "scriptsig not pushonly" + + case ErrScriptSigSize: + return "scriptsig size" + + case ErrTxTooLarge: + return "tx size" + + case ErrDust: + return "dust" + + case ErrMultiOpReturn: + return "multi op return" + + case ErrNonFinal: + return "non final" + + case ErrNonBIP68Final: + return "non BIP68 final" + + case ErrSameNonWitnessData: + return "txn same nonwitness data in mempool" + + case ErrNonMandatoryScriptVerifyFlag: + return "non mandatory script verify flag" + } + + return "unknown error" +} + +// BtcdErrMap takes the errors returned from btcd's `testmempoolaccept` and +// `sendrawtransaction` RPCs and map them to the errors defined above, which +// are results from calling either `testmempoolaccept` or `sendrawtransaction` +// in `bitcoind`. +// +// 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 +var BtcdErrMap = map[string]error{ + // BIP125 related errors. + // + // When fee rate used or fees paid doesn't meet the requirements. + "replacement transaction has an insufficient fee rate": ErrInsufficientFee, + "replacement transaction has an insufficient absolute fee": ErrInsufficientFee, + + // 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": ErrTooManyReplacements, + + // When a transaction adds new unconfirmed inputs. + "replacement transaction spends new unconfirmed input": ErrReplacementAddsUnconfirmed, + + // A transaction that spends conflicting tx outputs that are rejected. + "replacement transaction spends parent transaction": ErrConflictingTx, + + // A transaction that conflicts with an unconfirmed tx. Happens when + // RBF is not enabled. + "output already spent in mempool": ErrMempoolConflict, + + // A transaction with no outputs. + "transaction has no outputs": ErrEmptyOutput, + + // A transaction with no inputs. + "transaction has no inputs": ErrEmptyInput, + + // A transaction with duplicate inputs. + "transaction contains duplicate inputs": ErrDuplicateInput, + + // A non-coinbase transaction with coinbase-like outpoint. + "transaction input refers to previous output that is null": ErrEmptyPrevOut, + + // A transaction pays too little fee. + "fees which is under the required amount": ErrMempoolMinFeeNotMet, + "has insufficient priority": ErrInsufficientFee, + "has been rejected by the rate limiter due to low fees": ErrInsufficientFee, + + // A transaction with negative output value. + "transaction output has negative value": ErrNegativeOutput, + + // A transaction with too large output value. + "transaction output value is higher than max allowed value": ErrLargeOutput, + + // A transaction with too large sum of output values. + "total value of all transaction outputs exceeds max allowed value": ErrLargeTotalOutput, + + // A transaction with too many sigops. + "sigop cost is too hight": ErrTooManySigOps, + + // A transaction already in the blockchain. + "database contains entry for spent tx output": ErrTxAlreadyKnown, + "transaction already exists in blockchain": ErrTxAlreadyConfirmed, + + // A transaction in the mempool. + "already have transaction in mempool": ErrTxAlreadyInMempool, + + // A transaction with missing inputs, that never existed or only + // existed once in the past. + "either does not exist or has already been spent": ErrMissingInputs, + "orphan transaction": ErrMissingInputs, + + // A really large transaction. + "serialized transaction is too big": ErrOversizeTx, + + // A coinbase transaction. + "transaction is an invalid coinbase": ErrCoinbaseTx, + + // Some nonstandard transactions - a version currently non-standard. + "transaction version": ErrNonStandardVersion, + + // Some nonstandard transactions - non-standard script. + "non-standard script form": ErrNonStandardScript, + "has a non-standard input": ErrNonStandardScript, + + // Some nonstandard transactions - bare multisig script + // (2-of-3). + "milti-signature script": ErrBareMultiSig, + + // Some nonstandard transactions - not-pushonly scriptSig. + "signature script is not push only": ErrScriptSigNotPushOnly, + + // Some nonstandard transactions - too large scriptSig (>1650 + // bytes). + "signature script size is larger than max allowed": ErrScriptSigSize, + + // Some nonstandard transactions - too large tx size. + "weight of transaction is larger than max allowed": ErrTxTooLarge, + + // Some nonstandard transactions - output too small. + "payment is dust": ErrDust, + + // Some nonstandard transactions - muiltiple OP_RETURNs. + "more than one transaction output in a nulldata script": ErrMultiOpReturn, + + // A timelocked transaction. + "transaction is not finalized": ErrNonFinal, + "tried to spend coinbase transaction output": ErrNonFinal, + + // A transaction that is locked by BIP68 sequence logic. + "transaction's sequence locks on inputs not met": ErrNonBIP68Final, + + // TODO(yy): find/return the following errors in `btcd`. + // + // A tiny transaction(in non-witness bytes) that is disallowed. + // "unmatched btcd error 1": ErrTxTooSmall, + // "unmatched btcd error 2": ErrScriptVerifyFlag, + // // A transaction with invalid OP codes. + // "unmatched btcd error 3": ErrInvalidOpcode, + // // Minimally-small transaction(in non-witness bytes) that is + // // allowed. + // "unmatched btcd error 4": ErrSameNonWitnessData, + + // Returned from `testmempoolaccept` here: + // - https://github.com/btcsuite/btcd/blob/d881c686e61db35e332fb0309178152dac589b03/rpcserver.go#L3893 + "missing-inputs": ErrMissingInputs, + + // Returned from `testmempoolaccept` here: + // - https://github.com/btcsuite/btcd/blob/d881c686e61db35e332fb0309178152dac589b03/rpcserver.go#L3917 + "max-fee-exceeded": ErrMaxFeeExceeded, +} + +// matchErrStr takes an error returned from RPC client and matches it against +// the specified string. If the expected string pattern is found in the error +// passed, return true. Both the error strings are normalized before matching. +func matchErrStr(err error, s string) bool { + // Replace all dashes found in the error string with spaces. + strippedErrStr := strings.ReplaceAll(err.Error(), "-", " ") + + // Replace all dashes found in the error string with spaces. + strippedMatchStr := strings.ReplaceAll(s, "-", " ") + + // Match against the lowercase. + return strings.Contains( + strings.ToLower(strippedErrStr), + strings.ToLower(strippedMatchStr), + ) +} diff --git a/chain/errors_test.go b/chain/errors_test.go new file mode 100644 index 0000000000..d34e63426f --- /dev/null +++ b/chain/errors_test.go @@ -0,0 +1,87 @@ +package chain + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestMatchErrStr checks that `matchErrStr` can correctly replace the dashes +// with spaces and turn title cases into lowercases for a given error and match +// it against the specified string pattern. +func TestMatchErrStr(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + bitcoindErr error + matchStr string + matched bool + }{ + { + name: "error without dashes", + bitcoindErr: errors.New("missing input"), + matchStr: "missing input", + matched: true, + }, + { + name: "match str without dashes", + bitcoindErr: errors.New("missing-input"), + matchStr: "missing input", + matched: true, + }, + { + name: "error with dashes", + bitcoindErr: errors.New("missing-input"), + matchStr: "missing input", + matched: true, + }, + { + name: "match str with dashes", + bitcoindErr: errors.New("missing-input"), + matchStr: "missing-input", + matched: true, + }, + { + name: "error with title case and dash", + bitcoindErr: errors.New("Missing-Input"), + matchStr: "missing input", + matched: true, + }, + { + name: "match str with title case and dash", + bitcoindErr: errors.New("missing-input"), + matchStr: "Missing-Input", + matched: true, + }, + { + name: "unmatched error", + bitcoindErr: errors.New("missing input"), + matchStr: "missingorspent", + matched: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + matched := matchErrStr(tc.bitcoindErr, tc.matchStr) + require.Equal(t, tc.matched, matched) + }) + } +} + +// TestErrorSentinel checks that all defined RPCErr errors are added to the +// method `Error`. +func TestBitcoindErrorSentinel(t *testing.T) { + t.Parallel() + + rt := require.New(t) + + for i := uint32(0); i < uint32(errSentinel); i++ { + err := RPCErr(i) + rt.NotEqualf(err.Error(), "unknown error", "error code %d is "+ + "not defined, make sure to update it inside the Error "+ + "method", i) + } +} From f4133be0bff0ada32bc2b30fdba9b9e77b92e9c7 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 3 Jul 2024 22:27:52 +0800 Subject: [PATCH 2/3] chain+wallet: define and use `MapRPCErr` for chain backends This commit adds a new interface method `MapRPCErr` and use it in `TestMempoolAccept` and `SendRawTransaction`. --- chain/bitcoind_client.go | 22 +++++++++++++++++++++- chain/btcd.go | 30 ++++++++++++++++++++++++++++++ chain/interface.go | 1 + chain/neutrino.go | 21 ++++++++++++++++++++- wallet/mock.go | 4 ++++ wallet/wallet.go | 18 +++++------------- 6 files changed, 81 insertions(+), 15 deletions(-) diff --git a/chain/bitcoind_client.go b/chain/bitcoind_client.go index 4997877565..d8326acd1d 100644 --- a/chain/bitcoind_client.go +++ b/chain/bitcoind_client.go @@ -214,7 +214,27 @@ func (c *BitcoindClient) GetTxOut(txHash *chainhash.Hash, index uint32, func (c *BitcoindClient) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { - return c.chainConn.client.SendRawTransaction(tx, allowHighFees) + txid, err := c.chainConn.client.SendRawTransaction(tx, allowHighFees) + if err != nil { + return nil, c.MapRPCErr(err) + } + + return txid, nil +} + +// MapRPCErr takes an error returned from calling RPC methods from various +// chain backends and maps it to an defined error here. +func (c *BitcoindClient) MapRPCErr(rpcErr error) error { + // Try to match it against bitcoind's error. + for i := uint32(0); i < uint32(errSentinel); i++ { + err := RPCErr(i) + if matchErrStr(rpcErr, err.Error()) { + return err + } + } + + // If not matched, return the original error wrapped. + return fmt.Errorf("%w: %v", ErrUndefined, rpcErr) } // TestMempoolAcceptCmd returns result of mempool acceptance tests indicating diff --git a/chain/btcd.go b/chain/btcd.go index 85d8013a9f..f9cb78981f 100644 --- a/chain/btcd.go +++ b/chain/btcd.go @@ -6,6 +6,7 @@ package chain import ( "errors" + "fmt" "sync" "time" @@ -578,3 +579,32 @@ func (c *RPCClient) LookupInputMempoolSpend(op wire.OutPoint) ( return getTxSpendingPrevOut(op, c.Client) } + +// MapRPCErr takes an error returned from calling RPC methods from various +// chain backends and maps it to an defined error here. It uses the +// `BtcdErrMap`, whose keys are btcd error strings and values are errors made +// from bitcoind error strings. +func (c *RPCClient) MapRPCErr(rpcErr error) error { + // Iterate the map and find the matching error. + for btcdErr, matchedErr := range BtcdErrMap { + // Match it against btcd's error. + if matchErrStr(rpcErr, btcdErr) { + return matchedErr + } + } + + // If not matched, return the original error wrapped. + return fmt.Errorf("%w: %v", ErrUndefined, rpcErr) +} + +// SendRawTransaction sends a raw transaction via btcd. +func (c *RPCClient) SendRawTransaction(tx *wire.MsgTx, + allowHighFees bool) (*chainhash.Hash, error) { + + txid, err := c.Client.SendRawTransaction(tx, allowHighFees) + if err != nil { + return nil, c.MapRPCErr(err) + } + + return txid, nil +} diff --git a/chain/interface.go b/chain/interface.go index 5691e792ee..9ca341dbaf 100644 --- a/chain/interface.go +++ b/chain/interface.go @@ -49,6 +49,7 @@ type Interface interface { Notifications() <-chan interface{} BackEnd() string TestMempoolAccept([]*wire.MsgTx, float64) ([]*btcjson.TestMempoolAcceptResult, error) + MapRPCErr(err error) error } // Notification types. These are defined here and processed from from reading diff --git a/chain/neutrino.go b/chain/neutrino.go index 61e4d5f428..56bd06bc91 100644 --- a/chain/neutrino.go +++ b/chain/neutrino.go @@ -220,7 +220,7 @@ func (s *NeutrinoClient) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) *chainhash.Hash, error) { err := s.CS.SendTransaction(tx) if err != nil { - return nil, rpcclient.MapRPCErr(err) + return nil, s.MapRPCErr(err) } hash := tx.TxHash() return &hash, nil @@ -815,3 +815,22 @@ out: close(s.dequeueNotification) s.wg.Done() } + +// MapRPCErr takes an error returned from calling RPC methods from various +// chain backends and maps it to an defined error here. It uses the +// `BtcdErrMap`, whose keys are btcd error strings and values are errors made +// from bitcoind error strings. +// +// NOTE: we assume neutrino shares the same error strings as btcd. +func (s *NeutrinoClient) MapRPCErr(rpcErr error) error { + // Iterate the map and find the matching error. + for btcdErr, matchedErr := range BtcdErrMap { + // Match it against btcd's error. + if matchErrStr(rpcErr, btcdErr) { + return matchedErr + } + } + + // If not matched, return the original error wrapped. + return fmt.Errorf("%w: %v", ErrUndefined, rpcErr) +} diff --git a/wallet/mock.go b/wallet/mock.go index 8c995ebb57..43c3d881bb 100644 --- a/wallet/mock.go +++ b/wallet/mock.go @@ -94,3 +94,7 @@ func (m *mockChainClient) TestMempoolAccept(txns []*wire.MsgTx, return nil, nil } + +func (m *mockChainClient) MapRPCErr(err error) error { + return nil +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 362ca9b787..fb7f10419b 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -22,7 +22,6 @@ import ( "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/chain" @@ -3793,25 +3792,18 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { } txid := tx.TxHash() - _, err = chainClient.SendRawTransaction(tx, false) - if err == nil { + _, rpcErr := chainClient.SendRawTransaction(tx, false) + if rpcErr == nil { return &txid, nil } - // Map the error to an RPC-specific error type. - // - // NOTE: all the errors returned here are mapped to an error type - // defined in `rpcclient` package, where the error strings are taken - // from bitcoind. - rpcErr := rpcclient.MapRPCErr(err) - switch { - case errors.Is(rpcErr, rpcclient.ErrTxAlreadyInMempool): + case errors.Is(rpcErr, chain.ErrTxAlreadyInMempool): log.Infof("%v: tx already in mempool", txid) return &txid, nil - case errors.Is(rpcErr, rpcclient.ErrTxAlreadyKnown), - errors.Is(rpcErr, rpcclient.ErrTxAlreadyConfirmed): + case errors.Is(rpcErr, chain.ErrTxAlreadyKnown), + errors.Is(rpcErr, chain.ErrTxAlreadyConfirmed): dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) From a90a3ece75a9e8c5210770ced569237d78eb3860 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 3 Jul 2024 23:01:29 +0800 Subject: [PATCH 3/3] chain: match errors from `btcd pre-v0.24.2` --- chain/btcd.go | 30 ++++++++++++++++++++++++++++++ chain/errors.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ chain/neutrino.go | 10 ++++++++++ 3 files changed, 86 insertions(+) diff --git a/chain/btcd.go b/chain/btcd.go index f9cb78981f..7db148c4a4 100644 --- a/chain/btcd.go +++ b/chain/btcd.go @@ -593,6 +593,36 @@ func (c *RPCClient) MapRPCErr(rpcErr error) error { } } + // If no matching error is found, we try to match it to an older + // version of `btcd`. + // + // Get the backend's version. + backend, bErr := c.BackendVersion() + if bErr != nil { + // If there's an error getting the backend version, we return + // the original error and the backend error. + return fmt.Errorf("%w: %v, failed to get backend version %v", + ErrUndefined, rpcErr, bErr) + } + + // If this version doesn't support `testmempoolaccept`, it must be + // below v0.24.2. In this case, we will match the errors defined in + // pre-v0.24.2. + // + // NOTE: `testmempoolaccept` is implemented in v0.24.1, but this + // version was never tagged, which means it must be v0.24.2 when it's + // supported. + if !backend.SupportTestMempoolAccept() { + // If the backend is older than v0.24.2, we will try to match + // the error to the older version of `btcd`. + for btcdErr, matchedErr := range BtcdErrMapPre2402 { + // Match it against btcd's error. + if matchErrStr(rpcErr, btcdErr) { + return matchedErr + } + } + } + // If not matched, return the original error wrapped. return fmt.Errorf("%w: %v", ErrUndefined, rpcErr) } diff --git a/chain/errors.go b/chain/errors.go index b71241d681..6302ad6ccf 100644 --- a/chain/errors.go +++ b/chain/errors.go @@ -477,6 +477,52 @@ var BtcdErrMap = map[string]error{ "max-fee-exceeded": ErrMaxFeeExceeded, } +// BtcdErrMapPre2402 defines the error mapping for btcd versions prior to +// 0.24.2 - all the errors changed in this commit have been defined here to +// support older versions: +// - https://github.com/btcsuite/btcd/pull/2053/commits/ef54c49df443815d50765e8c4f31a87944d950a6 +var BtcdErrMapPre2402 = map[string]error{ + // A transaction with too large output value. + "is higher than max allowed value": ErrLargeOutput, + + // A transaction that conflicts with an unconfirmed tx. Happens when + // RBF is not enabled. + "already spent by transaction": ErrMempoolConflict, + + // When a transaction causes too many transactions being replaced. This + // is set by `MAX_REPLACEMENT_CANDIDATES` in `bitcoind` and defaults to + // 100. + "evicts more transactions than permitted": ErrTooManyReplacements, + + // A transaction that spends conflicting tx outputs that are rejected. + "spends parent transaction": ErrConflictingTx, + + // BIP125 related errors. + // + // When fee rate used or fees paid doesn't meet the requirements. + "has an insufficient fee rate": ErrInsufficientFee, + "has an insufficient absolute fee": ErrInsufficientFee, + + // A transaction in the mempool. + "already have transaction": ErrTxAlreadyInMempool, + + // A coinbase transaction. + "is an individual coinbase": ErrCoinbaseTx, + + // A transaction already in the blockchain. + "transaction already exists": ErrTxAlreadyConfirmed, + + // Some nonstandard transactions - too large tx size. + "is larger than max allowed weight of": ErrTxTooLarge, + + // Some nonstandard transactions - too large scriptSig (>1650 + // bytes). + "bytes is larger than max allowed size of": ErrScriptSigSize, + + // Some nonstandard transactions - output too small. + "is dust": ErrDust, +} + // matchErrStr takes an error returned from RPC client and matches it against // the specified string. If the expected string pattern is found in the error // passed, return true. Both the error strings are normalized before matching. diff --git a/chain/neutrino.go b/chain/neutrino.go index 56bd06bc91..bdeb19cecd 100644 --- a/chain/neutrino.go +++ b/chain/neutrino.go @@ -831,6 +831,16 @@ func (s *NeutrinoClient) MapRPCErr(rpcErr error) error { } } + // Neutrino doesn't support version check, we will try to match the + // errors from the older version of `btcd`, which are also used by + // neutrino. + for btcdErr, matchedErr := range BtcdErrMapPre2402 { + // Match it against btcd's error. + if matchErrStr(rpcErr, btcdErr) { + return matchedErr + } + } + // If not matched, return the original error wrapped. return fmt.Errorf("%w: %v", ErrUndefined, rpcErr) }