mempool: Add ErrorCode to returned TxRuleErrors

This adds the ErrorCode member to TxRuleError, filling it with
appropriate values throughout the mempool package. This allows clients
of the package to correctly identify error causes with a greater
granularity and respond appropriately.

It also deprecates the RejectCode attribute and ErrToRejectError
functions, to be removed in the next major version update of the
package.

All call sites that inspect mempool errors were updated to use the new
error codes instead of using RejectionCodes. Additional mempool tests
were added to ensure the correct behavior on some relevant cases.

Finally, given the introduction and use of a new public field, the main
module was updated to use an as-of-yet unfinished mempool v3.1.0, which
will include the required functionality.
This commit is contained in:
Matheus Degiovani 2019-09-18 10:21:16 -03:00 committed by Dave Collins
parent 13ee7e50f1
commit 450a680097
7 changed files with 363 additions and 88 deletions

View File

@ -595,6 +595,83 @@ func (b *blockManager) handleDonePeerMsg(peers *list.List, sp *serverPeer) {
}
}
// errToWireRejectCode determines the wire rejection code and description for a
// given error. This function can convert some select blockchain and mempool
// error types to the historical rejection codes used on the p2p wire protocol.
func errToWireRejectCode(err error) (wire.RejectCode, string) {
// Unwrap mempool errors.
if rerr, ok := err.(mempool.RuleError); ok {
err = rerr.Err
}
// The default reason to reject a transaction/block is due to it being
// invalid somehow.
code := wire.RejectInvalid
var reason string
switch err := err.(type) {
case blockchain.RuleError:
// Convert the chain error to a reject code.
switch err.ErrorCode {
// Rejected due to duplicate.
case blockchain.ErrDuplicateBlock:
code = wire.RejectDuplicate
// Rejected due to obsolete version.
case blockchain.ErrBlockVersionTooOld:
code = wire.RejectObsolete
// Rejected due to checkpoint.
case blockchain.ErrCheckpointTimeTooOld,
blockchain.ErrDifficultyTooLow,
blockchain.ErrBadCheckpoint,
blockchain.ErrForkTooOld:
code = wire.RejectCheckpoint
}
reason = err.Error()
case mempool.TxRuleError:
switch err.ErrorCode {
// Error codes which map to a duplicate transaction already
// mined or in the mempool.
case mempool.ErrMempoolDoubleSpend,
mempool.ErrAlreadyVoted,
mempool.ErrDuplicate,
mempool.ErrTooManyVotes,
mempool.ErrDuplicateRevocation,
mempool.ErrAlreadyExists,
mempool.ErrOrphan:
code = wire.RejectDuplicate
// Error codes which map to a non-standard transaction being
// relayed.
case mempool.ErrOrphanPolicyViolation,
mempool.ErrOldVote,
mempool.ErrSeqLockUnmet,
mempool.ErrNonStandard:
code = wire.RejectNonstandard
// Error codes which map to an insufficient fee being paid.
case mempool.ErrInsufficientFee,
mempool.ErrInsufficientPriority:
code = wire.RejectInsufficientFee
// Error codes which map to an attempt to create dust outputs.
case mempool.ErrDustOutput:
code = wire.RejectDust
}
reason = err.Error()
default:
reason = fmt.Sprintf("rejected: %v", err)
}
return code, reason
}
// handleTxMsg handles transaction messages from all peers.
func (b *blockManager) handleTxMsg(tmsg *txMsg) {
// NOTE: BitcoinJ, and possibly other wallets, don't follow the spec of
@ -649,7 +726,7 @@ func (b *blockManager) handleTxMsg(tmsg *txMsg) {
// Convert the error into an appropriate reject message and
// send it.
code, reason := mempool.ErrToRejectErr(err)
code, reason := errToWireRejectCode(err)
tmsg.peer.PushRejectMsg(wire.CmdTx, code, reason, txHash,
false)
return
@ -764,7 +841,7 @@ func (b *blockManager) handleBlockMsg(bmsg *blockMsg) {
// Convert the error into an appropriate reject message and
// send it.
code, reason := mempool.ErrToRejectErr(err)
code, reason := errToWireRejectCode(err)
bmsg.peer.PushRejectMsg(wire.CmdBlock, code, reason,
blockHash, false)
return
@ -1461,8 +1538,15 @@ func isDoubleSpendOrDuplicateError(err error) bool {
}
rerr, ok := merr.Err.(mempool.TxRuleError)
if ok && rerr.RejectCode == wire.RejectDuplicate {
return true
if ok {
switch rerr.ErrorCode {
case mempool.ErrDuplicate:
return true
case mempool.ErrAlreadyExists:
return true
default:
return false
}
}
cerr, ok := merr.Err.(blockchain.RuleError)

2
go.mod
View File

@ -22,7 +22,7 @@ require (
github.com/decred/dcrd/gcs/v2 v2.0.0
github.com/decred/dcrd/hdkeychain/v2 v2.0.1
github.com/decred/dcrd/lru v1.0.0
github.com/decred/dcrd/mempool/v3 v3.0.0
github.com/decred/dcrd/mempool/v3 v3.1.0
github.com/decred/dcrd/mining/v2 v2.0.0
github.com/decred/dcrd/peer/v2 v2.0.0
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0

View File

@ -6,6 +6,7 @@
package mempool
import (
"fmt"
"github.com/decred/dcrd/blockchain/v2"
"github.com/decred/dcrd/wire"
)
@ -28,14 +29,50 @@ func (e RuleError) Error() string {
return e.Err.Error()
}
// ErrorCode identifies the kind of error.
type ErrorCode int
const (
ErrOther ErrorCode = iota
ErrInvalid
ErrOrphanPolicyViolation
ErrMempoolDoubleSpend
ErrAlreadyVoted
ErrDuplicate
ErrCoinbase
ErrExpired
ErrNonStandard
ErrDustOutput
ErrInsufficientFee
ErrTooManyVotes
ErrDuplicateRevocation
ErrOldVote
ErrAlreadyExists
ErrSeqLockUnmet
ErrInsufficientPriority
ErrFeeTooHigh
ErrOrphan
)
// TxRuleError identifies a rule violation. It is used to indicate that
// processing of a transaction failed due to one of the many validation
// rules. The caller can use type assertions to determine if a failure was
// specifically due to a rule violation and access the ErrorCode field to
// ascertain the specific reason for the rule violation.
type TxRuleError struct {
RejectCode wire.RejectCode // The code to send with reject messages
Description string // Human readable description of the issue
// RejectCode is the corresponding rejection code to send when
// reporting the error via 'reject' wire protocol messages.
//
// Deprecated: This will be removed in the next major version. Use
// ErrorCode instead.
RejectCode wire.RejectCode
// ErrorCode is the mempool package error code ID.
ErrorCode ErrorCode
// Description is an additional human readable description of the
// error.
Description string
}
// Error satisfies the error interface and prints human-readable errors.
@ -45,9 +82,9 @@ func (e TxRuleError) Error() string {
// txRuleError creates an underlying TxRuleError with the given a set of
// arguments and returns a RuleError that encapsulates it.
func txRuleError(c wire.RejectCode, desc string) RuleError {
func txRuleError(c wire.RejectCode, code ErrorCode, desc string) RuleError {
return RuleError{
Err: TxRuleError{RejectCode: c, Description: desc},
Err: TxRuleError{RejectCode: c, ErrorCode: code, Description: desc},
}
}
@ -59,6 +96,46 @@ func chainRuleError(chainErr blockchain.RuleError) RuleError {
}
}
// IsErrorCode returns true if the passed error encodes a TxRuleError with the
// given ErrorCode, either directly or embedded in an outer RuleError.
func IsErrorCode(err error, code ErrorCode) bool {
// Unwrap RuleError if necessary.
if rerr, ok := err.(RuleError); ok {
err = rerr.Err
}
if trerr, ok := err.(TxRuleError); ok {
return trerr.ErrorCode == code
}
return false
}
// wrapTxRuleError returns a new RuleError with an underlying TxRuleError,
// replacing the description with the provided one while retaining both the
// error code and rejection code from the original error if they can be
// determined.
func wrapTxRuleError(rejectCode wire.RejectCode, errorCode ErrorCode, desc string, err error) error {
// Unwrap the underlying error if err is a RuleError
if rerr, ok := err.(RuleError); ok {
err = rerr.Err
}
// Override the passed rejectCode and errorCode with the ones from the
// error, if it is a TxRuleError
if txerr, ok := err.(TxRuleError); ok {
rejectCode = txerr.RejectCode
errorCode = txerr.ErrorCode
}
// Fill a default error description if empty.
if desc == "" {
desc = fmt.Sprintf("rejected: %v", err)
}
return txRuleError(rejectCode, errorCode, desc)
}
// extractRejectCode attempts to return a relevant reject code for a given error
// by examining the error for known types. It will return true if a code
// was successfully extracted.
@ -110,6 +187,8 @@ func extractRejectCode(err error) (wire.RejectCode, bool) {
// ErrToRejectErr examines the underlying type of the error and returns a reject
// code and string appropriate to be sent in a wire.MsgReject message.
//
// Deprecated: This will be removed in the next major version of this package.
func ErrToRejectErr(err error) (wire.RejectCode, string) {
// Return the reject code along with the error text if it can be
// extracted from the error.

View File

@ -450,7 +450,7 @@ func (mp *TxPool) maybeAddOrphan(tx *dcrutil.Tx) error {
str := fmt.Sprintf("orphan transaction size of %d bytes is "+
"larger than max allowed size of %d bytes",
serializedLen, mp.cfg.Policy.MaxOrphanTxSize)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrOrphanPolicyViolation, str)
}
// Add the orphan if the none of the above disqualified it.
@ -732,7 +732,7 @@ func (mp *TxPool) checkPoolDoubleSpend(tx *dcrutil.Tx, txType stake.TxType) erro
if txR, exists := mp.outpoints[txIn.PreviousOutPoint]; exists {
str := fmt.Sprintf("transaction %v in the pool "+
"already spends the same coins", txR.Hash())
return txRuleError(wire.RejectDuplicate, str)
return txRuleError(wire.RejectDuplicate, ErrMempoolDoubleSpend, str)
}
}
@ -771,7 +771,7 @@ func (mp *TxPool) checkVoteDoubleSpend(vote *dcrutil.Tx) error {
str := fmt.Sprintf("vote %v spending ticket %v already votes on "+
"block %s (height %d)", vote.Hash(), ticketSpent, hashVotedOn,
heightVotedOn)
return txRuleError(wire.RejectDuplicate, str)
return txRuleError(wire.RejectDuplicate, ErrAlreadyVoted, str)
}
}
mp.votesMtx.RUnlock()
@ -873,7 +873,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
// 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)
return nil, txRuleError(wire.RejectDuplicate, ErrDuplicate, str)
}
// Perform preliminary sanity checks on the transaction. This makes
@ -891,7 +891,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
if standalone.IsCoinBaseTx(msgTx) {
str := fmt.Sprintf("transaction %v is an individual coinbase",
txHash)
return nil, txRuleError(wire.RejectInvalid, str)
return nil, txRuleError(wire.RejectInvalid, ErrCoinbase, str)
}
// Get the current height of the main chain. A standalone transaction
@ -904,7 +904,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
if blockchain.IsExpired(tx, nextBlockHeight) {
str := fmt.Sprintf("transaction %v expired at height %d",
txHash, msgTx.Expiry)
return nil, txRuleError(wire.RejectInvalid, str)
return nil, txRuleError(wire.RejectInvalid, ErrExpired, str)
}
// Determine what type of transaction we're dealing with (regular or stake).
@ -938,7 +938,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
}
str := "violates sequence lock consensus bug"
return nil, txRuleError(wire.RejectInvalid, str)
return nil, txRuleError(wire.RejectInvalid, ErrInvalid, str)
}
}
}
@ -948,7 +948,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
if isVote && nextBlockHeight < stakeValidationHeight {
str := fmt.Sprintf("votes are not valid until block height %d (next "+
"block height %d)", stakeValidationHeight, nextBlockHeight)
return nil, txRuleError(wire.RejectInvalid, str)
return nil, txRuleError(wire.RejectInvalid, ErrInvalid, str)
}
// Reject revocations before they can possibly be valid. A vote must be
@ -959,7 +959,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
if isRevocation && nextBlockHeight < stakeValidationHeight+1 {
str := fmt.Sprintf("revocations are not valid until block height %d "+
"(next block height %d)", stakeValidationHeight+1, nextBlockHeight)
return nil, txRuleError(wire.RejectInvalid, str)
return nil, txRuleError(wire.RejectInvalid, ErrInvalid, str)
}
// Don't allow non-standard transactions if the mempool config forbids
@ -970,16 +970,10 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
medianTime, 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, txRuleError(rejectCode, str)
return nil, wrapTxRuleError(wire.RejectNonstandard,
ErrNonStandard, str, err)
}
}
@ -998,7 +992,8 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
str := fmt.Sprintf("transaction %v has not enough funds "+
"to meet stake difficulty (ticket diff %v < next diff %v)",
txHash, msgTx.TxOut[0].Value, sDiff)
return nil, txRuleError(wire.RejectInsufficientFee, str)
return nil, txRuleError(wire.RejectInsufficientFee,
ErrInsufficientFee, str)
}
}
@ -1039,7 +1034,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
str := fmt.Sprintf("transaction %v in the pool with more than "+
"%v votes", msgTx.TxIn[1].PreviousOutPoint,
maxVoteDoubleSpends)
return nil, txRuleError(wire.RejectDuplicate, str)
return nil, txRuleError(wire.RejectDuplicate, ErrTooManyVotes, str)
}
}
@ -1051,7 +1046,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
str := fmt.Sprintf("transaction %v in the pool as a "+
"revocation. Only one revocation is allowed.",
msgTx.TxIn[0].PreviousOutPoint)
return nil, txRuleError(wire.RejectDuplicate, str)
return nil, txRuleError(wire.RejectDuplicate, ErrDuplicateRevocation, str)
}
}
}
@ -1067,7 +1062,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
"block height of %v which is before the "+
"current cutoff height of %v",
tx.Hash(), voteHeight, nextBlockHeight-maximumVoteAgeDelta)
return nil, txRuleError(wire.RejectNonstandard, str)
return nil, txRuleError(wire.RejectNonstandard, ErrOldVote, str)
}
}
@ -1087,7 +1082,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
// already fully spent.
txEntry := utxoView.LookupEntry(txHash)
if txEntry != nil && !txEntry.IsFullySpent() {
return nil, txRuleError(wire.RejectDuplicate,
return nil, txRuleError(wire.RejectDuplicate, ErrAlreadyExists,
"transaction already exists")
}
delete(utxoView.Entries(), *txHash)
@ -1139,7 +1134,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
return nil, err
}
if !blockchain.SequenceLockActive(seqLock, nextBlockHeight, medianTime) {
return nil, txRuleError(wire.RejectNonstandard,
return nil, txRuleError(wire.RejectNonstandard, ErrSeqLockUnmet,
"transaction sequence locks on inputs not met")
}
@ -1162,16 +1157,10 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
if !mp.cfg.Policy.AcceptNonStd {
err := checkInputsStandard(tx, txType, 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, txRuleError(rejectCode, str)
return nil, wrapTxRuleError(wire.RejectNonstandard,
ErrNonStandard, str, err)
}
}
@ -1197,7 +1186,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
if numSigOps > mp.cfg.Policy.MaxSigOpsPerTx {
str := fmt.Sprintf("transaction %v has too many sigops: %d > %d",
txHash, numSigOps, mp.cfg.Policy.MaxSigOpsPerTx)
return nil, txRuleError(wire.RejectNonstandard, str)
return nil, txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
// Don't allow transactions with fees too low to get into a mined block.
@ -1222,7 +1211,8 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
str := fmt.Sprintf("transaction %v has %v fees which "+
"is under the required amount of %v", txHash,
txFee, minFee)
return nil, txRuleError(wire.RejectInsufficientFee, str)
return nil, txRuleError(wire.RejectInsufficientFee,
ErrInsufficientFee, str)
}
}
@ -1241,7 +1231,8 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
str := fmt.Sprintf("transaction %v has insufficient "+
"priority (%g <= %g)", txHash,
currentPriority, mining.MinHighPriority)
return nil, txRuleError(wire.RejectInsufficientFee, str)
return nil, txRuleError(wire.RejectInsufficientFee,
ErrInsufficientPriority, str)
}
}
@ -1260,7 +1251,8 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
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, txRuleError(wire.RejectInsufficientFee, str)
return nil, txRuleError(wire.RejectInsufficientFee,
ErrInsufficientFee, str)
}
oldTotal := mp.pennyTotal
@ -1281,7 +1273,8 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
str := fmt.Sprintf("ticket purchase transaction %v has a %v "+
"fee which is under the required threshold amount of %d",
txHash, txFee, minTicketFee)
return nil, txRuleError(wire.RejectInsufficientFee, str)
return nil, txRuleError(wire.RejectInsufficientFee,
ErrInsufficientFee, str)
}
}
@ -1292,10 +1285,10 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
maxFee := calcMinRequiredTxRelayFee(serializedSize*maxRelayFeeMultiplier,
mp.cfg.Policy.MinRelayTxFee)
if txFee > maxFee {
err = fmt.Errorf("transaction %v has %v fee which is above the "+
str := fmt.Sprintf("transaction %v has %v fee which is above the "+
"allowHighFee check threshold amount of %v", txHash,
txFee, maxFee)
return nil, err
return nil, txRuleError(wire.RejectInvalid, ErrFeeTooHigh, str)
}
}
@ -1573,7 +1566,7 @@ func (mp *TxPool) ProcessTransaction(tx *dcrutil.Tx, allowOrphan, rateLimit, all
str := fmt.Sprintf("orphan transaction %v references "+
"outputs of unknown or fully-spent "+
"transaction %v", tx.Hash(), missingParents[0])
return nil, txRuleError(wire.RejectDuplicate, str)
return nil, txRuleError(wire.RejectDuplicate, ErrOrphan, str)
}
// Potentially add the orphan transaction to the orphan pool.

