From d015286f6536ef080439f32966e2bb9b772acd32 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Mon, 13 Apr 2020 12:28:59 +0300 Subject: [PATCH] [NOD-909] Add tests for double spends (#694) * [NOD-909] Add tests for double spends * [NOD-909] Add prepareAndProcessBlock that gets parent hashes and transactions as argument * [NOD-909] Use PrepareAndProcessBlockForTest where possible * [NOD-909] Use more meaningful names * [NOD-909] Change a comment * [NOD-909] Fix comment * [NOD-909] Fix comment --- blockdag/common_test.go | 20 +----- blockdag/dag_test.go | 145 +++++++++++++++++++++++++++++++++++--- blockdag/ghostdag_test.go | 10 +-- blockdag/test_utils.go | 23 ++++++ blockdag/validate_test.go | 6 +- mempool/mempool_test.go | 51 ++++++++++++++ 6 files changed, 221 insertions(+), 34 deletions(-) diff --git a/blockdag/common_test.go b/blockdag/common_test.go index 8a245a748..46155e3ff 100644 --- a/blockdag/common_test.go +++ b/blockdag/common_test.go @@ -172,28 +172,12 @@ func checkRuleError(gotErr, wantErr error) error { return nil } -func prepareAndProcessBlock(t *testing.T, dag *BlockDAG, parents ...*wire.MsgBlock) *wire.MsgBlock { +func prepareAndProcessBlockByParentMsgBlocks(t *testing.T, dag *BlockDAG, parents ...*wire.MsgBlock) *wire.MsgBlock { parentHashes := make([]*daghash.Hash, len(parents)) for i, parent := range parents { parentHashes[i] = parent.BlockHash() } - daghash.Sort(parentHashes) - block, err := PrepareBlockForTest(dag, parentHashes, nil) - if err != nil { - t.Fatalf("error in PrepareBlockForTest: %s", err) - } - utilBlock := util.NewBlock(block) - isOrphan, isDelayed, err := dag.ProcessBlock(utilBlock, BFNoPoWCheck) - if err != nil { - t.Fatalf("unexpected error in ProcessBlock: %s", err) - } - if isDelayed { - t.Fatalf("block is too far in the future") - } - if isOrphan { - t.Fatalf("block was unexpectedly orphan") - } - return block + return PrepareAndProcessBlockForTest(t, dag, parentHashes, nil) } func nodeByMsgBlock(t *testing.T, dag *BlockDAG, block *wire.MsgBlock) *blockNode { diff --git a/blockdag/dag_test.go b/blockdag/dag_test.go index 450e12873..4a6b9dea6 100644 --- a/blockdag/dag_test.go +++ b/blockdag/dag_test.go @@ -688,7 +688,7 @@ func TestConfirmations(t *testing.T) { chainBlocks := make([]*wire.MsgBlock, 5) chainBlocks[0] = dag.dagParams.GenesisBlock for i := uint32(1); i < 5; i++ { - chainBlocks[i] = prepareAndProcessBlock(t, dag, chainBlocks[i-1]) + chainBlocks[i] = prepareAndProcessBlockByParentMsgBlocks(t, dag, chainBlocks[i-1]) } // Make sure that each one of the chain blocks has the expected confirmations number @@ -707,8 +707,8 @@ func TestConfirmations(t *testing.T) { branchingBlocks := make([]*wire.MsgBlock, 2) // Add two branching blocks - branchingBlocks[0] = prepareAndProcessBlock(t, dag, chainBlocks[1]) - branchingBlocks[1] = prepareAndProcessBlock(t, dag, branchingBlocks[0]) + branchingBlocks[0] = prepareAndProcessBlockByParentMsgBlocks(t, dag, chainBlocks[1]) + branchingBlocks[1] = prepareAndProcessBlockByParentMsgBlocks(t, dag, branchingBlocks[0]) // Check that the genesis has a confirmations number == len(chainBlocks) genesisConfirmations, err = dag.blockConfirmations(dag.genesis) @@ -738,7 +738,7 @@ func TestConfirmations(t *testing.T) { // Generate 100 blocks to force the "main" chain to become red branchingChainTip := branchingBlocks[1] for i := uint32(0); i < 100; i++ { - nextBranchingChainTip := prepareAndProcessBlock(t, dag, branchingChainTip) + nextBranchingChainTip := prepareAndProcessBlockByParentMsgBlocks(t, dag, branchingChainTip) branchingChainTip = nextBranchingChainTip } @@ -797,7 +797,7 @@ func TestAcceptingBlock(t *testing.T) { chainBlocks := make([]*wire.MsgBlock, numChainBlocks) chainBlocks[0] = dag.dagParams.GenesisBlock for i := uint32(1); i <= numChainBlocks-1; i++ { - chainBlocks[i] = prepareAndProcessBlock(t, dag, chainBlocks[i-1]) + chainBlocks[i] = prepareAndProcessBlockByParentMsgBlocks(t, dag, chainBlocks[i-1]) } // Make sure that each chain block (including the genesis) is accepted by its child @@ -825,7 +825,7 @@ func TestAcceptingBlock(t *testing.T) { // Generate a chain tip that will be in the anticone of the selected tip and // in dag.virtual.blues. - branchingChainTip := prepareAndProcessBlock(t, dag, chainBlocks[len(chainBlocks)-3]) + branchingChainTip := prepareAndProcessBlockByParentMsgBlocks(t, dag, chainBlocks[len(chainBlocks)-3]) // Make sure that branchingChainTip is not in the selected parent chain isBranchingChainTipInSelectedParentChain, err := dag.IsInSelectedParentChain(branchingChainTip.BlockHash()) @@ -863,7 +863,7 @@ func TestAcceptingBlock(t *testing.T) { intersectionBlock := chainBlocks[1] sideChainTip := intersectionBlock for i := 0; i < len(chainBlocks)-3; i++ { - sideChainTip = prepareAndProcessBlock(t, dag, sideChainTip) + sideChainTip = prepareAndProcessBlockByParentMsgBlocks(t, dag, sideChainTip) } // Make sure that the accepting block of the parent of the branching block didn't change @@ -879,7 +879,7 @@ func TestAcceptingBlock(t *testing.T) { // Make sure that a block that is found in the red set of the selected tip // doesn't have an accepting block - prepareAndProcessBlock(t, dag, sideChainTip, chainBlocks[len(chainBlocks)-1]) + prepareAndProcessBlockByParentMsgBlocks(t, dag, sideChainTip, chainBlocks[len(chainBlocks)-1]) sideChainTipAcceptingBlock, err := acceptingBlockByMsgBlock(sideChainTip) if err != nil { @@ -1117,3 +1117,132 @@ func TestIsDAGCurrentMaxDiff(t *testing.T) { } } } + +func TestDoubleSpends(t *testing.T) { + params := dagconfig.SimnetParams + params.BlockCoinbaseMaturity = 0 + // Create a new database and dag instance to run tests against. + dag, teardownFunc, err := DAGSetup("TestDoubleSpends", true, Config{ + DAGParams: ¶ms, + }) + if err != nil { + t.Fatalf("Failed to setup dag instance: %v", err) + } + defer teardownFunc() + + fundingBlock := PrepareAndProcessBlockForTest(t, dag, []*daghash.Hash{params.GenesisHash}, nil) + cbTx := fundingBlock.Transactions[0] + + signatureScript, err := txscript.PayToScriptHashSignatureScript(OpTrueScript, nil) + if err != nil { + t.Fatalf("Failed to build signature script: %s", err) + } + txIn := &wire.TxIn{ + PreviousOutpoint: wire.Outpoint{TxID: *cbTx.TxID(), Index: 0}, + SignatureScript: signatureScript, + Sequence: wire.MaxTxInSequenceNum, + } + txOut := &wire.TxOut{ + ScriptPubKey: OpTrueScript, + Value: uint64(1), + } + tx1 := wire.NewNativeMsgTx(wire.TxVersion, []*wire.TxIn{txIn}, []*wire.TxOut{txOut}) + + doubleSpendTxOut := &wire.TxOut{ + ScriptPubKey: OpTrueScript, + Value: uint64(2), + } + doubleSpendTx1 := wire.NewNativeMsgTx(wire.TxVersion, []*wire.TxIn{txIn}, []*wire.TxOut{doubleSpendTxOut}) + + blockWithTx1 := PrepareAndProcessBlockForTest(t, dag, []*daghash.Hash{fundingBlock.BlockHash()}, []*wire.MsgTx{tx1}) + + // Check that a block will be rejected if it has a transaction that already exists in its past. + anotherBlockWithTx1, err := PrepareBlockForTest(dag, []*daghash.Hash{blockWithTx1.BlockHash()}, nil) + if err != nil { + t.Fatalf("PrepareBlockForTest: %v", err) + } + + // Manually add tx1. + anotherBlockWithTx1.Transactions = append(anotherBlockWithTx1.Transactions, tx1) + anotherBlockWithTx1UtilTxs := make([]*util.Tx, len(anotherBlockWithTx1.Transactions)) + for i, tx := range anotherBlockWithTx1.Transactions { + anotherBlockWithTx1UtilTxs[i] = util.NewTx(tx) + } + anotherBlockWithTx1.Header.HashMerkleRoot = BuildHashMerkleTreeStore(anotherBlockWithTx1UtilTxs).Root() + + isOrphan, isDelayed, err := dag.ProcessBlock(util.NewBlock(anotherBlockWithTx1), BFNoPoWCheck) + if err == nil { + t.Errorf("ProcessBlock expected an error") + } else { + var ruleErr RuleError + if ok := errors.As(err, &ruleErr); ok { + if ruleErr.ErrorCode != ErrOverwriteTx { + t.Errorf("ProcessBlock expected an %v error code but got %v", ErrOverwriteTx, ruleErr.ErrorCode) + } + } else { + t.Errorf("ProcessBlock expected a blockdag.RuleError but got %v", err) + } + } + if isDelayed { + t.Fatalf("ProcessBlock: anotherBlockWithTx1 " + + "is too far in the future") + } + if isOrphan { + t.Fatalf("ProcessBlock: anotherBlockWithTx1 got unexpectedly orphaned") + } + + // Check that a block will be rejected if it has a transaction that double spends + // a transaction from its past. + blockWithDoubleSpendForTx1, err := PrepareBlockForTest(dag, []*daghash.Hash{blockWithTx1.BlockHash()}, nil) + if err != nil { + t.Fatalf("PrepareBlockForTest: %v", err) + } + + // Manually add a transaction that double spends the block past. + blockWithDoubleSpendForTx1.Transactions = append(blockWithDoubleSpendForTx1.Transactions, doubleSpendTx1) + blockWithDoubleSpendForTx1UtilTxs := make([]*util.Tx, len(blockWithDoubleSpendForTx1.Transactions)) + for i, tx := range blockWithDoubleSpendForTx1.Transactions { + blockWithDoubleSpendForTx1UtilTxs[i] = util.NewTx(tx) + } + blockWithDoubleSpendForTx1.Header.HashMerkleRoot = BuildHashMerkleTreeStore(blockWithDoubleSpendForTx1UtilTxs).Root() + + isOrphan, isDelayed, err = dag.ProcessBlock(util.NewBlock(blockWithDoubleSpendForTx1), BFNoPoWCheck) + if err == nil { + t.Errorf("ProcessBlock expected an error") + } else { + var ruleErr RuleError + if ok := errors.As(err, &ruleErr); ok { + if ruleErr.ErrorCode != ErrMissingTxOut { + t.Errorf("ProcessBlock expected an %v error code but got %v", ErrMissingTxOut, ruleErr.ErrorCode) + } + } else { + t.Errorf("ProcessBlock expected a blockdag.RuleError but got %v", err) + } + } + if isDelayed { + t.Fatalf("ProcessBlock: blockWithDoubleSpendForTx1 " + + "is too far in the future") + } + if isOrphan { + t.Fatalf("ProcessBlock: blockWithDoubleSpendForTx1 got unexpectedly orphaned") + } + + blockInAnticoneOfBlockWithTx1, err := PrepareBlockForTest(dag, []*daghash.Hash{fundingBlock.BlockHash()}, []*wire.MsgTx{doubleSpendTx1}) + if err != nil { + t.Fatalf("PrepareBlockForTest: %v", err) + } + + // Check that a block will not get rejected if it has a transaction that double spends + // a transaction from its anticone. + isOrphan, isDelayed, err = dag.ProcessBlock(util.NewBlock(blockInAnticoneOfBlockWithTx1), BFNoPoWCheck) + if err != nil { + t.Fatalf("ProcessBlock: %v", err) + } + if isDelayed { + t.Fatalf("ProcessBlock: blockInAnticoneOfBlockWithTx1 " + + "is too far in the future") + } + if isOrphan { + t.Fatalf("ProcessBlock: blockInAnticoneOfBlockWithTx1 got unexpectedly orphaned") + } +} diff --git a/blockdag/ghostdag_test.go b/blockdag/ghostdag_test.go index 80b7e24b7..920bff7fd 100644 --- a/blockdag/ghostdag_test.go +++ b/blockdag/ghostdag_test.go @@ -293,14 +293,14 @@ func TestBlueAnticoneSizeErrors(t *testing.T) { // Prepare a block chain with size K beginning with the genesis block currentBlockA := dag.dagParams.GenesisBlock for i := dagconfig.KType(0); i < dag.dagParams.K; i++ { - newBlock := prepareAndProcessBlock(t, dag, currentBlockA) + newBlock := prepareAndProcessBlockByParentMsgBlocks(t, dag, currentBlockA) currentBlockA = newBlock } // Prepare another block chain with size K beginning with the genesis block currentBlockB := dag.dagParams.GenesisBlock for i := dagconfig.KType(0); i < dag.dagParams.K; i++ { - newBlock := prepareAndProcessBlock(t, dag, currentBlockB) + newBlock := prepareAndProcessBlockByParentMsgBlocks(t, dag, currentBlockB) currentBlockB = newBlock } @@ -332,11 +332,11 @@ func TestGHOSTDAGErrors(t *testing.T) { defer teardownFunc() // Add two child blocks to the genesis - block1 := prepareAndProcessBlock(t, dag, dag.dagParams.GenesisBlock) - block2 := prepareAndProcessBlock(t, dag, dag.dagParams.GenesisBlock) + block1 := prepareAndProcessBlockByParentMsgBlocks(t, dag, dag.dagParams.GenesisBlock) + block2 := prepareAndProcessBlockByParentMsgBlocks(t, dag, dag.dagParams.GenesisBlock) // Add a child block to the previous two blocks - block3 := prepareAndProcessBlock(t, dag, block1, block2) + block3 := prepareAndProcessBlockByParentMsgBlocks(t, dag, block1, block2) // Clear the reachability store dag.reachabilityStore.loaded = map[daghash.Hash]*reachabilityData{} diff --git a/blockdag/test_utils.go b/blockdag/test_utils.go index e40952a0e..012194d97 100644 --- a/blockdag/test_utils.go +++ b/blockdag/test_utils.go @@ -15,6 +15,7 @@ import ( "sort" "strings" "sync" + "testing" "github.com/kaspanet/kaspad/util/subnetworkid" @@ -279,6 +280,28 @@ func PrepareBlockForTest(dag *BlockDAG, parentHashes []*daghash.Hash, transactio return block, nil } +// PrepareAndProcessBlockForTest prepares a block that points to the given parent +// hashes and process it. +func PrepareAndProcessBlockForTest(t *testing.T, dag *BlockDAG, parentHashes []*daghash.Hash, transactions []*wire.MsgTx) *wire.MsgBlock { + daghash.Sort(parentHashes) + block, err := PrepareBlockForTest(dag, parentHashes, transactions) + if err != nil { + t.Fatalf("error in PrepareBlockForTest: %s", err) + } + utilBlock := util.NewBlock(block) + isOrphan, isDelayed, err := dag.ProcessBlock(utilBlock, BFNoPoWCheck) + if err != nil { + t.Fatalf("unexpected error in ProcessBlock: %s", err) + } + if isDelayed { + t.Fatalf("block is too far in the future") + } + if isOrphan { + t.Fatalf("block was unexpectedly orphan") + } + return block +} + // generateDeterministicExtraNonceForTest returns a unique deterministic extra nonce for coinbase data, in order to create unique coinbase transactions. func generateDeterministicExtraNonceForTest() uint64 { extraNonceForTest++ diff --git a/blockdag/validate_test.go b/blockdag/validate_test.go index 4f65d3651..064387826 100644 --- a/blockdag/validate_test.go +++ b/blockdag/validate_test.go @@ -570,9 +570,9 @@ func TestValidateParents(t *testing.T) { } defer teardownFunc() - a := prepareAndProcessBlock(t, dag, dag.dagParams.GenesisBlock) - b := prepareAndProcessBlock(t, dag, a) - c := prepareAndProcessBlock(t, dag, dag.dagParams.GenesisBlock) + a := prepareAndProcessBlockByParentMsgBlocks(t, dag, dag.dagParams.GenesisBlock) + b := prepareAndProcessBlockByParentMsgBlocks(t, dag, a) + c := prepareAndProcessBlockByParentMsgBlocks(t, dag, dag.dagParams.GenesisBlock) aNode := nodeByMsgBlock(t, dag, a) bNode := nodeByMsgBlock(t, dag, b) diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index 58e799608..1e8ae7337 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -795,6 +795,57 @@ func TestDoubleSpends(t *testing.T) { testPoolMembership(tc, tx2, false, true, false) } +func TestDoubleSpendsFromDAG(t *testing.T) { + tc, spendableOuts, teardownFunc, err := newPoolHarness(t, &dagconfig.SimnetParams, 2, "TestDoubleSpendsFromDAG") + if err != nil { + t.Fatalf("unable to create test pool: %v", err) + } + defer teardownFunc() + harness := tc.harness + + tx, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest), 1) + if err != nil { + t.Fatalf("unable to create transaction: %v", err) + } + + dag := harness.txPool.cfg.DAG + blockdag.PrepareAndProcessBlockForTest(t, dag, dag.TipHashes(), []*wire.MsgTx{tx.MsgTx()}) + + // Check that a transaction that double spends the DAG UTXO set is orphaned. + doubleSpendTx, err := harness.createTx(spendableOuts[0], uint64(txRelayFeeForTest), 2) + if err != nil { + t.Fatalf("unable to create transaction: %v", err) + } + + _, err = harness.txPool.ProcessTransaction(doubleSpendTx, true, 0) + if err != nil { + t.Fatalf("ProcessTransaction: %s", err) + } + testPoolMembership(tc, doubleSpendTx, true, false, false) + + // If you send a transaction that some of its outputs exist in the DAG UTXO + // set, it won't be added to the orphan pool, and will completely get rejected + // from the mempool. + // This happens because transactions with the same ID as old transactions + // are not allowed as long as some of the old transaction outputs exist + // in the UTXO. + _, err = harness.txPool.ProcessTransaction(tx, true, 0) + var ruleErr RuleError + if ok := errors.As(err, &ruleErr); ok { + var txRuleErr TxRuleError + if ok := errors.As(ruleErr.Err, &txRuleErr); ok { + if txRuleErr.RejectCode != wire.RejectDuplicate { + t.Errorf("ProcessTransaction expected an %v reject code but got %v", wire.RejectDuplicate, txRuleErr.RejectCode) + } + } else { + t.Errorf("ProcessTransaction expected a ruleErr.Err to be a TxRuleError but got %v", err) + } + } else { + t.Errorf("ProcessTransaction expected a RuleError but got %v", err) + } + testPoolMembership(tc, tx, false, false, false) +} + //TestFetchTransaction checks that FetchTransaction //returns only transaction from the main pool and not from the orphan pool func TestFetchTransaction(t *testing.T) {