core,light: implement fast lookup of ancestor hashes

This commit is contained in:
Martin Holst Swende 2019-03-22 12:46:16 +01:00
parent c5c5e0dbe8
commit 7aa5ec37d2
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
7 changed files with 465 additions and 55 deletions

View file

@ -1679,6 +1679,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, []
switch status { switch status {
case CanonStatTy: case CanonStatTy:
bc.hc.hashHistory.Set(block.Header())
log.Debug("Inserted new block", "number", block.Number(), "hash", block.Hash(), log.Debug("Inserted new block", "number", block.Number(), "hash", block.Hash(),
"uncles", len(block.Uncles()), "txs", len(block.Transactions()), "gas", block.GasUsed(), "uncles", len(block.Uncles()), "txs", len(block.Transactions()), "gas", block.GasUsed(),
"elapsed", common.PrettyDuration(time.Since(start)), "elapsed", common.PrettyDuration(time.Since(start)),
@ -1967,6 +1968,8 @@ func (bc *BlockChain) reorg(oldBlock, newBlock *types.Block) error {
// Write lookup entries for hash based transaction/receipt searches // Write lookup entries for hash based transaction/receipt searches
rawdb.WriteTxLookupEntries(bc.db, newChain[i]) rawdb.WriteTxLookupEntries(bc.db, newChain[i])
addedTxs = append(addedTxs, newChain[i].Transactions()...) addedTxs = append(addedTxs, newChain[i].Transactions()...)
// Add to hash history
bc.hc.hashHistory.Set(newChain[i].Header())
} }
// When transactions get deleted from the database, the receipts that were // When transactions get deleted from the database, the receipts that were
// created in the fork must also be deleted // created in the fork must also be deleted
@ -2224,3 +2227,8 @@ func (bc *BlockChain) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscript
func (bc *BlockChain) SubscribeBlockProcessingEvent(ch chan<- bool) event.Subscription { func (bc *BlockChain) SubscribeBlockProcessingEvent(ch chan<- bool) event.Subscription {
return bc.scope.Track(bc.blockProcFeed.Subscribe(ch)) return bc.scope.Track(bc.blockProcFeed.Subscribe(ch))
} }
//GetAncestorHash return the hash of ancestor at the given number
func (bc *BlockChain) GetAncestorHash(ref *types.Header, target uint64) common.Hash {
return bc.hc.GetAncestorHash(ref, target)
}

View file

@ -31,8 +31,8 @@ type ChainContext interface {
// Engine retrieves the chain's consensus engine. // Engine retrieves the chain's consensus engine.
Engine() consensus.Engine Engine() consensus.Engine
// GetHeader returns the hash corresponding to their hash. // GetAncestorHash return the hash of ancestor at the given number
GetHeader(common.Hash, uint64) *types.Header GetAncestorHash(child *types.Header, number uint64) common.Hash
} }
// NewEVMContext creates a new context for use in the EVM. // NewEVMContext creates a new context for use in the EVM.
@ -60,27 +60,8 @@ func NewEVMContext(msg Message, header *types.Header, chain ChainContext, author
// GetHashFn returns a GetHashFunc which retrieves header hashes by number // GetHashFn returns a GetHashFunc which retrieves header hashes by number
func GetHashFn(ref *types.Header, chain ChainContext) func(n uint64) common.Hash { func GetHashFn(ref *types.Header, chain ChainContext) func(n uint64) common.Hash {
var cache map[uint64]common.Hash
return func(n uint64) common.Hash { return func(n uint64) common.Hash {
// If there's no hash cache yet, make one return chain.GetAncestorHash(ref, n)
if cache == nil {
cache = map[uint64]common.Hash{
ref.Number.Uint64() - 1: ref.ParentHash,
}
}
// Try to fulfill the request from the cache
if hash, ok := cache[n]; ok {
return hash
}
// Not cached, iterate the blocks and cache the hashes
for header := chain.GetHeader(ref.ParentHash, ref.Number.Uint64()-1); header != nil; header = chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) {
cache[header.Number.Uint64()-1] = header.ParentHash
if n == header.Number.Uint64()-1 {
return header.ParentHash
}
}
return common.Hash{}
} }
} }

173
core/hashbuffer.go Normal file
View file

@ -0,0 +1,173 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
// For EVM execution, we need around 256 items. We add a few more to allow reorgs.
// For mainnet, a couple more would suffice, but a few more added for
// testnets/private nets
// For LES, we use a larger buffer. 500K * 32 bytes = 16M
const hashBufferElems = 500000
var (
hashHitCounter = metrics.NewRegisteredGauge("chain/headerhash/hit", nil)
hashMissCounter = metrics.NewRegisteredGauge("chain/headerhash/miss", nil)
hashHeadGauge = metrics.NewRegisteredGauge("chain/headerhash/head", nil)
hashTailGauge = metrics.NewRegisteredGauge("chain/headerhash/tail", nil)
)
// hashBuffer implements a storage for chains of hashes, intended to be used for quick lookup of block hashes.
// Internally, it uses an array of hashes in a circular buffer.
// It enforces that all hashes added have a contiguous parent-child relation, and supports rollbacks
// It is thread-safe.
type hashBuffer struct {
// The data holds the hashes. The hashes are sequential, but also a
// circular buffer.
// The `head` points to the position of the latest hash.
// The parent, if present, is located 32 bytes back.
// [.., .., ..., head-2 , head-1, head, oldest, ... ]
data [hashBufferElems]common.Hash
head uint64 // index of hash for the head block
headNumber uint64 // The block number for head (the most recent block)
tailNumber uint64 // The block number for tail (the oldest block)
mu sync.RWMutex
}
// newHashBuffer creates a new storage with a header in it.
// Since we take a header here, a hash storage can never be empty.
// This makes things easier later on (in Set)
func newHashBuffer(header *types.Header) *hashBuffer {
return &hashBuffer{
headNumber: header.Number.Uint64(),
tailNumber: header.Number.Uint64(),
data: [hashBufferElems]common.Hash{header.Hash()},
}
}
// Get locates the hash for the requested number
func (hs *hashBuffer) Get(number uint64) (common.Hash, bool) {
hs.mu.RLock()
defer hs.mu.RUnlock()
if !hs.has(number) {
hashMissCounter.Inc(1)
return common.Hash{}, false
}
hashHitCounter.Inc(1)
distance := hs.headNumber - number
index := (hs.head + hashBufferElems - distance) % hashBufferElems
return hs.data[index], true
}
// has returns if the storage has a hash for the given number
func (hs *hashBuffer) has(number uint64) bool {
return number <= hs.headNumber && number >= hs.tailNumber
}
// Contains checks if the hash at the given number matches the expected
func (hs *hashBuffer) Contains(number uint64, expected common.Hash) bool {
hs.mu.RLock()
defer hs.mu.RUnlock()
if hs.contains(number, expected) {
hashHitCounter.Inc(1)
return true
} else {
hashMissCounter.Inc(1)
return false
}
}
// contains is the non-concurrency safe internal version of Contains
func (hs *hashBuffer) contains(number uint64, expected common.Hash) bool {
if !hs.has(number) {
return false
}
distance := hs.headNumber - number
index := (hs.head + hashBufferElems - distance) % hashBufferElems
return hs.data[index] == expected
}
// Newest returns the most recent (number, hash) stored
func (hs *hashBuffer) Newest() (uint64, common.Hash) {
hs.mu.RLock()
defer hs.mu.RUnlock()
return hs.headNumber, hs.data[hs.head]
}
// Oldest returns the oldest (number, hash) found
func (hs *hashBuffer) Oldest() (uint64, common.Hash) {
hs.mu.RLock()
defer hs.mu.RUnlock()
distance := hs.headNumber - hs.tailNumber
index := (hs.head + hashBufferElems - distance) % hashBufferElems
return hs.tailNumber, hs.data[index]
}
// Set inserts a new header (hash) to the storage.
// If
// a) Header already exists, this is a no-op
// b) Number is occupied by other header, the new header replaces it, and also
// truncates any descendants
//
// If the new header does not have any ancestors, it replaces the entire storage.
func (hs *hashBuffer) Set(header *types.Header) {
var (
number = header.Number.Uint64()
index uint64
hash = header.Hash()
)
hs.mu.Lock()
defer hs.mu.Unlock()
if hs.contains(number-1, header.ParentHash) {
if hs.headNumber >= number {
distance := hs.headNumber - number
index = (hs.head + hashBufferElems - distance) % hashBufferElems
if hs.data[index] == hash {
return
}
// Continue by replacing this number and wipe descendants
} else {
// head is parent of this new header - regular append
index = (hs.head + 1) % hashBufferElems
}
} else {
// This should not normally happen, and indicates a programming error
log.Error("Hash storage wiping ancestors", "oldhead", hs.headNumber, "newhead", number, "oldtail", hs.tailNumber)
// Wipe ancestors
hs.tailNumber = number
}
hs.head = index
hs.headNumber = number
hs.data[hs.head] = hash
if number-hs.tailNumber == hashBufferElems {
// It's full, need to move the tail
hs.tailNumber++
}
hashTailGauge.Update(int64(hs.tailNumber))
hashHeadGauge.Update(int64(hs.headNumber))
}

204
core/hashbuffer_test.go Normal file
View file

@ -0,0 +1,204 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"bytes"
"math/big"
"math/rand"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
func init() {
rand.Seed(time.Now().Unix())
}
func dummyHeader(n int, prev common.Hash) *types.Header {
return &types.Header{
Number: new(big.Int).SetUint64(uint64(n)),
ParentHash: prev,
}
}
func uniqueHeader(n int, prev common.Hash) *types.Header {
return &types.Header{
Number: new(big.Int).SetUint64(uint64(n)),
ParentHash: prev,
GasUsed: rand.Uint64(),
}
}
// TestConsecutiveHashes does inserts and rollback, but uses contiguous chains
func TestConsecutiveHashes(t *testing.T) {
t.Parallel()
// This should we swapped out very quickly
hs := newHashBuffer(uniqueHeader(0, common.Hash{0xaa}))
parent := common.Hash{}
expected := make(map[int]common.Hash)
assertEmpty := func(num int) {
h, found := hs.Get(uint64(num))
if found {
t.Fatalf("expected %d not to be present", num)
}
if h != (common.Hash{}) {
t.Fatalf("expected empty hash, got %x", h)
}
}
// test 10 entries
for n := 1; n < 10; n++ {
h := dummyHeader(n, parent)
hs.Set(h)
if _, lh := hs.Newest(); lh != h.Hash() {
t.Fatalf("num %d, wrong last hash, got %x exp %x", n, lh, h.Hash())
}
expected[n] = h.Hash()
parent = h.Hash()
}
n, _ := hs.Oldest()
if n != 1 {
t.Fatalf("wrong oldest, expected %d got %d", 1, n)
}
for n := 1; n < 10; n++ {
got, _ := hs.Get(uint64(n))
exp := expected[n]
if got != exp {
t.Errorf("num %d, got %x expected %x", n, got, exp)
}
}
assertEmpty(11)
assertEmpty(0)
// Write another 300, overflowing the storage
for n := 10; n < hashBufferElems+10; n++ {
h := dummyHeader(n, parent)
hs.Set(h)
if _, lh := hs.Newest(); lh != h.Hash() {
t.Fatalf("num %d, wrong last hash, got %x exp %x", n, lh, h.Hash())
}
expected[n] = h.Hash()
parent = h.Hash()
}
x, _ := hs.Oldest()
if x != 10 {
t.Fatalf("wrong oldest, expected %d got %d", 10, x)
}
// The last 256 should be available
for n := hashBufferElems + 10 - 1; n > 10; n-- {
got, found := hs.Get(uint64(n))
exp := expected[n]
if !found {
t.Fatalf("expected %d to be found", n)
}
if got != exp {
t.Fatalf("num %d, got %x expected %x", n, got, exp)
}
}
// The older ones should be flushed
for ; n > 0; n-- {
got, found := hs.Get(n)
if found {
t.Fatalf("expected %d to be flushed", n)
}
if got != (common.Hash{}) {
t.Fatalf("expected empty hash, got %x", got)
}
}
}
func TestHashStorageNonContiguous(t *testing.T) {
hs := newHashBuffer(uniqueHeader(0, common.Hash{0xff}))
parent := common.Hash{}
expected := make(map[int]common.Hash)
for n := 1; n < 10; n++ {
hs.Set(uniqueHeader(n, parent))
hdr := uniqueHeader(n, parent)
hs.Set(hdr)
parent = hdr.Hash()
expected[n] = hdr.Hash()
}
n, _ := hs.Oldest()
if n != 1 {
t.Fatalf("wrong oldest, expected %d got %d", 1, n)
}
for n := 1; n < 10; n++ {
got, _ := hs.Get(uint64(n))
exp := expected[n]
if got != exp {
t.Errorf("num %d, got %x expected %x", n, got, exp)
}
}
// 9 headers there [ 1,2,3,4a,5,6,7,8,9]
// Setting a new in the middle should change it to
// [ 1, 2, 3, 4b]
{
parent = expected[4]
hdr := uniqueHeader(5, parent)
hs.Set(hdr)
if hs.headNumber != 5 {
t.Fatalf("expected head num 5, got %d", hs.headNumber)
}
got, found := hs.Get(5)
if !found {
t.Fatalf("expected hash to exist")
}
if !bytes.Equal(got[:], hdr.Hash().Bytes()) {
t.Fatalf("expected %x, got %x", hdr.Hash(), got)
}
}
// Set a totally new header at 3, should clean out everything else
{
hdr := uniqueHeader(4, common.Hash{0x1})
hs.Set(hdr)
if hs.headNumber != 4 {
t.Fatalf("expected head num 4, got %d", hs.headNumber)
}
if hs.tailNumber != 4 {
t.Fatalf("expected head num 4, got %d", hs.tailNumber)
}
if _, exist := hs.Get(3); exist {
t.Fatalf("should be gone: %d", 3)
}
if _, exist := hs.Get(5); exist {
t.Fatalf("should be gone: %d", 5)
}
if _, exist := hs.Get(0); exist {
t.Fatalf("should be gone: %d", 0)
}
// 4 should be there
got, found := hs.Get(4)
if !found {
t.Fatalf("expected hash to exist")
}
if !bytes.Equal(got[:], hdr.Hash().Bytes()) {
t.Fatalf("expected %x, got %x", hdr.Hash(), got)
}
}
}

View file

@ -60,6 +60,8 @@ type HeaderChain struct {
tdCache *lru.Cache // Cache for the most recent block total difficulties tdCache *lru.Cache // Cache for the most recent block total difficulties
numberCache *lru.Cache // Cache for the most recent block numbers numberCache *lru.Cache // Cache for the most recent block numbers
hashHistory *hashBuffer // Cache for recent hashes
procInterrupt func() bool procInterrupt func() bool
rand *mrand.Rand rand *mrand.Rand
@ -96,6 +98,7 @@ func NewHeaderChain(chainDb ethdb.Database, config *params.ChainConfig, engine c
if hc.genesisHeader == nil { if hc.genesisHeader == nil {
return nil, ErrNoGenesis return nil, ErrNoGenesis
} }
hc.hashHistory = newHashBuffer(hc.genesisHeader)
hc.currentHeader.Store(hc.genesisHeader) hc.currentHeader.Store(hc.genesisHeader)
if head := rawdb.ReadHeadBlockHash(chainDb); head != (common.Hash{}) { if head := rawdb.ReadHeadBlockHash(chainDb); head != (common.Hash{}) {
@ -287,6 +290,7 @@ func (hc *HeaderChain) InsertHeaderChain(chain []*types.Header, writeHeader WhCa
if err := writeHeader(header); err != nil { if err := writeHeader(header); err != nil {
return i, err return i, err
} }
hc.hashHistory.Set(header)
stats.processed++ stats.processed++
} }
// Report some public statistics so the user has a clue what's going on // Report some public statistics so the user has a clue what's going on
@ -339,35 +343,19 @@ func (hc *HeaderChain) GetAncestor(hash common.Hash, number, ancestor uint64, ma
if ancestor > number { if ancestor > number {
return common.Hash{}, 0 return common.Hash{}, 0
} }
if ancestor == 1 { ref := hc.GetHeader(hash, number)
// in this case it is cheaper to just read the header if ref == nil {
if header := hc.GetHeader(hash, number); header != nil {
return header.ParentHash, number - 1
} else {
return common.Hash{}, 0 return common.Hash{}, 0
} }
if number == 0 {
return ref.Hash(), number
} }
for ancestor != 0 { target := number - ancestor
if rawdb.ReadCanonicalHash(hc.chainDb, number) == hash { ancestorHash := hc.GetAncestorHash(ref, target)
ancestorHash := rawdb.ReadCanonicalHash(hc.chainDb, number-ancestor) if ancestorHash == (common.Hash{}) {
if rawdb.ReadCanonicalHash(hc.chainDb, number) == hash {
number -= ancestor
return ancestorHash, number
}
}
if *maxNonCanonical == 0 {
return common.Hash{}, 0 return common.Hash{}, 0
} }
*maxNonCanonical-- return ancestorHash, target
ancestor--
header := hc.GetHeader(hash, number)
if header == nil {
return common.Hash{}, 0
}
hash = header.ParentHash
number--
}
return hash, number
} }
// GetTd retrieves a block's total difficulty in the canonical chain from the // GetTd retrieves a block's total difficulty in the canonical chain from the
@ -464,6 +452,7 @@ func (hc *HeaderChain) SetCurrentHeader(head *types.Header) {
hc.currentHeader.Store(head) hc.currentHeader.Store(head)
hc.currentHeaderHash = head.Hash() hc.currentHeaderHash = head.Hash()
hc.hashHistory.Set(head)
headHeaderGauge.Update(head.Number.Int64()) headHeaderGauge.Update(head.Number.Int64())
} }
@ -543,3 +532,49 @@ func (hc *HeaderChain) Engine() consensus.Engine { return hc.engine }
func (hc *HeaderChain) GetBlock(hash common.Hash, number uint64) *types.Block { func (hc *HeaderChain) GetBlock(hash common.Hash, number uint64) *types.Block {
return nil return nil
} }
// GetAncestorHash return the hash of ancestor at the given number
func (hc *HeaderChain) GetAncestorHash(ref *types.Header, target uint64) common.Hash {
number := ref.Number.Uint64() - 1
hash := ref.ParentHash
if target == number {
return hash
}
if target > number {
// Should never happen
log.Error("Ancestor number must be <= descendant", "target", target, "descendant", number)
return common.Hash{}
}
var (
maxNonCanonLookups = uint64(100)
)
if hashHistoryTail, _ := hc.hashHistory.Oldest(); hashHistoryTail <= target {
// Iterate the chain until we hit the target or we hit a ancestor
// within the storage
for ; !hc.hashHistory.Contains(number, hash); maxNonCanonLookups-- {
if maxNonCanonLookups == 0 {
return common.Hash{}
}
header := hc.GetHeader(hash, number)
if header == nil {
return common.Hash{}
}
number, hash = header.Number.Uint64()-1, header.ParentHash
if number == target {
return hash
}
}
// The hash storage has the right ancestor chain
if h, ok := hc.hashHistory.Get(target); ok {
return h
}
}
// At this point, we have failed to find the ancestor in our hash history lookup.
// Either it's some very long sidefork, or the 'ref' is a very old header, too
// old to be in the history. In that case, it should exist in the canon chain.
if rawdb.ReadCanonicalHash(hc.chainDb, number) == hash {
return rawdb.ReadCanonicalHash(hc.chainDb, target)
}
// They are requesting a very old header which is not in the canon chain,
return common.Hash{}
}

View file

@ -179,18 +179,18 @@ func testGetBlockHeaders(t *testing.T, protocol int) {
headers = append(headers, pm.blockchain.GetBlockByHash(hash).Header()) headers = append(headers, pm.blockchain.GetBlockByHash(hash).Header())
} }
// Send the hash request and verify the response // Send the hash request and verify the response
p2p.Send(peer.app, 0x03, tt.query) p2p.Send(peer.app, GetBlockHeadersMsg, tt.query)
if err := p2p.ExpectMsg(peer.app, 0x04, headers); err != nil { if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, headers); err != nil {
t.Errorf("test %d: headers mismatch: %v", i, err) t.Fatalf("test %d: headers mismatch: %v", i, err)
} }
// If the test used number origins, repeat with hashes as the too // If the test used number origins, repeat with hashes as the too
if tt.query.Origin.Hash == (common.Hash{}) { if tt.query.Origin.Hash == (common.Hash{}) {
if origin := pm.blockchain.GetBlockByNumber(tt.query.Origin.Number); origin != nil { if origin := pm.blockchain.GetBlockByNumber(tt.query.Origin.Number); origin != nil {
tt.query.Origin.Hash, tt.query.Origin.Number = origin.Hash(), 0 tt.query.Origin.Hash, tt.query.Origin.Number = origin.Hash(), 0
p2p.Send(peer.app, 0x03, tt.query) p2p.Send(peer.app, GetBlockHeadersMsg, tt.query)
if err := p2p.ExpectMsg(peer.app, 0x04, headers); err != nil { if err := p2p.ExpectMsg(peer.app, BlockHeadersMsg, headers); err != nil {
t.Errorf("test %d: headers mismatch: %v", i, err) t.Fatalf("test %d: headers mismatch: %v", i, err)
} }
} }
} }

View file

@ -542,3 +542,12 @@ func (lc *LightChain) DisableCheckFreq() {
func (lc *LightChain) EnableCheckFreq() { func (lc *LightChain) EnableCheckFreq() {
atomic.StoreInt32(&lc.disableCheckFreq, 0) atomic.StoreInt32(&lc.disableCheckFreq, 0)
} }
// GetAncestorHash return the hash of ancestor at the given number
// This implementation is slow, since it does not use a hashstorage
// lookup like blockchain. However, the light clients does not require
// fast lookups for EVM execution. This should probably be improved if this
// method becomes more used.
func (lc *LightChain) GetAncestorHash(ref *types.Header, target uint64) common.Hash {
return lc.hc.GetAncestorHash(ref, target)
}