View File

@ -967,9 +967,8 @@ func TestVoteOrphan(t *testing.T) {
// Ensure the vote is rejected because it is an orphan.
_, err = harness.txPool.ProcessTransaction(vote, false, false, true)
if err == nil {
t.Fatalf("ProcessTransaction: accepted transaction references " +
"outputs of unknown or fully-spent transaction")
if !IsErrorCode(err, ErrOrphan) {
t.Fatalf("Process Transaction: did not get expected ErrOrphan")
}
testPoolMembership(tc, vote, false, false)
@ -1038,9 +1037,9 @@ func TestRevocationOrphan(t *testing.T) {
// Ensure the vote is rejected because it is an orphan.
_, err = harness.txPool.ProcessTransaction(revocation, false, false, true)
if err == nil {
t.Fatalf("ProcessTransaction: accepted transaction references " +
"outputs of unknown or fully-spent transaction")
if !IsErrorCode(err, ErrOrphan) {
t.Fatalf("Process Transaction: did not get expected " +
"ErrTooManyVotes error code")
}
testPoolMembership(tc, revocation, false, false)
@ -1117,6 +1116,11 @@ func TestOrphanReject(t *testing.T) {
"-- got %v, want %v", code, wire.RejectDuplicate)
}
if !IsErrorCode(err, ErrOrphan) {
t.Fatalf("ProcessTransaction: unexpected error code "+
"-- got %v, want %v", code, ErrOrphan)
}
// Ensure no transactions were reported as accepted.
if len(acceptedTxns) != 0 {
t.Fatal("ProcessTransaction: reported %d accepted "+
@ -1769,6 +1773,10 @@ func TestSequenceLockAcceptance(t *testing.T) {
case acceptSeqLocks && !test.valid && err == nil:
t.Fatalf("%s: did not reject tx", test.name)
case acceptSeqLocks && !test.valid && !IsErrorCode(err, ErrSeqLockUnmet):
t.Fatalf("%s: did not get expected ErrSeqLockUnmet",
test.name)
}
// Ensure the number of reported accepted transactions and pool
@ -1882,6 +1890,10 @@ func TestMaxVoteDoubleSpendRejection(t *testing.T) {
t.Fatalf("ProcessTransaction: accepted double-spending vote with " +
"more than max allowed")
}
if !IsErrorCode(err, ErrTooManyVotes) {
t.Fatalf("Process Transaction: did not get expected " +
"ErrTooManyVotes error code")
}
// Ensure no transactions were reported as accepted.
if len(acceptedTxns) != 0 {
@ -1916,9 +1928,9 @@ func TestMaxVoteDoubleSpendRejection(t *testing.T) {
// in the transaction pool, and not reported as available.
vote = votes[maxVoteDoubleSpends+1]
_, err = harness.txPool.ProcessTransaction(vote, false, false, true)
if err == nil {
t.Fatalf("ProcessTransaction: accepted double-spending vote with " +
"more than max allowed")
if !IsErrorCode(err, ErrTooManyVotes) {
t.Fatalf("Process Transaction: did not get expected " +
"ErrTooManyVotes error code")
}
testPoolMembership(tc, vote, false, false)
}
@ -1988,9 +2000,9 @@ func TestDuplicateVoteRejection(t *testing.T) {
// ensure it is not in the orphan pool, not in the transaction pool, and not
// reported as available.
_, err = harness.txPool.ProcessTransaction(dupVote, false, false, true)
if err == nil {
t.Fatalf("ProcessTransaction: accepted duplicate vote with different " +
"hash")
if !IsErrorCode(err, ErrAlreadyVoted) {
t.Fatalf("Process Transaction: did not get expected " +
"ErrTooManyVotes error code")
}
testPoolMembership(tc, dupVote, false, false)
@ -2008,3 +2020,107 @@ func TestDuplicateVoteRejection(t *testing.T) {
}
testPoolMembership(tc, dupVote, false, true)
}
// TestDuplicateTxError ensures that attempting to add a transaction to the
// pool which is an exact duplicate of another transaction fails with the
// appropriate error.
func TestDuplicateTxError(t *testing.T) {
t.Parallel()
harness, spendableOuts, err := newPoolHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
tc := &testContext{t, harness}
// Create a regular transaction from the first spendable output provided by
// the harness.
tx, err := harness.CreateTx(spendableOuts[0])
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
// Ensure the transaction is accepted to the pool.
_, err = harness.txPool.ProcessTransaction(tx, true, false, true)
if err != nil {
t.Fatalf("ProcessTransaction: failed to accept initial tx: %v", err)
}
testPoolMembership(tc, tx, false, true)
// Ensure a second attempt to process the tx is rejected with the
// correct error code and that the transaction remains in the pool.
_, err = harness.txPool.ProcessTransaction(tx, true, false, true)
if !IsErrorCode(err, ErrDuplicate) {
t.Fatalf("ProcessTransaction: did get the expected ErrDuplicate")
}
testPoolMembership(tc, tx, false, true)
// Create an orphan transaction to perform the same test but this time
// in the orphan pool. The orphan tx is the second one in the created
// chain.
txs, err := harness.CreateTxChain(txOutToSpendableOut(tx, 0, 0), 2)
if err != nil {
t.Fatalf("unable to create orphan chain: %v", err)
}
orphan := txs[1]
// The first call to ProcessTransaction should succeed when enabling
// orphans.
_, err = harness.txPool.ProcessTransaction(orphan, true, false, true)
if err != nil {
t.Fatalf("ProcessTransaction: failed to accept orphan tx: %v", err)
}
testPoolMembership(tc, orphan, true, false)
// The second call should fail with the expected ErrDuplicate error.
_, err = harness.txPool.ProcessTransaction(orphan, true, false, true)
if !IsErrorCode(err, ErrDuplicate) {
t.Fatalf("ProcessTransaction: did not get expected ErrDuplicate")
}
testPoolMembership(tc, orphan, true, false)
}
// TestMempoolDoubleSpend ensures that attempting to add a transaction to the
// pool which spends an output already in the mempool fails for the correct
// reason.
func TestMempoolDoubleSpend(t *testing.T) {
t.Parallel()
harness, spendableOuts, err := newPoolHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
tc := &testContext{t, harness}
// Create a regular transaction from the first spendable output provided by
// the harness.
tx, err := harness.CreateTx(spendableOuts[0])
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
// Ensure the transaction is accepted to the pool.
_, err = harness.txPool.ProcessTransaction(tx, true, false, true)
if err != nil {
t.Fatalf("ProcessTransaction: failed to accept initial tx: %v", err)
}
testPoolMembership(tc, tx, false, true)
// Create a second transaction, spending the same outputs. Create with
// 2 outputs so that it is a different transaction than the original
// one.
doubleSpendTx, err := harness.CreateSignedTx(spendableOuts, 2)
if err != nil {
t.Fatalf("unable to create double spend tx: %v", err)
}
// Ensure a second attempt to process the tx is rejected with the
// correct error code, that the original transaction remains in the
// pool and the double spend is not added to the pool.
_, err = harness.txPool.ProcessTransaction(doubleSpendTx, true, false, true)
if !IsErrorCode(err, ErrMempoolDoubleSpend) {
t.Fatalf("ProcessTransaction: did not get expected ErrMempoolDoubleSpend")
}
testPoolMembership(tc, tx, false, true)
testPoolMembership(tc, doubleSpendTx, false, false)
}

View File

@ -103,6 +103,9 @@ func calcMinRequiredTxRelayFee(serializedSize int64, minRelayTxFee dcrutil.Amoun
// not perform those checks because the script engine already does this more
// accurately and concisely via the txscript.ScriptVerifyCleanStack and
// txscript.ScriptVerifySigPushOnly flags.
//
// Note: all non-nil errors MUST be RuleError with an underlying TxRuleError
// instance.
func checkInputsStandard(tx *dcrutil.Tx, txType stake.TxType, utxoView *blockchain.UtxoViewpoint) error {
// NOTE: The reference implementation also does a coinbase check here,
// but coinbases have already been rejected prior to calling this
@ -129,13 +132,13 @@ func checkInputsStandard(tx *dcrutil.Tx, txType stake.TxType, utxoView *blockcha
"%d signature operations which is more "+
"than the allowed max amount of %d",
i, numSigOps, maxStandardP2SHSigOps)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
case txscript.NonStandardTy:
str := fmt.Sprintf("transaction input #%d has a "+
"non-standard script form", i)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
}
@ -147,6 +150,9 @@ func checkInputsStandard(tx *dcrutil.Tx, txType stake.TxType, utxoView *blockcha
// A standard public key script is one that is a recognized form, and for
// multi-signature scripts, only contains from 1 to maxStandardMultiSigKeys
// public keys.
//
// Note: all non-nil errors MUST be RuleError with an underlying TxRuleError
// instance.
func checkPkScriptStandard(version uint16, pkScript []byte,
scriptClass txscript.ScriptClass) error {
// Only default Bitcoin-style script is standard except for
@ -155,7 +161,7 @@ func checkPkScriptStandard(version uint16, pkScript []byte,
str := fmt.Sprintf("versions other than default pkscript version " +
"are currently non-standard except for provably unspendable " +
"outputs")
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
switch scriptClass {
@ -164,38 +170,38 @@ func checkPkScriptStandard(version uint16, pkScript []byte,
if err != nil {
str := fmt.Sprintf("multi-signature script parse "+
"failure: %v", err)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
// A standard multi-signature public key script must contain
// from 1 to maxStandardMultiSigKeys public keys.
if numPubKeys < 1 {
str := "multi-signature script with no pubkeys"
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
if numPubKeys > maxStandardMultiSigKeys {
str := fmt.Sprintf("multi-signature script with %d "+
"public keys which is more than the allowed "+
"max of %d", numPubKeys, maxStandardMultiSigKeys)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
// A standard multi-signature public key script must have at
// least 1 signature and no more signatures than available
// public keys.
if numSigs < 1 {
return txRuleError(wire.RejectNonstandard,
return txRuleError(wire.RejectNonstandard, ErrNonStandard,
"multi-signature script with no signatures")
}
if numSigs > numPubKeys {
str := fmt.Sprintf("multi-signature script with %d "+
"signatures which is more than the available "+
"%d public keys", numSigs, numPubKeys)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
case txscript.NonStandardTy:
return txRuleError(wire.RejectNonstandard,
return txRuleError(wire.RejectNonstandard, ErrNonStandard,
"non-standard script form")
}
@ -282,6 +288,9 @@ func isDust(txOut *wire.TxOut, minRelayTxFee dcrutil.Amount) bool {
// 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).
//
// Note: all non-nil errors MUST be RuleError with an underlying TxRuleError
// instance.
func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
medianTime time.Time, minRelayTxFee dcrutil.Amount,
maxTxVersion uint16) error {
@ -292,18 +301,18 @@ func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
if msgTx.SerType != wire.TxSerializeFull {
str := fmt.Sprintf("transaction is not serialized with all "+
"required data -- type %v", msgTx.SerType)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
if msgTx.Version > maxTxVersion || msgTx.Version < 1 {
str := fmt.Sprintf("transaction version %d is not in the "+
"valid range of %d-%d", msgTx.Version, 1, maxTxVersion)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
// The transaction must be finalized to be standard and therefore
// considered for inclusion in a block.
if !blockchain.IsFinalizedTransaction(tx, height, medianTime) {
return txRuleError(wire.RejectNonstandard,
return txRuleError(wire.RejectNonstandard, ErrNonStandard,
"transaction is not finalized")
}
@ -315,7 +324,7 @@ func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
if serializedLen > maxStandardTxSize {
str := fmt.Sprintf("transaction size of %v is larger than max "+
"allowed size of %v", serializedLen, maxStandardTxSize)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
for i, txIn := range msgTx.TxIn {
@ -328,7 +337,7 @@ func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
"script size of %d bytes is large than max "+
"allowed size of %d bytes", i, sigScriptLen,
maxStandardSigScriptSize)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
// Each transaction input signature script must only contain
@ -336,7 +345,7 @@ func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
if !txscript.IsPushOnlyScript(txIn.SignatureScript) {
str := fmt.Sprintf("transaction input %d: signature "+
"script is not push only", i)
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
}
@ -348,15 +357,9 @@ func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
scriptClass := txscript.GetScriptClass(txOut.Version, txOut.PkScript)
err := checkPkScriptStandard(txOut.Version, txOut.PkScript, scriptClass)
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 output %d: %v", i, err)
return txRuleError(rejectCode, str)
return wrapTxRuleError(wire.RejectNonstandard,
ErrNonStandard, str, err)
}
// Accumulate the number of outputs which only carry data. For
@ -367,7 +370,7 @@ func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
} else if txType == stake.TxTypeRegular && isDust(txOut, minRelayTxFee) {
str := fmt.Sprintf("transaction output %d: payment "+
"of %d is dust", i, txOut.Value)
return txRuleError(wire.RejectDust, str)
return txRuleError(wire.RejectDust, ErrDustOutput, str)
}
}
@ -378,7 +381,7 @@ func checkTransactionStandard(tx *dcrutil.Tx, txType stake.TxType, height int64,
if numNullDataOutputs > maxNullDataOutputs && txType == stake.TxTypeRegular {
str := "more than one transaction output in a nulldata script for a " +
"regular type tx"
return txRuleError(wire.RejectNonstandard, str)
return txRuleError(wire.RejectNonstandard, ErrNonStandard, str)
}
return nil

View File

@ -4111,9 +4111,9 @@ func handleSendRawTransaction(s *rpcServer, cmd interface{}, closeChan <-chan st
err = fmt.Errorf("rejected transaction %v: %v", tx.Hash(),
err)
rpcsLog.Debugf("%v", err)
txRuleErr, ok := rErr.Err.(mempool.TxRuleError)
if ok && txRuleErr.RejectCode == wire.RejectDuplicate {
// return a duplicate tx error
if mempool.IsErrorCode(rErr, mempool.ErrDuplicate) {
// This is an actual exact duplicate tx, so
// return the specific duplicate tx error.
return nil, rpcDuplicateTxError("%v", err)
}