From b5b371bfccbda70bf6ad5d74bbc08640eb8dd109 Mon Sep 17 00:00:00 2001 From: Donald Adu-Poku Date: Thu, 15 Oct 2020 01:37:08 +0000 Subject: [PATCH] multi: update blockchain and mempool error types. This updates the blockchain and mempool error types to leverage go 1.13 errors.Is/As functionality as well as confirm to the error infrastructure best practices. --- blockchain/chain.go | 11 +- blockchain/chain_test.go | 22 +- blockchain/common_test.go | 20 +- blockchain/error.go | 439 +++++++------------ blockchain/error_test.go | 123 ++++-- blockchain/fullblocks_test.go | 14 +- blockchain/fullblocktests/generate.go | 10 +- blockchain/validate_test.go | 47 +- blockmanager.go | 143 +++--- cmd/addblock/import.go | 3 +- internal/mempool/error.go | 192 ++++---- internal/mempool/error_test.go | 160 +++++++ internal/mempool/mempool_test.go | 38 +- internal/mempool/policy_test.go | 25 +- internal/mining/cpuminer/cpuminer.go | 2 +- internal/rpcserver/rpcserver.go | 2 +- internal/rpcserver/rpcserverhandlers_test.go | 2 +- 17 files changed, 625 insertions(+), 628 deletions(-) create mode 100644 internal/mempool/error_test.go diff --git a/blockchain/chain.go b/blockchain/chain.go index 477e7ad82c..3c157851ea 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -1260,21 +1260,22 @@ func (b *BlockChain) forceHeadReorganization(formerBest chainhash.Hash, newBest // We can't reorganize the chain unless our head block matches up with // b.bestChain. if !formerBestNode.hash.IsEqual(&formerBest) { - return ruleError(ErrForceReorgWrongChain, "tried to force reorg "+ - "on wrong chain") + str := "tried to force reorg on wrong chain" + return ruleError(ErrForceReorgWrongChain, str) } // Child to reorganize to is missing. newBestNode := b.index.LookupNode(&newBest) if newBestNode == nil || newBestNode.parent != formerBestNode.parent { - return ruleError(ErrForceReorgMissingChild, "missing child of "+ - "common parent for forced reorg") + str := "missing child of common parent for forced reorg" + return ruleError(ErrForceReorgMissingChild, str) } // Don't allow a reorganize to a known invalid chain. newBestNodeStatus := b.index.NodeStatus(newBestNode) if newBestNodeStatus.KnownInvalid() { - return ruleError(ErrKnownInvalidBlock, "block is known to be invalid") + str := "block is known to be invalid" + return ruleError(ErrKnownInvalidBlock, str) } // Reorganize the chain and flush any potential unsaved changes to the diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index 3ac5300c6c..663f1105ab 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -157,8 +157,8 @@ func TestForceHeadReorg(t *testing.T) { // // rejectForceTipReorg forces the chain instance to reorganize the // current tip of the main chain from the given block to the given - // block and expected it to be rejected with the provided error code. - rejectForceTipReorg := func(fromTipName, toTipName string, code ErrorCode) { + // block and expected it to be rejected with the provided error kind. + rejectForceTipReorg := func(fromTipName, toTipName string, kind ErrorKind) { from := g.BlockByName(fromTipName) to := g.BlockByName(toTipName) t.Logf("Testing forced reorg from %s (hash %s, height %d) "+ @@ -175,25 +175,15 @@ func TestForceHeadReorg(t *testing.T) { to.Header.Height) } - // Ensure the error code is of the expected type and the reject - // code matches the value specified in the test instance. - var rerr RuleError - if !errors.As(err, &rerr) { - t.Fatalf("forced header reorg from block %q (hash %s, "+ - "height %d) to block %q (hash %s, height %d) "+ - "returned unexpected error type -- got %T, "+ - "want blockchain.RuleError", fromTipName, - from.BlockHash(), from.Header.Height, toTipName, - to.BlockHash(), to.Header.Height, err) - } - if rerr.ErrorCode != code { + // Ensure the error kind is of the expected type and matches + // the value specified in the test instance. + if !errors.Is(err, kind) { t.Fatalf("forced header reorg from block %q (hash %s, "+ "height %d) to block %q (hash %s, height %d) "+ "does not have expected reject code -- got %v, "+ "want %v", fromTipName, from.BlockHash(), from.Header.Height, toTipName, - to.BlockHash(), to.Header.Height, rerr.ErrorCode, - code) + to.BlockHash(), to.Header.Height, err, kind) } } diff --git a/blockchain/common_test.go b/blockchain/common_test.go index e5fbc8a296..67843b78b7 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -532,8 +532,8 @@ func (g *chaingenHarness) AcceptTipBlock() { } // RejectBlock expects the block associated with the given name in the harness -// generator to be rejected with the provided error code. -func (g *chaingenHarness) RejectBlock(blockName string, code ErrorCode) { +// generator to be rejected with the provided error kind. +func (g *chaingenHarness) RejectBlock(blockName string, kind ErrorKind) { g.t.Helper() msgBlock := g.BlockByName(blockName) @@ -548,27 +548,21 @@ func (g *chaingenHarness) RejectBlock(blockName string, code ErrorCode) { blockName, block.Hash(), blockHeight) } - // Ensure the error code is of the expected type and the reject code matches + // Ensure the error kind is of the expected type and matches // the value specified in the test instance. - var rerr RuleError - if !errors.As(err, &rerr) { - g.t.Fatalf("block %q (hash %s, height %d) returned unexpected error "+ - "type -- got %T, want blockchain.RuleError", blockName, - block.Hash(), blockHeight, err) - } - if rerr.ErrorCode != code { + if !errors.Is(err, kind) { g.t.Fatalf("block %q (hash %s, height %d) does not have expected reject "+ "code -- got %v, want %v", blockName, block.Hash(), blockHeight, - rerr.ErrorCode, code) + err, kind) } } // RejectTipBlock expects the current tip block associated with the harness // generator to be rejected with the provided error code. -func (g *chaingenHarness) RejectTipBlock(code ErrorCode) { +func (g *chaingenHarness) RejectTipBlock(kind ErrorKind) { g.t.Helper() - g.RejectBlock(g.TipName(), code) + g.RejectBlock(g.TipName(), kind) } // ExpectTip expects the provided block to be the current tip of the main chain diff --git a/blockchain/error.go b/blockchain/error.go index 5edce24a7d..dd2de481f6 100644 --- a/blockchain/error.go +++ b/blockchain/error.go @@ -6,7 +6,6 @@ package blockchain import ( - "errors" "fmt" "github.com/decred/dcrd/chaincfg/chainhash" @@ -93,703 +92,563 @@ func (e AssertError) Error() string { return "assertion failed: " + string(e) } -// ErrorCode identifies a kind of error. -type ErrorCode int +// ErrorKind identifies a kind of error. It has full support for errors.Is and +// errors.As, so the caller can directly check against an error kind when +// determining the reason for an error. +type ErrorKind string // These constants are used to identify a specific RuleError. const ( // ErrDuplicateBlock indicates a block with the same hash already // exists. - ErrDuplicateBlock ErrorCode = iota + ErrDuplicateBlock = ErrorKind("ErrDuplicateBlock") // ErrMissingParent indicates that the block was an orphan. - ErrMissingParent + ErrMissingParent = ErrorKind("ErrMissingParent") // ErrBlockTooBig indicates the serialized block size exceeds the // maximum allowed size. - ErrBlockTooBig + ErrBlockTooBig = ErrorKind("ErrBlockTooBig") // ErrWrongBlockSize indicates that the block size in the header is not // the actual serialized size of the block. - ErrWrongBlockSize + ErrWrongBlockSize = ErrorKind("ErrWrongBlockSize") // ErrBlockVersionTooOld indicates the block version is too old and is // no longer accepted since the majority of the network has upgraded // to a newer version. - ErrBlockVersionTooOld + ErrBlockVersionTooOld = ErrorKind("ErrBlockVersionTooOld") // ErrBadStakeVersionindicates the block version is too old and is no // longer accepted since the majority of the network has upgraded to a // newer version. - ErrBadStakeVersion + ErrBadStakeVersion = ErrorKind("ErrBadStakeVersion") // ErrInvalidTime indicates the time in the passed block has a precision // that is more than one second. The chain consensus rules require // timestamps to have a maximum precision of one second. - ErrInvalidTime + ErrInvalidTime = ErrorKind("ErrInvalidTime") // ErrTimeTooOld indicates the time is either before the median time of // the last several blocks per the chain consensus rules or prior to the // most recent checkpoint. - ErrTimeTooOld + ErrTimeTooOld = ErrorKind("ErrTimeTooOld") // ErrTimeTooNew indicates the time is too far in the future as compared // the current time. - ErrTimeTooNew + ErrTimeTooNew = ErrorKind("ErrTimeTooNew") // ErrDifficultyTooLow indicates the difficulty for the block is lower // than the difficulty required by the most recent checkpoint. - ErrDifficultyTooLow + ErrDifficultyTooLow = ErrorKind("ErrDifficultyTooLow") // ErrUnexpectedDifficulty indicates specified bits do not align with // the expected value either because it doesn't match the calculated // value based on difficulty regarding the rules or it is out of the // valid range. - ErrUnexpectedDifficulty + ErrUnexpectedDifficulty = ErrorKind("ErrUnexpectedDifficulty") // ErrHighHash indicates the block does not hash to a value which is // lower than the required target difficultly. - ErrHighHash + ErrHighHash = ErrorKind("ErrHighHash") // ErrBadMerkleRoot indicates the calculated merkle root does not match // the expected value. - ErrBadMerkleRoot + ErrBadMerkleRoot = ErrorKind("ErrBadMerkleRoot") // ErrBadCommitmentRoot indicates the calculated commitment root does // not match the expected value. - ErrBadCommitmentRoot + ErrBadCommitmentRoot = ErrorKind("ErrBadCommitmentRoot") // ErrBadCheckpoint indicates a block that is expected to be at a // checkpoint height does not match the expected one. - ErrBadCheckpoint + ErrBadCheckpoint = ErrorKind("ErrBadCheckpoint") // ErrForkTooOld indicates a block is attempting to fork the block chain // before the most recent checkpoint. - ErrForkTooOld + ErrForkTooOld = ErrorKind("ErrForkTooOld") // ErrCheckpointTimeTooOld indicates a block has a timestamp before the // most recent checkpoint. - ErrCheckpointTimeTooOld + ErrCheckpointTimeTooOld = ErrorKind("ErrCheckpointTimeTooOld") // ErrNoTransactions indicates the block does not have a least one // transaction. A valid block must have at least the coinbase // transaction. - ErrNoTransactions + ErrNoTransactions = ErrorKind("ErrNoTransactions") // ErrTooManyTransactions indicates the block has more transactions than // are allowed. - ErrTooManyTransactions + ErrTooManyTransactions = ErrorKind("ErrTooManyTransactions") // ErrNoTxInputs indicates a transaction does not have any inputs. A // valid transaction must have at least one input. - ErrNoTxInputs + ErrNoTxInputs = ErrorKind("ErrNoTxInputs") // ErrNoTxOutputs indicates a transaction does not have any outputs. A // valid transaction must have at least one output. - ErrNoTxOutputs + ErrNoTxOutputs = ErrorKind("ErrNoTxOutputs") // ErrInvalidTxOutputs indicates a transaction does not have the exact // number of outputs. - ErrInvalidTxOutputs + ErrInvalidTxOutputs = ErrorKind("ErrInvalidTxOutputs") // ErrTxTooBig indicates a transaction exceeds the maximum allowed size // when serialized. - ErrTxTooBig + ErrTxTooBig = ErrorKind("ErrTxTooBig") // ErrBadTxOutValue indicates an output value for a transaction is // invalid in some way such as being out of range. - ErrBadTxOutValue + ErrBadTxOutValue = ErrorKind("ErrBadTxOutValue") // ErrDuplicateTxInputs indicates a transaction references the same // input more than once. - ErrDuplicateTxInputs + ErrDuplicateTxInputs = ErrorKind("ErrDuplicateTxInputs") // ErrBadTxInput indicates a transaction input is invalid in some way // such as referencing a previous transaction outpoint which is out of // range or not referencing one at all. - ErrBadTxInput + ErrBadTxInput = ErrorKind("ErrBadTxInput") // ErrMissingTxOut indicates a transaction output referenced by an input // either does not exist or has already been spent. - ErrMissingTxOut + ErrMissingTxOut = ErrorKind("ErrMissingTxOut") // ErrUnfinalizedTx indicates a transaction has not been finalized. // A valid block may only contain finalized transactions. - ErrUnfinalizedTx + ErrUnfinalizedTx = ErrorKind("ErrUnfinalizedTx") // ErrDuplicateTx indicates a block contains an identical transaction // (or at least two transactions which hash to the same value). A // valid block may only contain unique transactions. - ErrDuplicateTx + ErrDuplicateTx = ErrorKind("ErrDuplicateTx") // ErrOverwriteTx indicates a block contains a transaction that has // the same hash as a previous transaction which has not been fully // spent. - ErrOverwriteTx + ErrOverwriteTx = ErrorKind("ErrOverwriteTx") // ErrImmatureSpend indicates a transaction is attempting to spend a // coinbase that has not yet reached the required maturity. - ErrImmatureSpend + ErrImmatureSpend = ErrorKind("ErrImmatureSpend") // ErrSpendTooHigh indicates a transaction is attempting to spend more // value than the sum of all of its inputs. - ErrSpendTooHigh + ErrSpendTooHigh = ErrorKind("ErrSpendTooHigh") // ErrBadFees indicates the total fees for a block are invalid due to // exceeding the maximum possible value. - ErrBadFees + ErrBadFees = ErrorKind("ErrBadFees") // ErrTooManySigOps indicates the total number of signature operations // for a transaction or block exceed the maximum allowed limits. - ErrTooManySigOps + ErrTooManySigOps = ErrorKind("ErrTooManySigOps") // ErrFirstTxNotCoinbase indicates the first transaction in a block // is not a coinbase transaction. - ErrFirstTxNotCoinbase + ErrFirstTxNotCoinbase = ErrorKind("ErrFirstTxNotCoinbase") // ErrFirstTxNotOpReturn indicates the first transaction in a block // is not an OP_RETURN. - ErrFirstTxNotOpReturn + ErrFirstTxNotOpReturn = ErrorKind("ErrFirstTxNotOpReturn") // ErrCoinbaseHeight indicates that the encoded height in the coinbase // is incorrect. - ErrCoinbaseHeight + ErrCoinbaseHeight = ErrorKind("ErrCoinbaseHeight") // ErrMultipleCoinbases indicates a block contains more than one // coinbase transaction. - ErrMultipleCoinbases + ErrMultipleCoinbases = ErrorKind("ErrMultipleCoinbases") // ErrStakeTxInRegularTree indicates a stake transaction was found in // the regular transaction tree. - ErrStakeTxInRegularTree + ErrStakeTxInRegularTree = ErrorKind("ErrStakeTxInRegularTree") // ErrRegTxInStakeTree indicates that a regular transaction was found in // the stake transaction tree. - ErrRegTxInStakeTree + ErrRegTxInStakeTree = ErrorKind("ErrRegTxInStakeTree") // ErrBadCoinbaseScriptLen indicates the length of the signature script // for a coinbase transaction is not within the valid range. - ErrBadCoinbaseScriptLen + ErrBadCoinbaseScriptLen = ErrorKind("ErrBadCoinbaseScriptLen") // ErrBadCoinbaseValue indicates the amount of a coinbase value does // not match the expected value of the subsidy plus the sum of all fees. - ErrBadCoinbaseValue + ErrBadCoinbaseValue = ErrorKind("ErrBadCoinbaseValue") // ErrBadCoinbaseOutpoint indicates that the outpoint used by a coinbase // as input was non-null. - ErrBadCoinbaseOutpoint + ErrBadCoinbaseOutpoint = ErrorKind("ErrBadCoinbaseOutpoint") // ErrBadCoinbaseFraudProof indicates that the fraud proof for a coinbase // input was non-null. - ErrBadCoinbaseFraudProof + ErrBadCoinbaseFraudProof = ErrorKind("ErrBadCoinbaseFraudProof") // ErrBadCoinbaseAmountIn indicates that the AmountIn (=subsidy) for a // coinbase input was incorrect. - ErrBadCoinbaseAmountIn + ErrBadCoinbaseAmountIn = ErrorKind("ErrBadCoinbaseAmountIn") // ErrBadStakebaseAmountIn indicates that the AmountIn (=subsidy) for a // stakebase input was incorrect. - ErrBadStakebaseAmountIn + ErrBadStakebaseAmountIn = ErrorKind("ErrBadStakebaseAmountIn") // ErrBadStakebaseScriptLen indicates the length of the signature script // for a stakebase transaction is not within the valid range. - ErrBadStakebaseScriptLen + ErrBadStakebaseScriptLen = ErrorKind("ErrBadStakebaseScriptLen") // ErrBadStakebaseScrVal indicates the signature script for a stakebase // transaction was not set to the network consensus value. - ErrBadStakebaseScrVal + ErrBadStakebaseScrVal = ErrorKind("ErrBadStakebaseScrVal") // ErrScriptMalformed indicates a transaction script is malformed in // some way. For example, it might be longer than the maximum allowed // length or fail to parse. - ErrScriptMalformed + ErrScriptMalformed = ErrorKind("ErrScriptMalformed") // ErrScriptValidation indicates the result of executing transaction // script failed. The error covers any failure when executing scripts // such signature verification failures and execution past the end of // the stack. - ErrScriptValidation + ErrScriptValidation = ErrorKind("ErrScriptValidation") // ErrNotEnoughStake indicates that there was for some SStx in a given block, // the given SStx did not have enough stake to meet the network target. - ErrNotEnoughStake + ErrNotEnoughStake = ErrorKind("ErrNotEnoughStake") // ErrStakeBelowMinimum indicates that for some SStx in a given block, // the given SStx had an amount of stake below the minimum network target. - ErrStakeBelowMinimum + ErrStakeBelowMinimum = ErrorKind("ErrStakeBelowMinimum") // ErrNonstandardStakeTx indicates that a block contained a stake tx that // was not one of the allowed types of a stake transactions. - ErrNonstandardStakeTx + ErrNonstandardStakeTx = ErrorKind("ErrNonstandardStakeTx") // ErrNotEnoughVotes indicates that a block contained less than a majority // of voters. - ErrNotEnoughVotes + ErrNotEnoughVotes = ErrorKind("ErrNotEnoughVotes") // ErrTooManyVotes indicates that a block contained more than the maximum // allowable number of votes. - ErrTooManyVotes + ErrTooManyVotes = ErrorKind("ErrTooManyVotes") // ErrFreshStakeMismatch indicates that a block's header contained a different // number of SStx as compared to what was found in the block. - ErrFreshStakeMismatch + ErrFreshStakeMismatch = ErrorKind("ErrFreshStakeMismatch") // ErrTooManySStxs indicates that more than the allowed number of SStx was // found in a block. - ErrTooManySStxs + ErrTooManySStxs = ErrorKind("ErrTooManySStxs") // ErrInvalidEarlyStakeTx indicates that a tx type other than SStx was found // in the stake tx tree before the period when stake validation begins, or // before the stake tx type could possibly be included in the block. - ErrInvalidEarlyStakeTx + ErrInvalidEarlyStakeTx = ErrorKind("ErrInvalidEarlyStakeTx") // ErrTicketUnavailable indicates that a vote in the block spent a ticket // that could not be found. - ErrTicketUnavailable + ErrTicketUnavailable = ErrorKind("ErrTicketUnavailable") // ErrVotesOnWrongBlock indicates that an SSGen voted on a block not the // block's parent, and so was ineligible for inclusion into that block. - ErrVotesOnWrongBlock + ErrVotesOnWrongBlock = ErrorKind("ErrVotesOnWrongBlock") // ErrVotesMismatch indicates that the number of SSGen in the block was not // equivalent to the number of votes provided in the block header. - ErrVotesMismatch + ErrVotesMismatch = ErrorKind("ErrVotesMismatch") // ErrIncongruentVotebit indicates that the first votebit in votebits was not // the same as that determined by the majority of voters in the SSGen tx // included in the block. - ErrIncongruentVotebit + ErrIncongruentVotebit = ErrorKind("ErrIncongruentVotebit") // ErrInvalidSSRtx indicates than an SSRtx in a block could not be found to // have a valid missed sstx input as per the stake ticket database. - ErrInvalidSSRtx + ErrInvalidSSRtx = ErrorKind("ErrInvalidSSRtx") // ErrInvalidRevNum indicates that the number of revocations from the // header was not the same as the number of SSRtx included in the block. - ErrRevocationsMismatch + ErrRevocationsMismatch = ErrorKind("ErrRevocationsMismatch") // ErrTooManyRevocations indicates more revocations were found in a block // than were allowed. - ErrTooManyRevocations + ErrTooManyRevocations = ErrorKind("ErrTooManyRevocations") // ErrTicketCommitment indicates that a ticket commitment contains an amount // that does not coincide with the associated ticket input amount. - ErrTicketCommitment + ErrTicketCommitment = ErrorKind("ErrTicketCommitment") // ErrInvalidVoteInput indicates that an input to a vote transaction is // either not a stake ticket submission or is not a supported version. - ErrInvalidVoteInput + ErrInvalidVoteInput = ErrorKind("ErrInvalidVoteInput") // ErrBadNumPayees indicates that either a vote or revocation transaction // does not make the correct number of payments per the associated ticket // commitments. - ErrBadNumPayees + ErrBadNumPayees = ErrorKind("ErrBadNumPayees") // ErrBadPayeeScriptVersion indicates that either a vote or revocation // transaction output that corresponds to a ticket commitment does not use // a supported script version. - ErrBadPayeeScriptVersion + ErrBadPayeeScriptVersion = ErrorKind("ErrBadPayeeScriptVersion") // ErrBadPayeeScriptType indicates that either a vote or revocation // transaction output that corresponds to a ticket commitment does not pay // to the same script type required by the commitment. - ErrBadPayeeScriptType + ErrBadPayeeScriptType = ErrorKind("ErrBadPayeeScriptType") // ErrBadPayeeScriptType indicates that either a vote or revocation // transaction output that corresponds to a ticket commitment does not pay // to the hash required by the commitment. - ErrMismatchedPayeeHash + ErrMismatchedPayeeHash = ErrorKind("ErrMismatchedPayeeHash") // ErrBadPayeeValue indicates that either a vote or revocation transaction // output that corresponds to a ticket commitment does not pay the expected // amount required by the commitment. - ErrBadPayeeValue + ErrBadPayeeValue = ErrorKind("ErrBadPayeeValue") // ErrSSGenSubsidy indicates that there was an error in the amount of subsidy // generated in the vote. - ErrSSGenSubsidy + ErrSSGenSubsidy = ErrorKind("ErrSSGenSubsidy") // ErrImmatureTicketSpend indicates that a vote or revocation is attempting // to spend a ticket submission output that has not yet reached the required // maturity. - ErrImmatureTicketSpend + ErrImmatureTicketSpend = ErrorKind("ErrImmatureTicketSpend") // ErrTicketInputScript indicates that a ticket input is not one of the // supported script forms or versions. - ErrTicketInputScript + ErrTicketInputScript = ErrorKind("ErrTicketInputScript") // ErrInvalidRevokeInput indicates that an input to a revocation transaction // is either not a stake ticket submission or is not a supported version. - ErrInvalidRevokeInput + ErrInvalidRevokeInput = ErrorKind("ErrInvalidRevokeInput") // ErrSSRtxPayees indicates that the SSRtx failed to pay out to the committed // addresses or amounts from the originating SStx. - ErrSSRtxPayees + ErrSSRtxPayees = ErrorKind("ErrSSRtxPayees") // ErrTxSStxOutSpend indicates that a non SSGen or SSRtx tx attempted to spend // an OP_SSTX tagged output from an SStx. - ErrTxSStxOutSpend + ErrTxSStxOutSpend = ErrorKind("ErrTxSStxOutSpend") // ErrRegTxCreateStakeOut indicates that a regular tx attempted to create // a stake tagged output. - ErrRegTxCreateStakeOut + ErrRegTxCreateStakeOut = ErrorKind("ErrRegTxCreateStakeOut") // ErrInvalidFinalState indicates that the final state of the PRNG included // in the block differed from the calculated final state. - ErrInvalidFinalState + ErrInvalidFinalState = ErrorKind("ErrInvalidFinalState") // ErrPoolSize indicates an error in the ticket pool size for this block. - ErrPoolSize + ErrPoolSize = ErrorKind("ErrPoolSize") // ErrForceReorgWrongChain indicates that a reroganization was attempted // to be forced, but the chain indicated was not mirrored by b.bestChain. - ErrForceReorgWrongChain + ErrForceReorgWrongChain = ErrorKind("ErrForceReorgWrongChain") // ErrForceReorgMissingChild indicates that a reroganization was attempted // to be forced, but the child node to reorganize to could not be found. - ErrForceReorgMissingChild + ErrForceReorgMissingChild = ErrorKind("ErrForceReorgMissingChild") // ErrBadStakebaseValue indicates that a block's stake tx tree has spent // more than it is allowed. - ErrBadStakebaseValue + ErrBadStakebaseValue = ErrorKind("ErrBadStakebaseValue") // ErrDiscordantTxTree specifies that a given origin tx's content // indicated that it should exist in a different tx tree than the // one given in the TxIn outpoint. - ErrDiscordantTxTree + ErrDiscordantTxTree = ErrorKind("ErrDiscordantTxTree") // ErrStakeFees indicates an error with the fees found in the stake // transaction tree. - ErrStakeFees + ErrStakeFees = ErrorKind("ErrStakeFees") // ErrNoStakeTx indicates there were no stake transactions found in a // block after stake validation height. - ErrNoStakeTx + ErrNoStakeTx = ErrorKind("ErrNoStakeTx") // ErrBadBlockHeight indicates that a block header's embedded block height // was different from where it was actually embedded in the block chain. - ErrBadBlockHeight + ErrBadBlockHeight = ErrorKind("ErrBadBlockHeight") // ErrBlockOneTx indicates that block height 1 failed to correct generate // the block one initial payout transaction. - ErrBlockOneTx + ErrBlockOneTx = ErrorKind("ErrBlockOneTx") // ErrBlockOneTx indicates that block height 1 coinbase transaction in // zero was incorrect in some way. - ErrBlockOneInputs + ErrBlockOneInputs = ErrorKind("ErrBlockOneInputs") // ErrBlockOneOutputs indicates that block height 1 failed to incorporate // the ledger addresses correctly into the transaction's outputs. - ErrBlockOneOutputs + ErrBlockOneOutputs = ErrorKind("ErrBlockOneOutputs") // ErrNoTax indicates that there was no tax present in the coinbase of a // block after height 1. - ErrNoTax + ErrNoTax = ErrorKind("ErrNoTax") // ErrExpiredTx indicates that the transaction is currently expired. - ErrExpiredTx + ErrExpiredTx = ErrorKind("ErrExpiredTx") // ErrExpiryTxSpentEarly indicates that an output from a transaction // that included an expiry field was spent before coinbase maturity // many blocks had passed in the blockchain. - ErrExpiryTxSpentEarly + ErrExpiryTxSpentEarly = ErrorKind("ErrExpiryTxSpentEarly") // ErrFraudAmountIn indicates the witness amount given was fraudulent. - ErrFraudAmountIn + ErrFraudAmountIn = ErrorKind("ErrFraudAmountIn") // ErrFraudBlockHeight indicates the witness block height given was // fraudulent. - ErrFraudBlockHeight + ErrFraudBlockHeight = ErrorKind("ErrFraudBlockHeight") // ErrFraudBlockIndex indicates the witness block index given was // fraudulent. - ErrFraudBlockIndex + ErrFraudBlockIndex = ErrorKind("ErrFraudBlockIndex") // ErrZeroValueOutputSpend indicates that a transaction attempted to spend a // zero value output. - ErrZeroValueOutputSpend + ErrZeroValueOutputSpend = ErrorKind("ErrZeroValueOutputSpend") // ErrInvalidEarlyVoteBits indicates that a block before stake validation // height had an unallowed vote bits value. - ErrInvalidEarlyVoteBits + ErrInvalidEarlyVoteBits = ErrorKind("ErrInvalidEarlyVoteBits") // ErrInvalidEarlyFinalState indicates that a block before stake validation // height had a non-zero final state. - ErrInvalidEarlyFinalState + ErrInvalidEarlyFinalState = ErrorKind("ErrInvalidEarlyFinalState") // ErrKnownInvalidBlock indicates that this block has previously failed // validation. - ErrKnownInvalidBlock + ErrKnownInvalidBlock = ErrorKind("ErrKnownInvalidBlock") // ErrInvalidAncestorBlock indicates that an ancestor of this block has // failed validation. - ErrInvalidAncestorBlock + ErrInvalidAncestorBlock = ErrorKind("ErrInvalidAncestorBlock") // ErrInvalidTemplateParent indicates that a block template builds on a // block that is either not the current best chain tip or its parent. - ErrInvalidTemplateParent + ErrInvalidTemplateParent = ErrorKind("ErrInvalidTemplateParent") // ErrUnknownPiKey indicates that the provided public Pi Key is not // a well known key. - ErrUnknownPiKey + ErrUnknownPiKey = ErrorKind("ErrUnknownPiKey") // ErrInvalidPiSignature indicates that a treasury spend transaction // was not properly signed. - ErrInvalidPiSignature + ErrInvalidPiSignature = ErrorKind("ErrInvalidPiSignature") // ErrInvalidTVoteWindow indicates that a treasury spend transaction // appeared in a block that is prior to a valid treasury vote window. - ErrInvalidTVoteWindow + ErrInvalidTVoteWindow = ErrorKind("ErrInvalidTVoteWindow") // ErrNotTVI indicates that a treasury spend transaction appeared in a // block that is not at a TVI interval. - ErrNotTVI + ErrNotTVI = ErrorKind("ErrNotTVI") // ErrInvalidTSpendWindow indicates that this treasury spend // transaction is outside of the allowed window. - ErrInvalidTSpendWindow + ErrInvalidTSpendWindow = ErrorKind("ErrInvalidTSpendWindow") // ErrNotEnoughTSpendVotes indicates that a treasury spend transaction // does not have enough votes to be included in block. - ErrNotEnoughTSpendVotes + ErrNotEnoughTSpendVotes = ErrorKind("ErrNotEnoughTSpendVotes") // ErrInvalidTSpendValueIn indicates that a treasury spend transaction // ValueIn does not match the encoded copy in the first TxOut. - ErrInvalidTSpendValueIn + ErrInvalidTSpendValueIn = ErrorKind("ErrInvalidTSpendValueIn") // ErrTSpendExists indicates that a duplicate treasury spend // transaction has been mined on a TVI in the current best chain. - ErrTSpendExists + ErrTSpendExists = ErrorKind("ErrTSpendExists") // ErrInvalidExpenditure indicates that a treasury spend transaction // expenditure is out of range. - ErrInvalidExpenditure + ErrInvalidExpenditure = ErrorKind("ErrInvalidExpenditure") // ErrFirstTxNotTreasurybase indicates the first transaction in a block // is not a treasurybase transaction. - ErrFirstTxNotTreasurybase + ErrFirstTxNotTreasurybase = ErrorKind("ErrFirstTxNotTreasurybase") // ErrBadTreasurybaseOutpoint indicates that the outpoint used by a // treasurybase as input was non-null. - ErrBadTreasurybaseOutpoint + ErrBadTreasurybaseOutpoint = ErrorKind("ErrBadTreasurybaseOutpoint") // ErrBadTreasurybaseFraudProof indicates that the fraud proof for a // treasurybase input was non-null. - ErrBadTreasurybaseFraudProof + ErrBadTreasurybaseFraudProof = ErrorKind("ErrBadTreasurybaseFraudProof") // ErrBadTreasurybaseScriptLen indicates the length of the signature script // for a treasurybase transaction is not within the valid range. - ErrBadTreasurybaseScriptLen + ErrBadTreasurybaseScriptLen = ErrorKind("ErrBadTreasurybaseScriptLen") // ErrTreasurybaseTxNotOpReturn indicates the second ouptut of a // treasury base transaction is not an OP_RETURN. - ErrTreasurybaseTxNotOpReturn + ErrTreasurybaseTxNotOpReturn = ErrorKind("ErrTreasurybaseTxNotOpReturn") // ErrTreasurybaseHeight indicates that the encoded height in the // treasurybase is incorrect. - ErrTreasurybaseHeight + ErrTreasurybaseHeight = ErrorKind("ErrTreasurybaseHeight") // ErrInvalidTreasurybaseTxOutputs indicates that the transaction does // not have the correct number of outputs. - ErrInvalidTreasurybaseTxOutputs + ErrInvalidTreasurybaseTxOutputs = ErrorKind("ErrInvalidTreasurybaseTxOutputs") // ErrInvalidTreasurybaseVersion indicates that the transaction output // has the wrong version. - ErrInvalidTreasurybaseVersion + ErrInvalidTreasurybaseVersion = ErrorKind("ErrInvalidTreasurybaseVersion") // ErrInvalidTreasurybaseScript indicates that the transaction output // script is invalid. - ErrInvalidTreasurybaseScript + ErrInvalidTreasurybaseScript = ErrorKind("ErrInvalidTreasurybaseScript") // ErrTreasurybaseOutValue ensures that the OP_TADD value of a // treasurybase is not the expected amount. - ErrTreasurybaseOutValue + ErrTreasurybaseOutValue = ErrorKind("ErrTreasurybaseOutValue") // ErrMultipleTreasurybases indicates a block contains more than one // treasurybase transaction. - ErrMultipleTreasurybases + ErrMultipleTreasurybases = ErrorKind("ErrMultipleTreasurybases") // ErrBadTreasurybaseAmountIn indicates that a block contains an // invalid treasury contribution. - ErrBadTreasurybaseAmountIn + ErrBadTreasurybaseAmountIn = ErrorKind("ErrBadTreasurybaseAmountIn") // ErrBadTSpendOutpoint indicates that the outpoint used by a // treasury spend as input was non-null. - ErrBadTSpendOutpoint + ErrBadTSpendOutpoint = ErrorKind("ErrBadTSpendOutpoint") // ErrBadTSpendFraudProof indicates that the fraud proof for a treasury // spend transaction input was non-null. - ErrBadTSpendFraudProof + ErrBadTSpendFraudProof = ErrorKind("ErrBadTSpendFraudProof") // ErrBadTSpendScriptLen indicates the length of the signature script // for a treasury spend transaction is not within the valid range. - ErrBadTSpendScriptLen + ErrBadTSpendScriptLen = ErrorKind("ErrBadTSpendScriptLen") // ErrInvalidTAddChange indicates the change output of a TAdd is zero. - ErrInvalidTAddChange + ErrInvalidTAddChange = ErrorKind("ErrInvalidTAddChange") // ErrTooManyTAdds indicates the number of treasury adds in a given // block is larger than the maximum allowed. - ErrTooManyTAdds + ErrTooManyTAdds = ErrorKind("ErrTooManyTAdds") // ErrTicketExhaustion indicates extending a given block with another one // would result in an unrecoverable chain due to ticket exhaustion. - ErrTicketExhaustion - - // numErrorCodes is the maximum error code number used in tests. - numErrorCodes + ErrTicketExhaustion = ErrorKind("ErrTicketExhaustion") ) -// Map of ErrorCode values back to their constant names for pretty printing. -var errorCodeStrings = map[ErrorCode]string{ - ErrDuplicateBlock: "ErrDuplicateBlock", - ErrMissingParent: "ErrMissingParent", - ErrBlockTooBig: "ErrBlockTooBig", - ErrWrongBlockSize: "ErrWrongBlockSize", - ErrBlockVersionTooOld: "ErrBlockVersionTooOld", - ErrBadStakeVersion: "ErrBadStakeVersion", - ErrInvalidTime: "ErrInvalidTime", - ErrTimeTooOld: "ErrTimeTooOld", - ErrTimeTooNew: "ErrTimeTooNew", - ErrDifficultyTooLow: "ErrDifficultyTooLow", - ErrUnexpectedDifficulty: "ErrUnexpectedDifficulty", - ErrHighHash: "ErrHighHash", - ErrBadMerkleRoot: "ErrBadMerkleRoot", - ErrBadCommitmentRoot: "ErrBadCommitmentRoot", - ErrBadCheckpoint: "ErrBadCheckpoint", - ErrForkTooOld: "ErrForkTooOld", - ErrCheckpointTimeTooOld: "ErrCheckpointTimeTooOld", - ErrNoTransactions: "ErrNoTransactions", - ErrTooManyTransactions: "ErrTooManyTransactions", - ErrNoTxInputs: "ErrNoTxInputs", - ErrNoTxOutputs: "ErrNoTxOutputs", - ErrInvalidTxOutputs: "ErrInvalidTxOutputs", - ErrTxTooBig: "ErrTxTooBig", - ErrBadTxOutValue: "ErrBadTxOutValue", - ErrDuplicateTxInputs: "ErrDuplicateTxInputs", - ErrBadTxInput: "ErrBadTxInput", - ErrMissingTxOut: "ErrMissingTxOut", - ErrUnfinalizedTx: "ErrUnfinalizedTx", - ErrDuplicateTx: "ErrDuplicateTx", - ErrOverwriteTx: "ErrOverwriteTx", - ErrImmatureSpend: "ErrImmatureSpend", - ErrSpendTooHigh: "ErrSpendTooHigh", - ErrBadFees: "ErrBadFees", - ErrTooManySigOps: "ErrTooManySigOps", - ErrFirstTxNotCoinbase: "ErrFirstTxNotCoinbase", - ErrFirstTxNotOpReturn: "ErrFirstTxNotOpReturn", - ErrCoinbaseHeight: "ErrCoinbaseHeight", - ErrMultipleCoinbases: "ErrMultipleCoinbases", - ErrStakeTxInRegularTree: "ErrStakeTxInRegularTree", - ErrRegTxInStakeTree: "ErrRegTxInStakeTree", - ErrBadCoinbaseScriptLen: "ErrBadCoinbaseScriptLen", - ErrBadCoinbaseValue: "ErrBadCoinbaseValue", - ErrBadCoinbaseOutpoint: "ErrBadCoinbaseOutpoint", - ErrBadCoinbaseFraudProof: "ErrBadCoinbaseFraudProof", - ErrBadCoinbaseAmountIn: "ErrBadCoinbaseAmountIn", - ErrBadStakebaseAmountIn: "ErrBadStakebaseAmountIn", - ErrBadStakebaseScriptLen: "ErrBadStakebaseScriptLen", - ErrBadStakebaseScrVal: "ErrBadStakebaseScrVal", - ErrScriptMalformed: "ErrScriptMalformed", - ErrScriptValidation: "ErrScriptValidation", - ErrNotEnoughStake: "ErrNotEnoughStake", - ErrStakeBelowMinimum: "ErrStakeBelowMinimum", - ErrNonstandardStakeTx: "ErrNonstandardStakeTx", - ErrNotEnoughVotes: "ErrNotEnoughVotes", - ErrTooManyVotes: "ErrTooManyVotes", - ErrFreshStakeMismatch: "ErrFreshStakeMismatch", - ErrTooManySStxs: "ErrTooManySStxs", - ErrInvalidEarlyStakeTx: "ErrInvalidEarlyStakeTx", - ErrTicketUnavailable: "ErrTicketUnavailable", - ErrVotesOnWrongBlock: "ErrVotesOnWrongBlock", - ErrVotesMismatch: "ErrVotesMismatch", - ErrIncongruentVotebit: "ErrIncongruentVotebit", - ErrInvalidSSRtx: "ErrInvalidSSRtx", - ErrRevocationsMismatch: "ErrRevocationsMismatch", - ErrTooManyRevocations: "ErrTooManyRevocations", - ErrTicketCommitment: "ErrTicketCommitment", - ErrInvalidVoteInput: "ErrInvalidVoteInput", - ErrBadNumPayees: "ErrBadNumPayees", - ErrBadPayeeScriptVersion: "ErrBadPayeeScriptVersion", - ErrBadPayeeScriptType: "ErrBadPayeeScriptType", - ErrMismatchedPayeeHash: "ErrMismatchedPayeeHash", - ErrBadPayeeValue: "ErrBadPayeeValue", - ErrSSGenSubsidy: "ErrSSGenSubsidy", - ErrImmatureTicketSpend: "ErrImmatureTicketSpend", - ErrTicketInputScript: "ErrTicketInputScript", - ErrInvalidRevokeInput: "ErrInvalidRevokeInput", - ErrSSRtxPayees: "ErrSSRtxPayees", - ErrTxSStxOutSpend: "ErrTxSStxOutSpend", - ErrRegTxCreateStakeOut: "ErrRegTxCreateStakeOut", - ErrInvalidFinalState: "ErrInvalidFinalState", - ErrPoolSize: "ErrPoolSize", - ErrForceReorgWrongChain: "ErrForceReorgWrongChain", - ErrForceReorgMissingChild: "ErrForceReorgMissingChild", - ErrBadStakebaseValue: "ErrBadStakebaseValue", - ErrDiscordantTxTree: "ErrDiscordantTxTree", - ErrStakeFees: "ErrStakeFees", - ErrNoStakeTx: "ErrNoStakeTx", - ErrBadBlockHeight: "ErrBadBlockHeight", - ErrBlockOneTx: "ErrBlockOneTx", - ErrBlockOneInputs: "ErrBlockOneInputs", - ErrBlockOneOutputs: "ErrBlockOneOutputs", - ErrNoTax: "ErrNoTax", - ErrExpiredTx: "ErrExpiredTx", - ErrExpiryTxSpentEarly: "ErrExpiryTxSpentEarly", - ErrFraudAmountIn: "ErrFraudAmountIn", - ErrFraudBlockHeight: "ErrFraudBlockHeight", - ErrFraudBlockIndex: "ErrFraudBlockIndex", - ErrZeroValueOutputSpend: "ErrZeroValueOutputSpend", - ErrInvalidEarlyVoteBits: "ErrInvalidEarlyVoteBits", - ErrInvalidEarlyFinalState: "ErrInvalidEarlyFinalState", - ErrKnownInvalidBlock: "ErrKnownInvalidBlock", - ErrInvalidAncestorBlock: "ErrInvalidAncestorBlock", - ErrUnknownPiKey: "ErrUnknownPiKey", - ErrInvalidPiSignature: "ErrInvalidPiSignature", - ErrInvalidTVoteWindow: "ErrInvalidTVoteWindow", - ErrNotTVI: "ErrNotTVI", - ErrInvalidTSpendWindow: "ErrInvalidTSpendWindow", - ErrNotEnoughTSpendVotes: "ErrNotEnoughTSpendVotes", - ErrInvalidTSpendValueIn: "ErrInvalidTSpendValueIn", - ErrTSpendExists: "ErrTSpendExists", - ErrInvalidExpenditure: "ErrInvalidExpenditure", - ErrFirstTxNotTreasurybase: "ErrFirstTxNotTreasurybase", - ErrBadTreasurybaseOutpoint: "ErrBadTreasurybaseOutpoint", - ErrBadTreasurybaseFraudProof: "ErrBadTreasurybaseFraudProof", - ErrBadTreasurybaseScriptLen: "ErrBadTreasurybaseScriptLen", - ErrTreasurybaseTxNotOpReturn: "ErrTreasurybaseTxNotOpReturn", - ErrTreasurybaseHeight: "ErrTreasurybaseHeight", - ErrTreasurybaseOutValue: "ErrTreasurybaseOutValue", - ErrInvalidTreasurybaseTxOutputs: "ErrInvalidTreasurybaseTxOutputs", - ErrInvalidTreasurybaseVersion: "ErrInvalidTreasurybaseVersion", - ErrInvalidTreasurybaseScript: "ErrInvalidTreasurybaseScript", - ErrMultipleTreasurybases: "ErrMultipleTreasurybases", - ErrBadTreasurybaseAmountIn: "ErrBadTreasurybaseAmountIn", - ErrBadTSpendOutpoint: "ErrBadTSpendOutpoint", - ErrBadTSpendFraudProof: "ErrBadTSpendFraudProof", - ErrBadTSpendScriptLen: "ErrBadTSpendScriptLen", - ErrInvalidTemplateParent: "ErrInvalidTemplateParent", - ErrInvalidTAddChange: "ErrInvalidTAddChange", - ErrTooManyTAdds: "ErrTooManyTAdds", - ErrTicketExhaustion: "ErrTicketExhaustion", -} - -// String returns the ErrorCode as a human-readable name. -func (e ErrorCode) String() string { - if s := errorCodeStrings[e]; s != "" { - return s - } - return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) +// Error satisfies the error interface and prints human-readable errors. +func (e ErrorKind) Error() string { + return string(e) } -// RuleError identifies a rule violation. It is used to indicate that -// processing of a block or 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. +// RuleError identifies a rule violation related to blocks. It has +// full support for errors.Is and errors.As, so the caller can ascertain the +// specific reason for the error by checking the underlying error. type RuleError struct { - ErrorCode ErrorCode // Describes the kind of error - Description string // Human readable description of the issue + Err error + Description string } // Error satisfies the error interface and prints human-readable errors. @@ -797,14 +656,12 @@ func (e RuleError) Error() string { return e.Description } -// ruleError creates a RuleError given a set of arguments. -func ruleError(c ErrorCode, desc string) RuleError { - return RuleError{ErrorCode: c, Description: desc} +// Unwrap returns the underlying wrapped error. +func (e RuleError) Unwrap() error { + return e.Err } -// IsErrorCode returns whether or not the provided error is a rule error with -// the provided error code. -func IsErrorCode(err error, c ErrorCode) bool { - var e RuleError - return errors.As(err, &e) && e.ErrorCode == c +// ruleError creates a RuleError given a set of arguments. +func ruleError(kind ErrorKind, desc string) RuleError { + return RuleError{Err: kind, Description: desc} } diff --git a/blockchain/error_test.go b/blockchain/error_test.go index c9b6993e51..8100da2a4e 100644 --- a/blockchain/error_test.go +++ b/blockchain/error_test.go @@ -1,18 +1,20 @@ // Copyright (c) 2014 The btcsuite developers -// Copyright (c) 2015-2018 The Decred developers +// Copyright (c) 2015-2020 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package blockchain import ( + "errors" + "io" "testing" ) -// TestErrorCodeStringer tests the stringized output for the ErrorCode type. -func TestErrorCodeStringer(t *testing.T) { +// TestErrorKindStringer tests the stringized output for the ErrorKind type. +func TestErrorKindStringer(t *testing.T) { tests := []struct { - in ErrorCode + in ErrorKind want string }{ {ErrDuplicateBlock, "ErrDuplicateBlock"}, @@ -145,20 +147,13 @@ func TestErrorCodeStringer(t *testing.T) { {ErrInvalidTAddChange, "ErrInvalidTAddChange"}, {ErrTooManyTAdds, "ErrTooManyTAdds"}, {ErrTicketExhaustion, "ErrTicketExhaustion"}, - {0xffff, "Unknown ErrorCode (65535)"}, - } - - // Detect additional error codes that don't have the stringer added. - if len(tests)-1 != int(numErrorCodes) { - t.Errorf("It appears an error code was added without adding an " + - "associated stringer test") } t.Logf("Running %d tests", len(tests)) for i, test := range tests { - result := test.in.String() + result := test.in.Error() if result != test.want { - t.Errorf("String #%d\n got: %s want: %s", i, result, + t.Errorf("#%d: got: %s want: %s", i, result, test.want) continue } @@ -185,51 +180,91 @@ func TestRuleError(t *testing.T) { for i, test := range tests { result := test.in.Error() if result != test.want { - t.Errorf("Error #%d\n got: %s want: %s", i, result, + t.Errorf("Error #%d: got: %s want: %s", i, result, test.want) continue } } } -// TestIsErrorCode ensures IsErrorCode works as intended. -func TestIsErrorCode(t *testing.T) { +// TestErrorKindIsAs ensures both ErrorKind and Error can be identified as being +// a specific error kind via errors.Is and unwrapped via errors.As. +func TestErrorKindIsAs(t *testing.T) { tests := []struct { - name string - err error - code ErrorCode - want bool + name string + err error + target error + wantMatch bool + wantAs ErrorKind }{{ - name: "ErrUnexpectedDifficulty testing for ErrUnexpectedDifficulty", - err: ruleError(ErrUnexpectedDifficulty, ""), - code: ErrUnexpectedDifficulty, - want: true, + name: "ErrDuplicateBlock == ErrDuplicateBlock", + err: ErrDuplicateBlock, + target: ErrDuplicateBlock, + wantMatch: true, + wantAs: ErrDuplicateBlock, + }, { + name: "RuleError.ErrDuplicateBlock == ErrDuplicateBlock", + err: ruleError(ErrDuplicateBlock, ""), + target: ErrDuplicateBlock, + wantMatch: true, + wantAs: ErrDuplicateBlock, }, { - name: "ErrHighHash testing for ErrHighHash", - err: ruleError(ErrHighHash, ""), - code: ErrHighHash, - want: true, + name: "RuleError.ErrDuplicateBlock == RuleError.ErrDuplicateBlock", + err: ruleError(ErrDuplicateBlock, ""), + target: ruleError(ErrDuplicateBlock, ""), + wantMatch: true, + wantAs: ErrDuplicateBlock, }, { - name: "ErrHighHash error testing for ErrUnexpectedDifficulty", - err: ruleError(ErrHighHash, ""), - code: ErrUnexpectedDifficulty, - want: false, + name: "ErrDuplicateBlock != ErrMissingParent", + err: ErrDuplicateBlock, + target: ErrMissingParent, + wantMatch: false, + wantAs: ErrDuplicateBlock, }, { - name: "ErrHighHash error testing for unknown error code", - err: ruleError(ErrHighHash, ""), - code: 0xffff, - want: false, + name: "RuleError.ErrDuplicateBlock != ErrMissingParent", + err: ruleError(ErrDuplicateBlock, ""), + target: ErrMissingParent, + wantMatch: false, + wantAs: ErrDuplicateBlock, }, { - name: "nil error testing for ErrUnexpectedDifficulty", - err: nil, - code: ErrUnexpectedDifficulty, - want: false, + name: "ErrDuplicateBlock != RuleError.ErrMissingParent", + err: ErrDuplicateBlock, + target: ruleError(ErrMissingParent, ""), + wantMatch: false, + wantAs: ErrDuplicateBlock, + }, { + name: "RuleError.ErrDuplicateBlock != RuleError.ErrMissingParent", + err: ruleError(ErrDuplicateBlock, ""), + target: ruleError(ErrMissingParent, ""), + wantMatch: false, + wantAs: ErrDuplicateBlock, + }, { + name: "RuleError.ErrDuplicateBlock != io.EOF", + err: ruleError(ErrDuplicateBlock, ""), + target: io.EOF, + wantMatch: false, + wantAs: ErrDuplicateBlock, }} + for _, test := range tests { - result := IsErrorCode(test.err, test.code) - if result != test.want { - t.Errorf("%s: unexpected result -- got: %v want: %v", test.name, - result, test.want) + // Ensure the error matches or not depending on the expected result. + result := errors.Is(test.err, test.target) + if result != test.wantMatch { + t.Errorf("%s: incorrect error identification -- got %v, want %v", + test.name, result, test.wantMatch) + continue + } + + // Ensure the underlying error kind can be unwrapped is and is the + // expected kind. + var kind ErrorKind + if !errors.As(test.err, &kind) { + t.Errorf("%s: unable to unwrap to error kind", test.name) + continue + } + if kind != test.wantAs { + t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v", + test.name, kind, test.wantAs) continue } } diff --git a/blockchain/fullblocks_test.go b/blockchain/fullblocks_test.go index 5a422a13f1..18890971db 100644 --- a/blockchain/fullblocks_test.go +++ b/blockchain/fullblocks_test.go @@ -149,7 +149,7 @@ func TestFullBlocks(t *testing.T) { var isOrphan bool forkLen, err := chain.ProcessBlock(block, blockchain.BFNone) - if blockchain.IsErrorCode(err, blockchain.ErrMissingParent) { + if errors.Is(err, blockchain.ErrMissingParent) { isOrphan = true err = nil } @@ -194,18 +194,10 @@ func TestFullBlocks(t *testing.T) { // Ensure the error code is of the expected type and the reject // code matches the value specified in the test instance. - var rerr blockchain.RuleError - if !errors.As(err, &rerr) { - t.Fatalf("block %q (hash %s, height %d) returned "+ - "unexpected error type -- got %T, want "+ - "blockchain.RuleError", item.Name, block.Hash(), - blockHeight, err) - } - if rerr.ErrorCode != item.RejectCode { + if !errors.Is(err, item.RejectKind) { t.Fatalf("block %q (hash %s, height %d) does not have "+ "expected reject code -- got %v, want %v", - item.Name, block.Hash(), blockHeight, - rerr.ErrorCode, item.RejectCode) + item.Name, block.Hash(), blockHeight, err, item.RejectKind) } } diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go index 1f50cfb6b4..88ad3819e4 100644 --- a/blockchain/fullblocktests/generate.go +++ b/blockchain/fullblocktests/generate.go @@ -93,7 +93,7 @@ func (b AcceptedBlock) FullBlockTestInstance() {} type RejectedBlock struct { Name string Block *wire.MsgBlock - RejectCode blockchain.ErrorCode + RejectKind blockchain.ErrorKind } // Ensure RejectedBlock implements the TestInstance interface. @@ -402,8 +402,8 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { acceptBlock := func(blockName string, block *wire.MsgBlock, isMainChain, isOrphan bool) TestInstance { return AcceptedBlock{blockName, block, isMainChain, isOrphan} } - rejectBlock := func(blockName string, block *wire.MsgBlock, code blockchain.ErrorCode) TestInstance { - return RejectedBlock{blockName, block, code} + rejectBlock := func(blockName string, block *wire.MsgBlock, kind blockchain.ErrorKind) TestInstance { + return RejectedBlock{blockName, block, kind} } rejectNonCanonicalBlock := func(blockName string, block *wire.MsgBlock) TestInstance { blockHeight := int32(block.Header.Height) @@ -449,9 +449,9 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { expectTipBlock(tipName, g.BlockByName(tipName)), }) } - rejected := func(code blockchain.ErrorCode) { + rejected := func(kind blockchain.ErrorKind) { tests = append(tests, []TestInstance{ - rejectBlock(g.TipName(), g.Tip(), code), + rejectBlock(g.TipName(), g.Tip(), kind), }) } rejectedNonCanonical := func() { diff --git a/blockchain/validate_test.go b/blockchain/validate_test.go index 90fb6f4477..9bc26f8c5c 100644 --- a/blockchain/validate_test.go +++ b/blockchain/validate_test.go @@ -602,15 +602,10 @@ func TestTxValidationErrors(t *testing.T) { // Ensure transaction is rejected due to being too large. err := CheckTransactionSanity(tx, chaincfg.MainNetParams(), noTreasury) - var rerr RuleError - if !errors.As(err, &rerr) { - t.Fatalf("CheckTransactionSanity: unexpected error type for "+ - "transaction that is too large -- got %T", err) - } - if rerr.ErrorCode != ErrTxTooBig { + if !errors.Is(err, ErrTxTooBig) { t.Fatalf("CheckTransactionSanity: unexpected error code for "+ "transaction that is too large -- got %v, want %v", - rerr.ErrorCode, ErrTxTooBig) + err, ErrTxTooBig) } } @@ -667,7 +662,7 @@ func TestCheckConnectBlockTemplate(t *testing.T) { block.Hash(), blockHeight, err) } } - rejectedBlockTemplate := func(code ErrorCode) { + rejectedBlockTemplate := func(kind ErrorKind) { msgBlock := g.Tip() blockHeight := msgBlock.Header.Height block := dcrutil.NewBlock(msgBlock) @@ -683,18 +678,10 @@ func TestCheckConnectBlockTemplate(t *testing.T) { // Ensure the error code is of the expected type and the reject // code matches the value specified in the test instance. - var rerr RuleError - if !errors.As(err, &rerr) { - t.Fatalf("block template %q (hash %s, height %d) "+ - "returned unexpected error type -- got %T, want "+ - "blockchain.RuleError", g.TipName(), - block.Hash(), blockHeight, err) - } - if rerr.ErrorCode != code { + if !errors.Is(err, kind) { t.Fatalf("block template %q (hash %s, height %d) does "+ "not have expected reject code -- got %v, want %v", - g.TipName(), block.Hash(), blockHeight, - rerr.ErrorCode, code) + g.TipName(), block.Hash(), blockHeight, err, kind) } } @@ -1054,16 +1041,6 @@ func TestCheckTicketExhaustion(t *testing.T) { params.StakeEnabledHeight = stakeEnabledHeight params.StakeValidationHeight = stakeValidationHeight - // isErr is a convenience func which acts as a limited version of errors.Is - // until the package is converted to support it at which point this can be - // removed. - isErr := func(err error, target error) bool { - if (err == nil) != (target == nil) { - return false - } - return target == nil || IsErrorCode(err, target.(RuleError).ErrorCode) - } - // ticketInfo is used to control the tests by specifying the details about // how many fake blocks to create with the specified number of tickets. type ticketInfo struct { @@ -1085,7 +1062,7 @@ func TestCheckTicketExhaustion(t *testing.T) { {126, 0}, // height: 126, 0 live, 0 immature }, newBlockTix: 0, // extending height: 126, 0 live, 0 immature - err: ruleError(ErrTicketExhaustion, ""), + err: ErrTicketExhaustion, }, { // Reach inevitable ticket exhaustion by not including any ticket // purchases up to just before the final possible block prior to svh @@ -1096,7 +1073,7 @@ func TestCheckTicketExhaustion(t *testing.T) { {126, 0}, // height: 126, 0 live, 0 immature }, newBlockTix: 4, // extending height: 126, 0 live, 4 immature - err: ruleError(ErrTicketExhaustion, ""), + err: ErrTicketExhaustion, }, { // Construct chain such that there are no ticket purchases up to just // before the final possible block prior to svh that can prevent ticket @@ -1119,7 +1096,7 @@ func TestCheckTicketExhaustion(t *testing.T) { {109, 0}, // height: 126, 4 live, 0 immature }, newBlockTix: 0, // extending height: 126, 4 live, 0 immature - err: ruleError(ErrTicketExhaustion, ""), + err: ErrTicketExhaustion, }, { name: "just enough live tickets at 1st possible exhaustion", ticketInfo: []ticketInfo{ @@ -1140,7 +1117,7 @@ func TestCheckTicketExhaustion(t *testing.T) { {110, 0}, // height: 127, 5 live, 0 immature }, newBlockTix: 0, // extending height: 127, 5 live, 0 immature - err: ruleError(ErrTicketExhaustion, ""), + err: ErrTicketExhaustion, }, { // Reach inevitable ticket exhaustion in the second possible block that // it can happen with one live ticket less than needed to prevent it. @@ -1151,7 +1128,7 @@ func TestCheckTicketExhaustion(t *testing.T) { {110, 0}, // height: 127, 9 live, 0 immature }, newBlockTix: 0, // extending height: 127, 9 live, 0 immature - err: ruleError(ErrTicketExhaustion, ""), + err: ErrTicketExhaustion, }, { // Construct chain to one block before svh such that there are exactly // enough live tickets to prevent exhaustion. @@ -1212,7 +1189,7 @@ func TestCheckTicketExhaustion(t *testing.T) { {75, 0}, // height: 226, 85 live, 0 immature }, newBlockTix: 0, // extending height: 226, 85 live, 0 immature - err: ruleError(ErrTicketExhaustion, ""), + err: ErrTicketExhaustion, }} for _, test := range tests { @@ -1261,7 +1238,7 @@ func TestCheckTicketExhaustion(t *testing.T) { // Ensure the expected result is returned from ticket exhaustion check. err := bc.CheckTicketExhaustion(&node.hash, test.newBlockTix) - if !isErr(err, test.err) { + if !errors.Is(err, test.err) { t.Errorf("%q: mismatched err -- got %v, want %v", test.name, err, test.err) continue diff --git a/blockmanager.go b/blockmanager.go index 017ac44b6b..c723346781 100644 --- a/blockmanager.go +++ b/blockmanager.go @@ -662,76 +662,63 @@ func (b *blockManager) handleDonePeerMsg(peer *peerpkg.Peer) { // 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. - var rerr mempool.RuleError - if errors.As(err, &rerr) { - err = rerr.Err - } - // The default reason to reject a transaction/block is due to it being // invalid somehow. code := wire.RejectInvalid var reason string - var berr blockchain.RuleError - var terr mempool.TxRuleError + // Convert recognized errors to a reject code. switch { - case errors.As(err, &berr): - // Convert the chain error to a reject code. - switch berr.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 = berr.Error() - case errors.As(err, &terr): - switch terr.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 = terr.Error() + // Rejected due to duplicate. + case errors.Is(err, blockchain.ErrDuplicateBlock): + code = wire.RejectDuplicate + reason = err.Error() + + // Rejected due to obsolete version. + case errors.Is(err, blockchain.ErrBlockVersionTooOld): + code = wire.RejectObsolete + reason = err.Error() + + // Rejected due to checkpoint. + case errors.Is(err, blockchain.ErrCheckpointTimeTooOld), + errors.Is(err, blockchain.ErrDifficultyTooLow), + errors.Is(err, blockchain.ErrBadCheckpoint), + errors.Is(err, blockchain.ErrForkTooOld): + code = wire.RejectCheckpoint + reason = err.Error() + + // Error codes which map to a duplicate transaction already + // mined or in the mempool. + case errors.Is(err, mempool.ErrMempoolDoubleSpend), + errors.Is(err, mempool.ErrAlreadyVoted), + errors.Is(err, mempool.ErrDuplicate), + errors.Is(err, mempool.ErrTooManyVotes), + errors.Is(err, mempool.ErrDuplicateRevocation), + errors.Is(err, mempool.ErrAlreadyExists), + errors.Is(err, mempool.ErrOrphan): + code = wire.RejectDuplicate + reason = err.Error() + + // Error codes which map to a non-standard transaction being + // relayed. + case errors.Is(err, mempool.ErrOrphanPolicyViolation), + errors.Is(err, mempool.ErrOldVote), + errors.Is(err, mempool.ErrSeqLockUnmet), + errors.Is(err, mempool.ErrNonStandard): + code = wire.RejectNonstandard + reason = err.Error() + + // Error codes which map to an insufficient fee being paid. + case errors.Is(err, mempool.ErrInsufficientFee), + errors.Is(err, mempool.ErrInsufficientPriority): + code = wire.RejectInsufficientFee + reason = err.Error() + + // Error codes which map to an attempt to create dust outputs. + case errors.Is(err, mempool.ErrDustOutput): + code = wire.RejectDust + reason = err.Error() + default: reason = fmt.Sprintf("rejected: %v", err) } @@ -1005,7 +992,7 @@ func (b *blockManager) processBlockAndOrphans(block *dcrutil.Block, flags blockc // returned indicates the block is an orphan. blockHash := block.Hash() forkLen, err := b.cfg.Chain.ProcessBlock(block, flags) - if blockchain.IsErrorCode(err, blockchain.ErrMissingParent) { + if errors.Is(err, blockchain.ErrMissingParent) { bmgrLog.Infof("Adding orphan block %v with parent %v", blockHash, block.MsgBlock().Header.PrevBlock) b.addOrphanBlock(block) @@ -1846,26 +1833,16 @@ func headerApprovesParent(header *wire.BlockHeader) bool { // is expected to have come from mempool, indicates a transaction was rejected // either due to containing a double spend or already existing in the pool. func isDoubleSpendOrDuplicateError(err error) bool { - var merr mempool.RuleError - if !errors.As(err, &merr) { - return false - } - - var rerr mempool.TxRuleError - if errors.As(merr.Err, &rerr) { - switch rerr.ErrorCode { - case mempool.ErrDuplicate: - return true - case mempool.ErrAlreadyExists: - return true - default: - return false - } + switch { + case errors.Is(err, mempool.ErrDuplicate): + return true + case errors.Is(err, mempool.ErrAlreadyExists): + return true + case errors.Is(err, blockchain.ErrMissingTxOut): + return true } - var cerr blockchain.RuleError - return errors.As(merr.Err, &cerr) && - cerr.ErrorCode == blockchain.ErrMissingTxOut + return false } // handleBlockchainNotification handles notifications from blockchain. It does diff --git a/cmd/addblock/import.go b/cmd/addblock/import.go index fa5c4cb57a..74ac43ab45 100644 --- a/cmd/addblock/import.go +++ b/cmd/addblock/import.go @@ -8,6 +8,7 @@ package main import ( "context" "encoding/binary" + "errors" "fmt" "io" "sync" @@ -129,7 +130,7 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) { // known checkpoints. forkLen, err := bi.chain.ProcessBlock(block, blockchain.BFFastAdd) if err != nil { - if blockchain.IsErrorCode(err, blockchain.ErrMissingParent) { + if errors.Is(err, blockchain.ErrMissingParent) { return false, fmt.Errorf("import file contains an orphan block: %v", blockHash) } diff --git a/internal/mempool/error.go b/internal/mempool/error.go index 114df6a7c3..11186eab7d 100644 --- a/internal/mempool/error.go +++ b/internal/mempool/error.go @@ -12,116 +12,132 @@ import ( "github.com/decred/dcrd/blockchain/v3" ) -// RuleError 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 use the Err field to access the -// underlying error, which will be either a TxRuleError or a -// blockchain.RuleError. -type RuleError struct { - Err error -} +// ErrorKind identifies a kind of error. It has full support for errors.Is and +// errors.As, so the caller can directly check against an error kind when +// determining the reason for an error. +type ErrorKind string -// Error satisfies the error interface and prints human-readable errors. -func (e RuleError) Error() string { - if e.Err == nil { - return "" - } - return e.Err.Error() -} +const ( + // ErrorInvalid indicates a mempool transaction is invalid per consensus. + ErrInvalid = ErrorKind("ErrInvalid") -// ErrorCode identifies the kind of error. -type ErrorCode int + // ErrorOrphanPolicyViolation indicates that an orphan block violates the + // prevailing orphan policy. + ErrOrphanPolicyViolation = ErrorKind("ErrOrphanPolicyViolation") -const ( - ErrOther ErrorCode = iota - ErrInvalid - ErrOrphanPolicyViolation - ErrMempoolDoubleSpend - ErrAlreadyVoted - ErrDuplicate - ErrCoinbase - ErrExpired - ErrNonStandard - ErrDustOutput - ErrInsufficientFee - ErrTooManyVotes - ErrDuplicateRevocation - ErrOldVote - ErrAlreadyExists - ErrSeqLockUnmet - ErrInsufficientPriority - ErrFeeTooHigh - ErrOrphan - ErrTooManyTSpends - ErrTSpendMinedOnAncestor - ErrTSpendInvalidExpiry + // ErrMempoolDoubleSpend indicates a transaction that attempts to to spend + // coins already spent by other transactions in the pool. + ErrMempoolDoubleSpend = ErrorKind("ErrMempoolDoubleSpend") + + // ErrAlreadyVoted indicates a ticket already voted. + ErrAlreadyVoted = ErrorKind("ErrorAlreadyVoted") + + // ErrDuplicate indicates a transaction already exists in the mempool. + ErrDuplicate = ErrorKind("ErrDuplicate") + + // ErrCoinbase indicates a transaction is a standalone coinbase transaction. + ErrCoinbase = ErrorKind("ErrCoinbase") + + // ErrExpired indicates a transaction will be expired as of the next block. + ErrExpired = ErrorKind("ErrExpired") + + // ErrNonStandard indicates a non-standard transaction. + ErrNonStandard = ErrorKind("ErrNonStandard") + + // ErrDustOutput indicates a transaction has one or more dust outputs. + ErrDustOutput = ErrorKind("ErrDustOutput") + + // ErrInsufficientFee indicates a transaction cannot does not pay the minimum + // fee required by the active policy. + ErrInsufficientFee = ErrorKind("ErrInsufficientFee") + + // ErrTooManyVotes indicates the number of vote double spends exceeds the + // maximum allowed. + ErrTooManyVotes = ErrorKind("ErrTooManyVotes") + + // ErrDuplicateRevocation indicates a revocation already exists in the + // mempool. + ErrDuplicateRevocation = ErrorKind("ErrDuplicateRevocation") + + // ErrOldVote indicates a ticket votes on a block height lower than + // the minimum allowed by the mempool. + ErrOldVote = ErrorKind("ErrOldVote") + + // ErrAlreadyExists indicates a transaction already exists on the + // main chain and is not fully spent. + ErrAlreadyExists = ErrorKind("ErrAlreadyExists") + + // ErrSeqLockUnmet indicates a transaction sequence locks are not active. + ErrSeqLockUnmet = ErrorKind("ErrSeqLockUnmet") + + // ErrInsufficientPriority indicates a non-stake transaction has a + // priority lower than the minimum allowed. + ErrInsufficientPriority = ErrorKind("ErrInsufficientPriority") + + // ErrFeeTooHigh indicates a transaction pays fees above the maximum + // allowed by the active policy. + ErrFeeTooHigh = ErrorKind("ErrFeeTooHigh") + + // ErrOrphan indicates a transaction is an orphan. + ErrOrphan = ErrorKind("ErrOrphan") + + // ErrTooManyTSpends indicates the number of treasury spend hashes exceeds + // the maximum allowed. + ErrTooManyTSpends = ErrorKind("ErrTooManyTSpends") + + // ErrTSpendMinedOnAncestor indicates a referenced treasury spend was + // already mined on an ancestor block. + ErrTSpendMinedOnAncestor = ErrorKind("ErrTSpendMinedOnAncestor") + + // ErrTSpendInvalidExpiry indicates a treasury spend expiry is invalid. + ErrTSpendInvalidExpiry = ErrorKind("ErrTSpendInvalidExpiry") ) -// TxRuleError identifies a rule violation. It is used to indicate that +// Error satisfies the error interface and prints human-readable errors. +func (e ErrorKind) Error() string { + return string(e) +} + +// RuleError 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 { - // ErrorCode is the mempool package error code ID. - ErrorCode ErrorCode - - // Description is an additional human readable description of the - // error. +// rules. It has full support for errors.Is and errors.As, so the caller +// can ascertain the specific reason for the error by checking the +// underlying error, which will be either an ErrorKind or blockchain.RuleError. +type RuleError struct { + Err error Description string } // Error satisfies the error interface and prints human-readable errors. -func (e TxRuleError) Error() string { +func (e RuleError) Error() string { return e.Description } -// txRuleError creates an underlying TxRuleError with the given a set of -// arguments and returns a RuleError that encapsulates it. -func txRuleError(code ErrorCode, desc string) RuleError { - return RuleError{ - Err: TxRuleError{ErrorCode: code, Description: desc}, - } +// Unwrap returns the underlying wrapped error. +func (e RuleError) Unwrap() error { + return e.Err } // chainRuleError returns a RuleError that encapsulates the given // blockchain.RuleError. func chainRuleError(chainErr blockchain.RuleError) RuleError { - return RuleError{ - Err: chainErr, - } + return RuleError{Err: chainErr, Description: chainErr.Description} } -// 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. - var rerr RuleError - if errors.As(err, &rerr) { - err = rerr.Err - } - - var trerr TxRuleError - return errors.As(err, &trerr) && - trerr.ErrorCode == code +// txRuleError creates a RuleError given a set of arguments. +func txRuleError(kind ErrorKind, desc string) RuleError { + return RuleError{Err: kind, Description: desc} } -// wrapTxRuleError returns a new RuleError with an underlying TxRuleError, +// wrapTxRuleError returns a new RuleError with an underlying ErrorKind, // replacing the description with the provided one while retaining the error -// code from the original error if it can be determined. -func wrapTxRuleError(errorCode ErrorCode, desc string, err error) error { - // Unwrap the underlying error if err is a RuleError - var rerr RuleError - if errors.As(err, &rerr) { - err = rerr.Err - } - - // Override the passed error code with the ones from the error if it is a - // TxRuleError. - var txerr TxRuleError - if errors.As(err, &txerr) { - errorCode = txerr.ErrorCode +// kind from the original error if it can be determined. +func wrapTxRuleError(kind ErrorKind, desc string, err error) error { + // Override the passed error kind with the one from the error if it is an + // ErrorKind. + var kerr ErrorKind + if errors.As(err, &kerr) { + kind = kerr } // Fill a default error description if empty. @@ -129,5 +145,5 @@ func wrapTxRuleError(errorCode ErrorCode, desc string, err error) error { desc = fmt.Sprintf("rejected: %v", err) } - return txRuleError(errorCode, desc) + return txRuleError(kind, desc) } diff --git a/internal/mempool/error_test.go b/internal/mempool/error_test.go new file mode 100644 index 0000000000..c428204295 --- /dev/null +++ b/internal/mempool/error_test.go @@ -0,0 +1,160 @@ +// Copyright (c) 2020 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mempool + +import ( + "errors" + "io" + "testing" +) + +// TestErrorKindStringer tests the stringized output for the ErrorKind type. +func TestErrorKindStringer(t *testing.T) { + tests := []struct { + in ErrorKind + want string + }{ + {ErrInvalid, "ErrInvalid"}, + {ErrOrphanPolicyViolation, "ErrOrphanPolicyViolation"}, + {ErrMempoolDoubleSpend, "ErrMempoolDoubleSpend"}, + {ErrAlreadyVoted, "ErrorAlreadyVoted"}, + {ErrDuplicate, "ErrDuplicate"}, + {ErrCoinbase, "ErrCoinbase"}, + {ErrExpired, "ErrExpired"}, + {ErrNonStandard, "ErrNonStandard"}, + {ErrDustOutput, "ErrDustOutput"}, + {ErrInsufficientFee, "ErrInsufficientFee"}, + {ErrTooManyVotes, "ErrTooManyVotes"}, + {ErrDuplicateRevocation, "ErrDuplicateRevocation"}, + {ErrOldVote, "ErrOldVote"}, + {ErrAlreadyExists, "ErrAlreadyExists"}, + {ErrSeqLockUnmet, "ErrSeqLockUnmet"}, + {ErrInsufficientPriority, "ErrInsufficientPriority"}, + {ErrFeeTooHigh, "ErrFeeTooHigh"}, + {ErrOrphan, "ErrOrphan"}, + {ErrTooManyTSpends, "ErrTooManyTSpends"}, + {ErrTSpendMinedOnAncestor, "ErrTSpendMinedOnAncestor"}, + {ErrTSpendInvalidExpiry, "ErrTSpendInvalidExpiry"}, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("#%d\n got: %s want: %s", i, result, + test.want) + continue + } + } +} + +// TestRuleError tests the error output for the RuleError type. +func TestRuleError(t *testing.T) { + tests := []struct { + in RuleError + want string + }{ + { + RuleError{Description: "duplicate block"}, + "duplicate block", + }, + { + RuleError{Description: "human-readable error"}, + "human-readable error", + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("#%d\n got: %s want: %s", i, result, test.want) + continue + } + } +} + +// TestErrorKindIsAs ensures both ErrorKind and Error can be identified as being +// a specific error kind via errors.Is and unwrapped via errors.As. +func TestErrorKindIsAs(t *testing.T) { + tests := []struct { + name string + err error + target error + wantMatch bool + wantAs ErrorKind + }{{ + name: "ErrOrphanPolicyViolation == ErrOrphanPolicyViolation", + err: ErrOrphanPolicyViolation, + target: ErrOrphanPolicyViolation, + wantMatch: true, + wantAs: ErrOrphanPolicyViolation, + }, { + name: "RuleError.ErrOrphanPolicyViolation == ErrOrphanPolicyViolation", + err: txRuleError(ErrOrphanPolicyViolation, ""), + target: ErrOrphanPolicyViolation, + wantMatch: true, + wantAs: ErrOrphanPolicyViolation, + }, { + name: "RuleError.ErrOrphanPolicyViolation == RuleError.ErrOrphanPolicyViolation", + err: txRuleError(ErrOrphanPolicyViolation, ""), + target: txRuleError(ErrOrphanPolicyViolation, ""), + wantMatch: true, + wantAs: ErrOrphanPolicyViolation, + }, { + name: "ErrOrphanPolicyViolation != ErrMempoolDoubleSpend", + err: ErrOrphanPolicyViolation, + target: ErrMempoolDoubleSpend, + wantMatch: false, + wantAs: ErrOrphanPolicyViolation, + }, { + name: "RuleError.ErrOrphanPolicyViolation != ErrMempoolDoubleSpend", + err: txRuleError(ErrOrphanPolicyViolation, ""), + target: ErrMempoolDoubleSpend, + wantMatch: false, + wantAs: ErrOrphanPolicyViolation, + }, { + name: "ErrOrphanPolicyViolation != RuleError.ErrMempoolDoubleSpend", + err: ErrOrphanPolicyViolation, + target: txRuleError(ErrMempoolDoubleSpend, ""), + wantMatch: false, + wantAs: ErrOrphanPolicyViolation, + }, { + name: "RuleError.ErrOrphanPolicyViolation != RuleError.ErrMempoolDoubleSpend", + err: txRuleError(ErrOrphanPolicyViolation, ""), + target: txRuleError(ErrMempoolDoubleSpend, ""), + wantMatch: false, + wantAs: ErrOrphanPolicyViolation, + }, { + name: "RuleError.ErrOrphanPolicyViolation != io.EOF", + err: txRuleError(ErrOrphanPolicyViolation, ""), + target: io.EOF, + wantMatch: false, + wantAs: ErrOrphanPolicyViolation, + }} + + for _, test := range tests { + // Ensure the error matches or not depending on the expected result. + result := errors.Is(test.err, test.target) + if result != test.wantMatch { + t.Errorf("%s: incorrect error identification -- got %v, want %v", + test.name, result, test.wantMatch) + continue + } + + // Ensure the underlying error kind can be unwrapped is and is the + // expected kind. + var kind ErrorKind + if !errors.As(test.err, &kind) { + t.Errorf("%s: unable to unwrap to error kind", test.name) + continue + } + if kind != test.wantAs { + t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v", + test.name, kind, test.wantAs) + continue + } + } +} diff --git a/internal/mempool/mempool_test.go b/internal/mempool/mempool_test.go index 3d0e83d323..67f49c1631 100644 --- a/internal/mempool/mempool_test.go +++ b/internal/mempool/mempool_test.go @@ -200,7 +200,7 @@ func (s *fakeChain) CalcSequenceLock(tx *dcrutil.Tx, view *blockchain.UtxoViewpo "either does not exist or has already been spent", txIn.PreviousOutPoint, tx.Hash(), txInIndex) return nil, blockchain.RuleError{ - ErrorCode: blockchain.ErrMissingTxOut, + Err: blockchain.ErrMissingTxOut, Description: str, } } @@ -1065,7 +1065,7 @@ func TestVoteOrphan(t *testing.T) { // Ensure the vote is rejected because it is an orphan. _, err = harness.txPool.ProcessTransaction(vote, false, false, true, 0) - if !IsErrorCode(err, ErrOrphan) { + if !errors.Is(err, ErrOrphan) { t.Fatalf("Process Transaction: did not get expected ErrOrphan") } testPoolMembership(tc, vote, false, false) @@ -1136,7 +1136,7 @@ func TestRevocationOrphan(t *testing.T) { // Ensure the vote is rejected because it is an orphan. _, err = harness.txPool.ProcessTransaction(revocation, false, false, true, 0) - if !IsErrorCode(err, ErrOrphan) { + if !errors.Is(err, ErrOrphan) { t.Fatalf("Process Transaction: did not get expected " + "ErrTooManyVotes error code") } @@ -1201,7 +1201,7 @@ func TestOrphanReject(t *testing.T) { t.Fatalf("ProcessTransaction: did not fail on orphan "+ "%v when allow orphans flag is false", tx.Hash()) } - if !IsErrorCode(err, ErrOrphan) { + if !errors.Is(err, ErrOrphan) { t.Fatalf("ProcessTransaction: unexpected error -- got %v, want %v", err, ErrOrphan) } @@ -1860,7 +1860,7 @@ 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): + case acceptSeqLocks && !test.valid && !errors.Is(err, ErrSeqLockUnmet): t.Fatalf("%s: did not get expected ErrSeqLockUnmet", test.name) } @@ -1977,7 +1977,7 @@ func TestMaxVoteDoubleSpendRejection(t *testing.T) { t.Fatalf("ProcessTransaction: accepted double-spending vote with " + "more than max allowed") } - if !IsErrorCode(err, ErrTooManyVotes) { + if !errors.Is(err, ErrTooManyVotes) { t.Fatalf("Process Transaction: did not get expected " + "ErrTooManyVotes error code") } @@ -2015,7 +2015,7 @@ 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, 0) - if !IsErrorCode(err, ErrTooManyVotes) { + if !errors.Is(err, ErrTooManyVotes) { t.Fatalf("Process Transaction: did not get expected " + "ErrTooManyVotes error code") } @@ -2088,7 +2088,7 @@ 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, 0) - if !IsErrorCode(err, ErrAlreadyVoted) { + if !errors.Is(err, ErrAlreadyVoted) { t.Fatalf("Process Transaction: did not get expected " + "ErrTooManyVotes error code") } @@ -2138,7 +2138,7 @@ func TestDuplicateTxError(t *testing.T) { // 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, 0) - if !IsErrorCode(err, ErrDuplicate) { + if !errors.Is(err, ErrDuplicate) { t.Fatalf("ProcessTransaction: did get the expected ErrDuplicate") } testPoolMembership(tc, tx, false, true) @@ -2162,7 +2162,7 @@ func TestDuplicateTxError(t *testing.T) { // The second call should fail with the expected ErrDuplicate error. _, err = harness.txPool.ProcessTransaction(orphan, true, false, true, 0) - if !IsErrorCode(err, ErrDuplicate) { + if !errors.Is(err, ErrDuplicate) { t.Fatalf("ProcessTransaction: did not get expected ErrDuplicate") } testPoolMembership(tc, orphan, true, false) @@ -2207,7 +2207,7 @@ func TestMempoolDoubleSpend(t *testing.T) { // pool and the double spend is not added to the pool. _, err = harness.txPool.ProcessTransaction(doubleSpendTx, true, false, true, 0) - if !IsErrorCode(err, ErrMempoolDoubleSpend) { + if !errors.Is(err, ErrMempoolDoubleSpend) { t.Fatalf("ProcessTransaction: did not get expected ErrMempoolDoubleSpend") } testPoolMembership(tc, tx, false, true) @@ -2451,13 +2451,13 @@ func TestHandlesTSpends(t *testing.T) { // Helper that attempts to add and asserts the given tspend is rejected // with the given error code. - rejectTSpend := func(tx *dcrutil.Tx, errCode ErrorCode) { + rejectTSpend := func(tx *dcrutil.Tx, kind ErrorKind) { t.Helper() _, err = harness.txPool.ProcessTransaction(tx, true, false, true, 0) - if !IsErrorCode(err, errCode) { + if !errors.Is(err, kind) { t.Fatalf("Unexpected error while processing rejected tspend. "+ - "want=%v got=%#v", errCode, err) + "want=%v got=%#v", kind, err) } testPoolMembership(tc, tx, false, false) } @@ -2467,17 +2467,13 @@ func TestHandlesTSpends(t *testing.T) { // // TODO: unify with the above after mempool errors correctly handle // errors.Is/As. - rejectTSpendChainError := func(tx *dcrutil.Tx, errCode blockchain.ErrorCode) { + rejectTSpendChainError := func(tx *dcrutil.Tx, kind blockchain.ErrorKind) { t.Helper() _, err = harness.txPool.ProcessTransaction(tx, true, false, true, 0) - rerr, ok := err.(RuleError) - if !ok { - t.Fatalf("Returned error is not a rule error: %#v", err) - } - if !blockchain.IsErrorCode(rerr.Err, errCode) { + if !errors.Is(err, kind) { t.Fatalf("Unexpected error while processing rejected tspend. "+ - "want=%v got=%#v", errCode, err) + "want=%v got=%#v", kind, err) } testPoolMembership(tc, tx, false, false) } diff --git a/internal/mempool/policy_test.go b/internal/mempool/policy_test.go index 7a8532484b..32a3d1878d 100644 --- a/internal/mempool/policy_test.go +++ b/internal/mempool/policy_test.go @@ -7,6 +7,7 @@ package mempool import ( "bytes" + "errors" "testing" "time" @@ -345,7 +346,7 @@ func TestCheckTransactionStandard(t *testing.T) { tx wire.MsgTx height int64 isStandard bool - code ErrorCode + err error }{ { name: "Typical pay-to-pubkey-hash transaction", @@ -370,7 +371,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "Transaction version too high", @@ -383,7 +384,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "Transaction is not finalized", @@ -400,7 +401,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "Transaction size is too large", @@ -417,7 +418,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "Signature script size is too large", @@ -435,7 +436,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "Signature script that does more than push data", @@ -453,7 +454,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "Valid but non standard public key script", @@ -469,7 +470,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "More than four nulldata outputs", @@ -497,7 +498,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrNonStandard, + err: ErrNonStandard, }, { name: "Dust output", @@ -513,7 +514,7 @@ func TestCheckTransactionStandard(t *testing.T) { }, height: 300000, isStandard: false, - code: ErrDustOutput, + err: ErrDustOutput, }, { name: "One nulldata output with 0 amount (standard)", @@ -556,9 +557,9 @@ func TestCheckTransactionStandard(t *testing.T) { } // Ensure the error code is the expected one. - if !IsErrorCode(err, test.code) { + if !errors.Is(err, test.err) { t.Errorf("checkTransactionStandard (%s): unexpected error -- got "+ - "%v, want %v", test.name, err, test.code) + "%v, want %v", test.name, err, test.err) continue } } diff --git a/internal/mining/cpuminer/cpuminer.go b/internal/mining/cpuminer/cpuminer.go index 4bcc6cb31e..9a251f7e8d 100644 --- a/internal/mining/cpuminer/cpuminer.go +++ b/internal/mining/cpuminer/cpuminer.go @@ -218,7 +218,7 @@ func (m *CPUMiner) submitBlock(block *dcrutil.Block) bool { // debug since it is expected to happen from time to time and not really // an error. if m.cfg.ChainParams.ReduceMinDifficulty && - rErr.ErrorCode == blockchain.ErrHighHash { + errors.Is(rErr, blockchain.ErrHighHash) { log.Debugf("Block submitted via CPU miner rejected because of "+ "ReduceMinDifficulty time sync failure: %v", err) return false diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index 6d2b48063c..a42f5c5aa7 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -4536,7 +4536,7 @@ func handleSendRawTransaction(_ context.Context, s *Server, cmd interface{}) (in err = fmt.Errorf("rejected transaction %v: %v", tx.Hash(), err) log.Debugf("%v", err) - if mempool.IsErrorCode(rErr, mempool.ErrDuplicate) { + if errors.Is(rErr, mempool.ErrDuplicate) { // This is an actual exact duplicate tx, so // return the specific duplicate tx error. return nil, rpcDuplicateTxError("%v", err) diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go index 10e4e38200..a369c8e25e 100644 --- a/internal/rpcserver/rpcserverhandlers_test.go +++ b/internal/rpcserver/rpcserverhandlers_test.go @@ -5693,7 +5693,7 @@ func TestHandleGetWork(t *testing.T) { mockSyncManager: func() *testSyncManager { syncManager := defaultMockSyncManager() syncManager.submitBlockErr = blockchain.RuleError{ - ErrorCode: blockchain.ErrDuplicateBlock, + Err: blockchain.ErrDuplicateBlock, Description: "Duplicate Block", } return syncManager