diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go index 4107820a3c..6b7269fe95 100644 --- a/blockchain/chain_test.go +++ b/blockchain/chain_test.go @@ -143,6 +143,9 @@ func TestForceHeadReorg(t *testing.T) { // // accepted expects the block to be accepted to the main chain. // + // rejected expects the block to be rejected with the provided error + // code. + // // expectTip expects the provided block to be the current tip of the // main chain. // @@ -154,6 +157,10 @@ func TestForceHeadReorg(t *testing.T) { // of the main chain from the given block to the given block. An error // will result if the provided from block is not actually the current // tip. + // + // 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. accepted := func() { msgBlock := g.Tip() blockHeight := msgBlock.Header.Height @@ -182,6 +189,36 @@ func TestForceHeadReorg(t *testing.T) { block.Hash(), blockHeight, isOrphan) } } + rejected := func(code ErrorCode) { + msgBlock := g.Tip() + blockHeight := msgBlock.Header.Height + block := dcrutil.NewBlock(msgBlock) + t.Logf("Testing block %s (hash %s, height %d)", g.TipName(), + block.Hash(), blockHeight) + + _, _, err := chain.ProcessBlock(block, BFNone) + if err == nil { + t.Fatalf("block %q (hash %s, height %d) should not "+ + "have been accepted", g.TipName(), block.Hash(), + blockHeight) + } + + // Ensure the error code is of the expected type and the reject + // code matches the value specified in the test instance. + rerr, ok := err.(RuleError) + if !ok { + t.Fatalf("block %q (hash %s, height %d) returned "+ + "unexpected error type -- got %T, want "+ + "blockchain.RuleError", g.TipName(), + block.Hash(), blockHeight, err) + } + if rerr.ErrorCode != code { + t.Fatalf("block %q (hash %s, height %d) does not have "+ + "expected reject code -- got %v, want %v", + g.TipName(), block.Hash(), blockHeight, + rerr.ErrorCode, code) + } + } expectTip := func(tipName string) { // Ensure hash and height match. wantTip := g.BlockByName(tipName) @@ -227,6 +264,11 @@ func TestForceHeadReorg(t *testing.T) { forceTipReorg := func(fromTipName, toTipName string) { from := g.BlockByName(fromTipName) to := g.BlockByName(toTipName) + t.Logf("Testing forced reorg from %s (hash %s, height %d) "+ + "to %s (hash %s, height %d)", fromTipName, + from.BlockHash(), from.Header.Height, toTipName, + to.BlockHash(), to.Header.Height) + err = chain.ForceHeadReorganization(from.BlockHash(), to.BlockHash()) if err != nil { t.Fatalf("failed to force header reorg from block %q "+ @@ -236,6 +278,44 @@ func TestForceHeadReorg(t *testing.T) { to.Header.Height, err) } } + rejectForceTipReorg := func(fromTipName, toTipName string, code ErrorCode) { + from := g.BlockByName(fromTipName) + to := g.BlockByName(toTipName) + t.Logf("Testing forced reorg from %s (hash %s, height %d) "+ + "to %s (hash %s, height %d)", fromTipName, + from.BlockHash(), from.Header.Height, toTipName, + to.BlockHash(), to.Header.Height) + + err = chain.ForceHeadReorganization(from.BlockHash(), to.BlockHash()) + if err == nil { + t.Fatalf("forced header reorg from block %q (hash %s, "+ + "height %d) to block %q (hash %s, height %d) "+ + "should have failed", fromTipName, from.BlockHash(), + from.Header.Height, toTipName, to.BlockHash(), + to.Header.Height) + } + + // Ensure the error code is of the expected type and the reject + // code matches the value specified in the test instance. + rerr, ok := err.(RuleError) + if !ok { + 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 { + 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) + } + } // Shorter versions of useful params for convenience. ticketsPerBlock := params.TicketsPerBlock @@ -350,23 +430,53 @@ func TestForceHeadReorg(t *testing.T) { // Forced header reorganization test. // --------------------------------------------------------------------- - // Start by building a couple of blocks at current tip (value in parens - // is which output is spent): + // Start by building a block at current tip (value in parens is which + // output is spent): // - // ... -> b1(0) -> b2(1) + // ... -> b1(0) g.NextBlock("b1", outs[0], ticketOuts[0]) accepted() - g.NextBlock("b2", outs[1], ticketOuts[1]) - accepted() + // Create a fork from b1 with an invalid block due to committing to an + // invalid number of votes. Since verifying the header commitment is a + // basic sanity check, no entry will be added to the block index. + // + // ... -> b1(0) -> b2(1) + // \-> b2bad0(1) + g.SetTip("b1") + g.NextBlock("b2bad0", outs[1], ticketOuts[1], func(b *wire.MsgBlock) { + b.Header.Voters++ + }) + rejected(ErrTooManyVotes) + + // Create a fork from b1 with an invalid block due to committing to an + // invalid input amount. Since verifying the fraud proof necessarily + // requires access to the inputs, this will trigger the failure late + // enough to ensure an entry is added to the block index. Further, + // since the block is attempting to extend the main chain it will also + // be fully checked and thus will be known invalid. + // + // ... -> b1(0) + // \-> b2bad1(1) + g.SetTip("b1") + g.NextBlock("b2bad1", outs[1], ticketOuts[1], func(b *wire.MsgBlock) { + b.Transactions[1].TxIn[0].ValueIn-- + }) + rejected(ErrFraudAmountIn) - // Create some forks from b1. There should not be a reorg since b2 was - // seen first. + // Create some forks from b1. There should not be a reorg since b1 is + // the current tip and b2 is seen first. // // ... -> b1(0) -> b2(1) // \-> b3(1) // \-> b4(1) // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + g.SetTip("b1") + g.NextBlock("b2", outs[1], ticketOuts[1]) + accepted() + g.SetTip("b1") g.NextBlock("b3", outs[1], ticketOuts[1]) acceptedToSideChainWithExpectedTip("b2") @@ -379,12 +489,37 @@ func TestForceHeadReorg(t *testing.T) { g.NextBlock("b5", outs[1], ticketOuts[1]) acceptedToSideChainWithExpectedTip("b2") + // Create a fork from b1 with an invalid block due to committing to an + // invalid input amount. Since verifying the fraud proof necessarily + // requires access to the inputs, this will trigger the failure late + // enough to ensure an entry is added to the block index. Further, + // since the block is not attempting to extend the main chain it will + // not be fully checked and thus will not yet have a known validation + // status. + // + // ... -> b1(0) -> b2(1) + // \-> b3(1) + // \-> b4(1) + // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) + g.SetTip("b1") + g.NextBlock("b2bad2", outs[1], ticketOuts[1], func(b *wire.MsgBlock) { + b.Transactions[1].TxIn[0].ValueIn-- + }) + acceptedToSideChainWithExpectedTip("b2") + // Force tip reorganization to b3. // // ... -> b1(0) -> b3(1) // \-> b2(1) // \-> b4(1) // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) + g.SetTip("b1") forceTipReorg("b2", "b3") expectTip("b3") @@ -394,6 +529,9 @@ func TestForceHeadReorg(t *testing.T) { // \-> b2(1) // \-> b3(1) // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) forceTipReorg("b3", "b4") expectTip("b4") @@ -403,8 +541,64 @@ func TestForceHeadReorg(t *testing.T) { // \-> b2(1) // \-> b3(1) // \-> b4(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) forceTipReorg("b4", "b5") expectTip("b5") + + // Force tip reorganization back to b3 to ensure cached validation + // results are exercised. + // + // ... -> b1(0) -> b3(1) + // \-> b2(1) + // \-> b4(1) + // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) + forceTipReorg("b5", "b3") + expectTip("b3") + + // Attempt to force tip reorganization from a block that is not the + // current tip. This should fail since that is not allowed. + // + // ... -> b1(0) -> b3(1) + // \-> b2(1) + // \-> b4(1) + // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) + rejectForceTipReorg("b2", "b4", ErrForceReorgWrongChain) + expectTip("b3") + + // Attempt to force tip reorganization to an invalid block that does + // not have an entry in the block index. + // + // ... -> b1(0) -> b3(1) + // \-> b2(1) + // \-> b4(1) + // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) + rejectForceTipReorg("b3", "b2bad0", ErrForceReorgMissingChild) + expectTip("b3") + + // Attempt to force tip reorganization to an invalid block that has an + // entry in the block index, but is not already known to be invalid. + // + // + // ... -> b1(0) -> b3(1) + // \-> b2(1) + // \-> b4(1) + // \-> b5(1) + // \-> b2bad0(1) + // \-> b2bad1(1) + // \-> b2bad2(1) + rejectForceTipReorg("b3", "b2bad2", ErrFraudAmountIn) + expectTip("b3") } // locatorHashes is a convenience function that returns the hashes for all of