blockchain: Combine ErrDoubleSpend & ErrMissingTx.

This replaces the ErrDoubleSpend and ErrMissingTx error codes with a
single error code named ErrMissingTxOut and updates the relevant errors
and expected test results accordingly.

Once upon a time, the code relied on a transaction index, so it was able
to definitively differentiate between a transaction output that
legitimately did not exist and one that had already been spent.

However, since the code now uses a pruned utxoset, it is no longer
possible to reliably differentiate since once all outputs of a
transaction are spent, it is removed from the utxoset completely.
Consequently, a missing transaction could be either because the
transaction never existed or because it is fully spent.

Also, while here, consistently use the LookupEntry function on the
UtxoView instead of directly accessing the internal map as intended.
This commit is contained in:
Dave Collins 2018-02-21 12:31:55 -06:00
parent 0dbfca4f79
commit 0536ed4db6
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
9 changed files with 79 additions and 111 deletions

View File

@ -157,9 +157,9 @@ const (
// range or not referencing one at all.
ErrBadTxInput
// ErrMissingTx indicates a transaction referenced by an input is
// missing.
ErrMissingTx
// ErrMissingTxOut indicates a transaction output referenced by an input
// either does not exist or has already been spent.
ErrMissingTxOut
// ErrUnfinalizedTx indicates a transaction has not been finalized.
// A valid block may only contain finalized transactions.
@ -179,10 +179,6 @@ const (
// coinbase that has not yet reached the required maturity.
ErrImmatureSpend
// ErrDoubleSpend indicates a transaction is attempting to spend coins
// that have already been spent.
ErrDoubleSpend
// ErrSpendTooHigh indicates a transaction is attempting to spend more
// value than the sum of all of its inputs.
ErrSpendTooHigh
@ -480,12 +476,11 @@ var errorCodeStrings = map[ErrorCode]string{
ErrBadTxOutValue: "ErrBadTxOutValue",
ErrDuplicateTxInputs: "ErrDuplicateTxInputs",
ErrBadTxInput: "ErrBadTxInput",
ErrMissingTx: "ErrMissingTx",
ErrMissingTxOut: "ErrMissingTxOut",
ErrUnfinalizedTx: "ErrUnfinalizedTx",
ErrDuplicateTx: "ErrDuplicateTx",
ErrOverwriteTx: "ErrOverwriteTx",
ErrImmatureSpend: "ErrImmatureSpend",
ErrDoubleSpend: "ErrDoubleSpend",
ErrSpendTooHigh: "ErrSpendTooHigh",
ErrBadFees: "ErrBadFees",
ErrTooManySigOps: "ErrTooManySigOps",

View File

@ -39,12 +39,11 @@ func TestErrorCodeStringer(t *testing.T) {
{blockchain.ErrDuplicateTxInputs, "ErrDuplicateTxInputs"},
{blockchain.ErrBadTxInput, "ErrBadTxInput"},
{blockchain.ErrBadCheckpoint, "ErrBadCheckpoint"},
{blockchain.ErrMissingTx, "ErrMissingTx"},
{blockchain.ErrMissingTxOut, "ErrMissingTxOut"},
{blockchain.ErrUnfinalizedTx, "ErrUnfinalizedTx"},
{blockchain.ErrDuplicateTx, "ErrDuplicateTx"},
{blockchain.ErrOverwriteTx, "ErrOverwriteTx"},
{blockchain.ErrImmatureSpend, "ErrImmatureSpend"},
{blockchain.ErrDoubleSpend, "ErrDoubleSpend"},
{blockchain.ErrSpendTooHigh, "ErrSpendTooHigh"},
{blockchain.ErrBadFees, "ErrBadFees"},
{blockchain.ErrTooManySigOps, "ErrTooManySigOps"},

View File

@ -718,7 +718,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
acceptedToSideChainWithExpectedTip("b6")
g.NextBlock("b8", outs[4], ticketOuts[4])
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// ---------------------------------------------------------------------
// Too much proof-of-work coinbase tests.
@ -888,7 +888,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
// \-> b3(1) -> b4(2)
g.SetTip("b21")
g.NextBlock("b23", &b3Tx1Out, nil)
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// Create block that forks and spends a tx created on a third fork.
//
@ -900,7 +900,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
acceptedToSideChainWithExpectedTip("b21")
g.NextBlock("b25", outs[6], ticketOuts[6])
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// ---------------------------------------------------------------------
// Immature coinbase tests.
@ -1238,12 +1238,11 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
doubleSpendTx := g.CreateSpendTx(outs[12], lowFee)
g.NextBlock("b42", outs[12], ticketOuts[12], additionalPoWTx(doubleSpendTx))
b42Tx1Out := chaingen.MakeSpendableOut(g.Tip(), 1, 0)
// TODO: This really shoud be ErrDoubleSpend
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
g.SetTip("b41")
g.NextBlock("b43", &b42Tx1Out, ticketOuts[12])
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// ---------------------------------------------------------------------
// Pay-to-script-hash signature operation count tests.
@ -1528,7 +1527,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
b.Transactions[1].TxIn[0].PreviousOutPoint.Hash = *hash
b.Transactions[1].TxIn[0].PreviousOutPoint.Index = 0
})
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// Create block with stake tx in regular tx tree.
//
@ -1598,7 +1597,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
g.NextBlock("b58", outs[16], ticketOuts[16], func(b *wire.MsgBlock) {
b.Transactions[1].TxIn[0].PreviousOutPoint.Index = 42
})
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// Create block with transaction that pays more than its inputs.
//
@ -1715,7 +1714,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
b.AddTransaction(tx3)
b.AddTransaction(tx2)
})
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// Create block that double spends a transaction created in the same
// block.
@ -1730,8 +1729,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
b.AddTransaction(tx3)
b.AddTransaction(tx4)
})
// TODO: This really shoud be ErrDoubleSpend
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// ---------------------------------------------------------------------
// Extra subsidy tests.
@ -2086,7 +2084,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
tx := g.CreateSpendTx(&b87OpReturnOut, zeroFee)
b.AddTransaction(tx)
})
rejected(blockchain.ErrMissingTx)
rejected(blockchain.ErrMissingTxOut)
// Create a block that has a transaction with multiple OP_RETURNs. Even
// though a transaction with a large number of OP_RETURNS is not

