dcrd/blockchain/error_test.go
Dave Collins 6a9a161c28
blockchain: Handle db upgrade paths for ver < 5.
This removes all code related to upgrading old databases prior to
version 5 and adds code so the main dcrd block loading logic is able to
query the blockchain if the database is too old to be upgraded and then
automatically handle removing and recreating the database when it is.

In other words, there is no manual intervention required by the user as
it will behave the same as if the user had manually removed the database
and thus redownload the chain from scratch as normal.

Since the blockchain package is used by other callers and utilities as
well, the upgrade path is also updated to detect the situation and
return an appropriate error along with additional information informing
the caller that the version is no longer supported and the database must
be removed.

While it would be nice to continue to support upgrading from super old
versions, there was a major overhaul from v4 to v5 such that an entire
reindex that needs to be tightly integrated with the code was required.

Since the aforementioned code has continued to change since that time,
attempting to continue to support this now would require reimplementing
a massive amount of code solely to support upgrading nodes that are
woefully out of data at this point and have long since been hard forked
from the network.

Upgrades starting from v5 are still supported.

Finally, in order to make the error detection more ergonomic, this
introduces a new ContextError type which can be used to wrap any error
with additional context such that errors.Is is able to detect the
underlying wrapped error.  Ideally, the other error types would be
consolidated in the future to make use to the new ContextError type
instead to provide the additional context, but since that would be a
breaking change the API, it will need to be done in a new major version.
2020-10-27 10:25:49 -05:00

327 lines
12 KiB
Go

// Copyright (c) 2014 The btcsuite 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"
)
// TestErrorKindStringer tests the stringized output for the ErrorKind type.
func TestErrorKindStringer(t *testing.T) {
tests := []struct {
in ErrorKind
want 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"},
{ErrInvalidTemplateParent, "ErrInvalidTemplateParent"},
{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"},
{ErrMultipleTreasurybases, "ErrMultipleTreasurybases"},
{ErrInvalidTreasurybaseTxOutputs, "ErrInvalidTreasurybaseTxOutputs"},
{ErrInvalidTreasurybaseVersion, "ErrInvalidTreasurybaseVersion"},
{ErrInvalidTreasurybaseScript, "ErrInvalidTreasurybaseScript"},
{ErrMultipleTreasurybases, "ErrMultipleTreasurybases"},
{ErrBadTreasurybaseAmountIn, "ErrBadTreasurybaseAmountIn"},
{ErrBadTSpendOutpoint, "ErrBadTSpendOutpoint"},
{ErrBadTSpendFraudProof, "ErrBadTSpendFraudProof"},
{ErrBadTSpendScriptLen, "ErrBadTSpendScriptLen"},
{ErrInvalidTAddChange, "ErrInvalidTAddChange"},
{ErrTooManyTAdds, "ErrTooManyTAdds"},
{ErrTicketExhaustion, "ErrTicketExhaustion"},
{ErrDBTooOldToUpgrade, "ErrDBTooOldToUpgrade"},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("#%d: 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: got: %s want: %s", i, result, test.want)
continue
}
}
}
// TestContextError tests the error output for the ContextError type.
func TestContextError(t *testing.T) {
tests := []struct {
in ContextError
want string
}{{
ContextError{Description: "duplicate block"},
"duplicate block",
}, {
ContextError{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: 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: "ErrDuplicateBlock == ErrDuplicateBlock",
err: ErrDuplicateBlock,
target: ErrDuplicateBlock,
wantMatch: true,
wantAs: ErrDuplicateBlock,
}, {
name: "RuleError.ErrDuplicateBlock == ErrDuplicateBlock",
err: ruleError(ErrDuplicateBlock, ""),
target: ErrDuplicateBlock,
wantMatch: true,
wantAs: ErrDuplicateBlock,
}, {
name: "RuleError.ErrDuplicateBlock == RuleError.ErrDuplicateBlock",
err: ruleError(ErrDuplicateBlock, ""),
target: ruleError(ErrDuplicateBlock, ""),
wantMatch: true,
wantAs: ErrDuplicateBlock,
}, {
name: "ErrDuplicateBlock != ErrMissingParent",
err: ErrDuplicateBlock,
target: ErrMissingParent,
wantMatch: false,
wantAs: ErrDuplicateBlock,
}, {
name: "RuleError.ErrDuplicateBlock != ErrMissingParent",
err: ruleError(ErrDuplicateBlock, ""),
target: ErrMissingParent,
wantMatch: false,
wantAs: ErrDuplicateBlock,
}, {
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,
}, {
name: "ContextError.ErrDBTooOldToUpgrade == ErrDBTooOldToUpgrade",
err: contextError(ErrDBTooOldToUpgrade, ""),
target: ErrDBTooOldToUpgrade,
wantMatch: true,
wantAs: ErrDBTooOldToUpgrade,
}, {
name: "ContextError.ErrDBTooOldToUpgrade == ContextError.ErrDBTooOldToUpgrade",
err: contextError(ErrDBTooOldToUpgrade, ""),
target: contextError(ErrDBTooOldToUpgrade, ""),
wantMatch: true,
wantAs: ErrDBTooOldToUpgrade,
}, {
name: "ContextError.ErrDBTooOldToUpgrade != ErrMissingParent",
err: contextError(ErrDBTooOldToUpgrade, ""),
target: ErrMissingParent,
wantMatch: false,
wantAs: ErrDBTooOldToUpgrade,
}, {
name: "ErrDBTooOldToUpgrade != ContextError.ErrMissingParent",
err: ErrDBTooOldToUpgrade,
target: contextError(ErrMissingParent, ""),
wantMatch: false,
wantAs: ErrDBTooOldToUpgrade,
}, {
name: "ContextError.ErrDBTooOldToUpgrade != ContextError.ErrMissingParent",
err: contextError(ErrDBTooOldToUpgrade, ""),
target: contextError(ErrMissingParent, ""),
wantMatch: false,
wantAs: ErrDBTooOldToUpgrade,
}, {
name: "ContextError.ErrDBTooOldToUpgrade != io.EOF",
err: contextError(ErrDBTooOldToUpgrade, ""),
target: io.EOF,
wantMatch: false,
wantAs: ErrDBTooOldToUpgrade,
}}
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
}
}
}