diff --git a/core/blockchain.go b/core/blockchain.go index b6605e66c..a3121cfd3 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1084,15 +1084,21 @@ func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) { if len(chain) == 0 { return 0, nil } + // Remove already known canon-blocks + var ( + block, prev *types.Block + ) // Do a sanity check that the provided chain is actually ordered and linked for i := 1; i < len(chain); i++ { - if chain[i].NumberU64() != chain[i-1].NumberU64()+1 || chain[i].ParentHash() != chain[i-1].Hash() { + block = chain[i] + prev = chain[i-1] + if block.NumberU64() != prev.NumberU64()+1 || block.ParentHash() != prev.Hash() { // Chain broke ancestry, log a message (programming error) and skip insertion - log.Error("Non contiguous block insert", "number", chain[i].Number(), "hash", chain[i].Hash(), - "parent", chain[i].ParentHash(), "prevnumber", chain[i-1].Number(), "prevhash", chain[i-1].Hash()) + log.Error("Non contiguous block insert", "number", block.Number(), "hash", block.Hash(), + "parent", block.ParentHash(), "prevnumber", prev.Number(), "prevhash", prev.Hash()) - return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x…], item %d is #%d [%x…] (parent [%x…])", i-1, chain[i-1].NumberU64(), - chain[i-1].Hash().Bytes()[:4], i, chain[i].NumberU64(), chain[i].Hash().Bytes()[:4], chain[i].ParentHash().Bytes()[:4]) + return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x…], item %d is #%d [%x…] (parent [%x…])", i-1, prev.NumberU64(), + prev.Hash().Bytes()[:4], i, block.NumberU64(), block.Hash().Bytes()[:4], block.ParentHash().Bytes()[:4]) } } // Pre-checks passed, start the full block imports @@ -1146,6 +1152,22 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] it := newInsertIterator(chain, results, bc.Validator()) block, err := it.next() + + // Left-trim all the known blocks + if err == ErrKnownBlock { + // First block (and state) is known + // 1. We did a roll-back, and should now do a re-import + // 2. The block is stored as a sidechain, and is lying about it's stateroot, and passes a stateroot + // from the canonical chain, which has not been verified. + // Skip all known blocks that are behind us + current := bc.CurrentBlock().NumberU64() + for block != nil && err == ErrKnownBlock && current >= block.NumberU64() { + stats.ignored++ + block, err = it.next() + } + // Falls through to the block import + } + switch { // First block is pruned, insert as sidechain and reorg only if TD grows enough case err == consensus.ErrPrunedAncestor: @@ -1165,20 +1187,6 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] // If there are any still remaining, mark as ignored return it.index, events, coalescedLogs, err - // First block (and state) is known - // 1. We did a roll-back, and should now do a re-import - // 2. The block is stored as a sidechain, and is lying about it's stateroot, and passes a stateroot - // from the canonical chain, which has not been verified. - case err == ErrKnownBlock: - // Skip all known blocks that behind us - current := bc.CurrentBlock().NumberU64() - - for block != nil && err == ErrKnownBlock && current >= block.NumberU64() { - stats.ignored++ - block, err = it.next() - } - // Falls through to the block import - // Some other error occurred, abort case err != nil: stats.ignored += len(it.chain) @@ -1304,7 +1312,12 @@ func (bc *BlockChain) insertSidechain(block *types.Block, it *insertIterator) (i for ; block != nil && (err == consensus.ErrPrunedAncestor); block, err = it.next() { // Check the canonical state root for that number if number := block.NumberU64(); current.NumberU64() >= number { - if canonical := bc.GetBlockByNumber(number); canonical != nil && canonical.Root() == block.Root() { + canonical := bc.GetBlockByNumber(number) + if canonical != nil && canonical.Hash() == block.Hash() { + // Not a sidechain block, this is a re-import of a canon block which has it's state pruned + continue + } + if canonical != nil && canonical.Root() == block.Root() { // This is most likely a shadow-state attack. When a fork is imported into the // database, and it eventually reaches a block height which is not pruned, we // just found that the state already exist! This means that the sidechain block diff --git a/core/blockchain_test.go b/core/blockchain_test.go index a16b3ba8a..1ac7b03fc 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -1521,3 +1521,85 @@ func TestLowDiffLongChain(t *testing.T) { header = chain.GetHeader(header.ParentHash, number-1) } } + +// Tests that importing a sidechain (S), where +// - S is sidechain, containing blocks [Sn...Sm] +// - C is canon chain, containing blocks [G..Cn..Cm] +// - A common ancestor is placed at prune-point + blocksBetweenCommonAncestorAndPruneblock +// - The sidechain S is prepended with numCanonBlocksInSidechain blocks from the canon chain +func testSideImport(t *testing.T, numCanonBlocksInSidechain, blocksBetweenCommonAncestorAndPruneblock int) { + + // Generate a canonical chain to act as the main dataset + engine := ethash.NewFaker() + db := ethdb.NewMemDatabase() + genesis := new(Genesis).MustCommit(db) + + // Generate and import the canonical chain + blocks, _ := GenerateChain(params.TestChainConfig, genesis, engine, db, 2*triesInMemory, nil) + diskdb := ethdb.NewMemDatabase() + new(Genesis).MustCommit(diskdb) + chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + if n, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + + lastPrunedIndex := len(blocks) - triesInMemory - 1 + lastPrunedBlock := blocks[lastPrunedIndex] + firstNonPrunedBlock := blocks[len(blocks)-triesInMemory] + + // Verify pruning of lastPrunedBlock + if chain.HasBlockAndState(lastPrunedBlock.Hash(), lastPrunedBlock.NumberU64()) { + t.Errorf("Block %d not pruned", lastPrunedBlock.NumberU64()) + } + // Verify firstNonPrunedBlock is not pruned + if !chain.HasBlockAndState(firstNonPrunedBlock.Hash(), firstNonPrunedBlock.NumberU64()) { + t.Errorf("Block %d pruned", firstNonPrunedBlock.NumberU64()) + } + // Generate the sidechain + // First block should be a known block, block after should be a pruned block. So + // canon(pruned), side, side... + + // Generate fork chain, make it longer than canon + parentIndex := lastPrunedIndex + blocksBetweenCommonAncestorAndPruneblock + parent := blocks[parentIndex] + fork, _ := GenerateChain(params.TestChainConfig, parent, engine, db, 2*triesInMemory, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{2}) + }) + // Prepend the parent(s) + var sidechain []*types.Block + for i := numCanonBlocksInSidechain; i > 0; i-- { + sidechain = append(sidechain, blocks[parentIndex+1-i]) + } + sidechain = append(sidechain, fork...) + _, err = chain.InsertChain(sidechain) + if err != nil { + t.Errorf("Got error, %v", err) + } + head := chain.CurrentBlock() + if got := fork[len(fork)-1].Hash(); got != head.Hash() { + t.Fatalf("head wrong, expected %x got %x", head.Hash(), got) + } +} + +// Tests that importing a sidechain (S), where +// - S is sidechain, containing blocks [Sn...Sm] +// - C is canon chain, containing blocks [G..Cn..Cm] +// - The common ancestor Cc is pruned +// - The first block in S: Sn, is == Cn +// That is: the sidechain for import contains some blocks already present in canon chain. +// So the blocks are +// [ Cn, Cn+1, Cc, Sn+3 ... Sm] +// ^ ^ ^ pruned +func TestPrunedImportSide(t *testing.T) { + //glogger := log.NewGlogHandler(log.StreamHandler(os.Stdout, log.TerminalFormat(false))) + //glogger.Verbosity(3) + //log.Root().SetHandler(log.Handler(glogger)) + testSideImport(t, 3, 3) + testSideImport(t, 3, -3) + testSideImport(t, 10, 0) + testSideImport(t, 1, 10) + testSideImport(t, 1, -10) +}