View File

@ -63,7 +63,7 @@ out:
"transaction %v referenced from "+
"transaction %v", originTxHash,
txVI.tx.Hash())
err := ruleError(ErrMissingTx, str)
err := ruleError(ErrMissingTxOut, str)
v.sendResult(err)
break out
}

View File

@ -67,10 +67,11 @@ func (b *BlockChain) calcSequenceLock(node *blockNode, tx *dcrutil.Tx, view *Utx
utxo := view.LookupEntry(&txIn.PreviousOutPoint.Hash)
if utxo == nil {
str := fmt.Sprintf("unable to find unspent output "+
"%v referenced from transaction %s:%d",
txIn.PreviousOutPoint, tx.Hash(), txInIndex)
return sequenceLock, ruleError(ErrMissingTx, str)
str := fmt.Sprintf("output %v referenced from "+
"transaction %s:%d either does not exist or "+
"has already been spent", txIn.PreviousOutPoint,
tx.Hash(), txInIndex)
return sequenceLock, ruleError(ErrMissingTxOut, str)
}
// Calculate the sequence locks from the point of view of the

View File

@ -1228,7 +1228,6 @@ func (b *BlockChain) checkDupTxs(txSet []*dcrutil.Tx, view *UtxoViewpoint) error
// NOTE: The transaction MUST have already been sanity checked with the
// CheckTransactionSanity function prior to calling this function.
func CheckTransactionInputs(subsidyCache *SubsidyCache, tx *dcrutil.Tx, txHeight int64, utxoView *UtxoViewpoint, checkFraudProof bool, chainParams *chaincfg.Params) (int64, error) {
msgTx := tx.MsgTx()
// Expired transactions are not allowed.
@ -1267,22 +1266,15 @@ func CheckTransactionInputs(subsidyCache *SubsidyCache, tx *dcrutil.Tx, txHeight
for idx, txIn := range msgTx.TxIn {
// Ensure the input is available.
txInHash := &txIn.PreviousOutPoint.Hash
utxoEntry, exists := utxoView.entries[*txInHash]
if !exists || utxoEntry == nil {
str := fmt.Sprintf("unable to find input "+
"transaction %v for transaction %v",
txInHash, txHash)
return 0, ruleError(ErrMissingTx, str)
}
// Ensure the transaction is not double spending coins.
originTxHash := &txIn.PreviousOutPoint.Hash
originTxIndex := txIn.PreviousOutPoint.Index
if utxoEntry.IsOutputSpent(originTxIndex) {
str := fmt.Sprintf("transaction %s:%d tried "+
"to double spend output %v", txHash,
idx, txIn.PreviousOutPoint)
return 0, ruleError(ErrDoubleSpend, str)
utxoEntry := utxoView.LookupEntry(originTxHash)
if utxoEntry == nil || utxoEntry.IsOutputSpent(originTxIndex) {
str := fmt.Sprintf("output %v referenced from "+
"transaction %s:%d either does not exist or "+
"has already been spent", txIn.PreviousOutPoint,
txHash, idx)
return 0, ruleError(ErrMissingTxOut, str)
}
// Check and make sure that the input is P2PKH or P2SH.
@ -1299,7 +1291,7 @@ func CheckTransactionInputs(subsidyCache *SubsidyCache, tx *dcrutil.Tx, txHeight
", txout %v referenced a txout that "+
"was not a PubKeyHashTy or "+
"ScriptHashTy pkScrpt (class: %v, "+
"version %v, script %x)", txInHash,
"version %v, script %x)", originTxHash,
originTxIndex, class, pkVer, pkScrpt)
return 0, ruleError(ErrSStxInScrType, errStr)
}
@ -1375,12 +1367,13 @@ func CheckTransactionInputs(subsidyCache *SubsidyCache, tx *dcrutil.Tx, txHeight
// We also need to make sure that the SSGen outputs that are
// P2PKH go to the addresses specified in the original SSTx.
// Check that too.
utxoEntrySstx, exists := utxoView.entries[sstxHash]
if !exists || utxoEntrySstx == nil {
errStr := fmt.Sprintf("Unable to find input sstx "+
"transaction %v for transaction %v", sstxHash,
txHash)
return 0, ruleError(ErrMissingTx, errStr)
utxoEntrySstx := utxoView.LookupEntry(&sstxHash)
if utxoEntrySstx == nil {
str := fmt.Sprintf("ticket output %v referenced from "+
"transaction %s:%d either does not exist or "+
"has already been spent", sstxIn.PreviousOutPoint,
txHash, 1)
return 0, ruleError(ErrMissingTxOut, str)
}
// While we're here, double check to make sure that the input
@ -1512,12 +1505,13 @@ func CheckTransactionInputs(subsidyCache *SubsidyCache, tx *dcrutil.Tx, txHeight
// We also need to make sure that the SSGen outputs that are
// P2PKH go to the addresses specified in the original SSTx.
// Check that too.
utxoEntrySstx, exists := utxoView.entries[sstxHash]
if !exists || utxoEntrySstx == nil {
errStr := fmt.Sprintf("Unable to find input sstx "+
"transaction %v for transaction %v", sstxHash,
txHash)
return 0, ruleError(ErrMissingTx, errStr)
utxoEntrySstx := utxoView.LookupEntry(&sstxHash)
if utxoEntrySstx == nil {
str := fmt.Sprintf("ticket output %v referenced from "+
"transaction %s:%d either does not exist or "+
"has already been spent", sstxIn.PreviousOutPoint,
txHash, 0)
return 0, ruleError(ErrMissingTxOut, str)
}
// While we're here, double check to make sure that the input
@ -1620,15 +1614,17 @@ func CheckTransactionInputs(subsidyCache *SubsidyCache, tx *dcrutil.Tx, txHeight
}
txInHash := &txIn.PreviousOutPoint.Hash
utxoEntry, exists := utxoView.entries[*txInHash]
if !exists || utxoEntry == nil {
str := fmt.Sprintf("unable to find input transaction "+
"%v for transaction %v", txInHash, txHash)
return 0, ruleError(ErrMissingTx, str)
originTxIndex := txIn.PreviousOutPoint.Index
utxoEntry := utxoView.LookupEntry(txInHash)
if utxoEntry == nil || utxoEntry.IsOutputSpent(originTxIndex) {
str := fmt.Sprintf("output %v referenced from "+
"transaction %s:%d either does not exist or "+
"has already been spent", txIn.PreviousOutPoint,
txHash, idx)
return 0, ruleError(ErrMissingTxOut, str)
}
// Check fraud proof witness data.
originTxIndex := txIn.PreviousOutPoint.Index
// Using zero value outputs as inputs is banned.
if utxoEntry.AmountByIndex(originTxIndex) == 0 {
@ -1699,14 +1695,6 @@ func CheckTransactionInputs(subsidyCache *SubsidyCache, tx *dcrutil.Tx, txHeight
}
}
// Ensure the transaction is not double spending coins.
if utxoEntry.IsOutputSpent(originTxIndex) {
str := fmt.Sprintf("transaction %s:%d tried to double "+
"spend output %v", txHash, originTxIndex,
txIn.PreviousOutPoint)
return 0, ruleError(ErrDoubleSpend, str)
}
// Ensure that the outpoint's tx tree makes sense.
originTxOPTree := txIn.PreviousOutPoint.Tree
originTxType := utxoEntry.TransactionType()
@ -1929,22 +1917,13 @@ func CountP2SHSigOps(tx *dcrutil.Tx, isCoinBaseTx bool, isStakeBaseTx bool, utxo
// Ensure the referenced input transaction is available.
originTxHash := &txIn.PreviousOutPoint.Hash
originTxIndex := txIn.PreviousOutPoint.Index
utxoEntry, ok := utxoView.entries[*originTxHash]
if !ok || utxoEntry == nil {
str := fmt.Sprintf("unable to find unspent transaction"+
" %v referenced from transaction %s:%d during "+
"CountP2SHSigOps: output missing",
txIn.PreviousOutPoint.Hash, tx.Hash(),
txInIndex)
return 0, ruleError(ErrMissingTx, str)
}
if utxoEntry.IsOutputSpent(originTxIndex) {
str := fmt.Sprintf("unable to find unspent output "+
"%v referenced from transaction %s:%d during "+
"CountP2SHSigOps: output spent",
txIn.PreviousOutPoint, tx.Hash(), txInIndex)
return 0, ruleError(ErrMissingTx, str)
utxoEntry := utxoView.LookupEntry(originTxHash)
if utxoEntry == nil || utxoEntry.IsOutputSpent(originTxIndex) {
str := fmt.Sprintf("output %v referenced from "+
"transaction %s:%d either does not exist or "+
"has already been spent", txIn.PreviousOutPoint,
tx.Hash(), txInIndex)
return 0, ruleError(ErrMissingTxOut, str)
}
// We're only interested in pay-to-script-hash types, so skip
@ -2342,7 +2321,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B
// an error now.
if node.hash.IsEqual(b.chainParams.GenesisHash) {
str := "the coinbase for the genesis block is not spendable"
return ruleError(ErrMissingTx, str)
return ruleError(ErrMissingTxOut, str)
}
// Ensure the view is for the node being checked.

View File

@ -1651,10 +1651,10 @@ func TestBlockValidationRules(t *testing.T) {
err)
}
// Fails and hits ErrMissingTx.
// Fails and hits ErrMissingTxOut.
err = chain.CheckConnectBlock(b154test, blockchain.BFNoPoWCheck)
if err == nil || err.(blockchain.RuleError).ErrorCode !=
blockchain.ErrMissingTx {
blockchain.ErrMissingTxOut {
t.Errorf("Unexpected no or wrong error for invalMissingInsS154 test: %v",
err)
}
@ -1686,7 +1686,7 @@ func TestBlockValidationRules(t *testing.T) {
}
// ----------------------------------------------------------------------------
// ErrMissingTx (formerly ErrZeroValueOutputSpend). In the latest version of
// ErrMissingTxOut (formerly ErrZeroValueOutputSpend). In the latest version of
// the database, zero value outputs are automatically pruned, so the output
// is simply missing.
mtxFromB = new(wire.MsgTx)
@ -1720,9 +1720,9 @@ func TestBlockValidationRules(t *testing.T) {
// Fails and hits ErrZeroValueOutputSpend.
err = chain.CheckConnectBlock(b154test, blockchain.BFNoPoWCheck)
if err == nil || err.(blockchain.RuleError).ErrorCode !=
blockchain.ErrMissingTx {
blockchain.ErrMissingTxOut {
t.Errorf("Unexpected no or wrong error for "+
"ErrMissingTx test: %v", err)
"ErrMissingTxOut test: %v", err)
}
// ----------------------------------------------------------------------------
@ -1778,16 +1778,16 @@ func TestBlockValidationRules(t *testing.T) {
err = blockchain.CheckWorklessBlockSanity(b166test, timeSource, params)
if err != nil {
t.Errorf("got unexpected error for ErrMissingTx test 1 sanity "+
t.Errorf("got unexpected error for ErrMissingTxOut test 1 sanity "+
"check: %v", err)
}
// Fails and hits ErrMissingTx.
// Fails and hits ErrMissingTxOut.
err = chain.CheckConnectBlock(b166test, blockchain.BFNoPoWCheck)
if err == nil || err.(blockchain.RuleError).ErrorCode !=
blockchain.ErrMissingTx {
blockchain.ErrMissingTxOut {
t.Errorf("Unexpected no or wrong error for "+
"ErrMissingTx test 1: %v", err)
"ErrMissingTxOut test 1: %v", err)
}
// ----------------------------------------------------------------------------
@ -1824,16 +1824,16 @@ func TestBlockValidationRules(t *testing.T) {
err = blockchain.CheckWorklessBlockSanity(b166test, timeSource, params)
if err != nil {
t.Errorf("got unexpected error for ErrMissingTx test 2 sanity "+
t.Errorf("got unexpected error for ErrMissingTxOut test 2 sanity "+
"check: %v", err)
}
// Fails and hits ErrMissingTx.
// Fails and hits ErrMissingTxOut.
err = chain.CheckConnectBlock(b166test, blockchain.BFNoPoWCheck)
if err == nil || err.(blockchain.RuleError).ErrorCode !=
blockchain.ErrMissingTx {
blockchain.ErrMissingTxOut {
t.Errorf("Unexpected no or wrong error for "+
"ErrMissingTx test 2: %v", err)
"ErrMissingTxOut test 2: %v", err)
}
// ----------------------------------------------------------------------------
@ -1871,7 +1871,7 @@ func TestBlockValidationRules(t *testing.T) {
"check: %v", err)
}
// Fails and hits ErrMissingTx. It may not be immediately clear
// Fails and hits ErrMissingTxOut. It may not be immediately clear
// why this happens, but in the case of the stake transaction
// tree, because you can't spend in chains, the txlookup code
// doesn't even bother to populate the spent list in the txlookup
@ -1901,10 +1901,10 @@ func TestBlockValidationRules(t *testing.T) {
"check: %v", err)
}
// Fails and hits ErrMissingTx.
// Fails and hits ErrMissingTxOut.
err = chain.CheckConnectBlock(b166test, blockchain.BFNoPoWCheck)
if err == nil || err.(blockchain.RuleError).ErrorCode !=
blockchain.ErrMissingTx {
blockchain.ErrMissingTxOut {
t.Errorf("Unexpected no or wrong error for "+
"double spend test 1: %v", err)
}
@ -1926,10 +1926,10 @@ func TestBlockValidationRules(t *testing.T) {
"check: %v", err)
}
// Fails and hits ErrMissingTx.
// Fails and hits ErrMissingTxOut.
err = chain.CheckConnectBlock(b166test, blockchain.BFNoPoWCheck)
if err == nil || err.(blockchain.RuleError).ErrorCode !=
blockchain.ErrMissingTx {
blockchain.ErrMissingTxOut {
t.Errorf("Unexpected no or wrong error for "+
"double spend test 2: %v", err)
}

View File

@ -75,8 +75,6 @@ func extractRejectCode(err error) (wire.RejectCode, bool) {
switch err.ErrorCode {
// Rejected due to duplicate.
case blockchain.ErrDuplicateBlock:
fallthrough
case blockchain.ErrDoubleSpend:
code = wire.RejectDuplicate
// Rejected due to obsolete version.

View File

@ -2900,7 +2900,7 @@ func chainErrToGBTErrString(err error) string {
return "bad-txns-dupinputs"
case blockchain.ErrBadTxInput:
return "bad-txns-badinput"
case blockchain.ErrMissingTx:
case blockchain.ErrMissingTxOut:
return "bad-txns-missinginput"
case blockchain.ErrUnfinalizedTx:
return "bad-txns-unfinalizedtx"
@ -2910,8 +2910,6 @@ func chainErrToGBTErrString(err error) string {
return "bad-txns-overwrite"
case blockchain.ErrImmatureSpend:
return "bad-txns-maturity"
case blockchain.ErrDoubleSpend:
return "bad-txns-dblspend"
case blockchain.ErrSpendTooHigh:
return "bad-txns-highspend"
case blockchain.ErrBadFees: