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.
327 lines
12 KiB
Go
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
|
|
}
|
|
}
|
|
}
|