Skip to content

Commit 61c99f5

Browse files
holimanfjl
authored andcommitted
core, eth: improve delivery speed on header requests (ethereum#23105)
This PR reduces the amount of work we do when answering header queries, e.g. when a peer is syncing from us. For some items, e.g block bodies, when we read the rlp-data from database, we plug it directly into the response package. We didn't do that for headers, but instead read headers-rlp, decode to types.Header, and re-encode to rlp. This PR changes that to keep it in RLP-form as much as possible. When a node is syncing from us, it typically requests 192 contiguous headers. On master it has the following effect: - For headers not in ancient: 2 db lookups. One for translating hash->number (even though the request is by number), and another for reading by hash (this latter one is sometimes cached). - For headers in ancient: 1 file lookup/syscall for translating hash->number (even though the request is by number), and another for reading the header itself. After this, it also performes a hashing of the header, to ensure that the hash is what it expected. In this PR, I instead move the logic for "give me a sequence of blocks" into the lower layers, where the database can determine how and what to read from leveldb and/or ancients. There are basically four types of requests; three of them are improved this way. The fourth, by hash going backwards, is more tricky to optimize. However, since we know that the gap is 0, we can look up by the parentHash, and stlil shave off all the number->hash lookups. The gapped collection can be optimized similarly, as a follow-up, at least in three out of four cases. Co-authored-by: Felix Lange <fjl@twurst.com>
1 parent d00ef91 commit 61c99f5

File tree

13 files changed

+368
-24
lines changed

13 files changed

+368
-24
lines changed

core/blockchain_reader.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ func (bc *BlockChain) GetHeaderByNumber(number uint64) *types.Header {
7373
return bc.hc.GetHeaderByNumber(number)
7474
}
7575

76+
// GetHeadersFrom returns a contiguous segment of headers, in rlp-form, going
77+
// backwards from the given number.
78+
func (bc *BlockChain) GetHeadersFrom(number, count uint64) []rlp.RawValue {
79+
return bc.hc.GetHeadersFrom(number, count)
80+
}
81+
7682
// GetBody retrieves a block body (transactions and uncles) from the database by
7783
// hash, caching it if found.
7884
func (bc *BlockChain) GetBody(hash common.Hash) *types.Body {

core/headerchain.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/ethereum/go-ethereum/ethdb"
3434
"github.com/ethereum/go-ethereum/log"
3535
"github.com/ethereum/go-ethereum/params"
36+
"github.com/ethereum/go-ethereum/rlp"
3637
lru "github.com/hashicorp/golang-lru"
3738
)
3839

@@ -498,6 +499,46 @@ func (hc *HeaderChain) GetHeaderByNumber(number uint64) *types.Header {
498499
return hc.GetHeader(hash, number)
499500
}
500501

502+
// GetHeadersFrom returns a contiguous segment of headers, in rlp-form, going
503+
// backwards from the given number.
504+
// If the 'number' is higher than the highest local header, this method will
505+
// return a best-effort response, containing the headers that we do have.
506+
func (hc *HeaderChain) GetHeadersFrom(number, count uint64) []rlp.RawValue {
507+
// If the request is for future headers, we still return the portion of
508+
// headers that we are able to serve
509+
if current := hc.CurrentHeader().Number.Uint64(); current < number {
510+
if count > number-current {
511+
count -= number - current
512+
number = current
513+
} else {
514+
return nil
515+
}
516+
}
517+
var headers []rlp.RawValue
518+
// If we have some of the headers in cache already, use that before going to db.
519+
hash := rawdb.ReadCanonicalHash(hc.chainDb, number)
520+
if hash == (common.Hash{}) {
521+
return nil
522+
}
523+
for count > 0 {
524+
header, ok := hc.headerCache.Get(hash)
525+
if !ok {
526+
break
527+
}
528+
h := header.(*types.Header)
529+
rlpData, _ := rlp.EncodeToBytes(h)
530+
headers = append(headers, rlpData)
531+
hash = h.ParentHash
532+
count--
533+
number--
534+
}
535+
// Read remaining from db
536+
if count > 0 {
537+
headers = append(headers, rawdb.ReadHeaderRange(hc.chainDb, number, count)...)
538+
}
539+
return headers
540+
}
541+
501542
func (hc *HeaderChain) GetCanonicalHash(number uint64) common.Hash {
502543
return rawdb.ReadCanonicalHash(hc.chainDb, number)
503544
}

core/rawdb/accessors_chain.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,56 @@ func WriteFastTxLookupLimit(db ethdb.KeyValueWriter, number uint64) {
279279
}
280280
}
281281

282+
// ReadHeaderRange returns the rlp-encoded headers, starting at 'number', and going
283+
// backwards towards genesis. This method assumes that the caller already has
284+
// placed a cap on count, to prevent DoS issues.
285+
// Since this method operates in head-towards-genesis mode, it will return an empty
286+
// slice in case the head ('number') is missing. Hence, the caller must ensure that
287+
// the head ('number') argument is actually an existing header.
288+
//
289+
// N.B: Since the input is a number, as opposed to a hash, it's implicit that
290+
// this method only operates on canon headers.
291+
func ReadHeaderRange(db ethdb.Reader, number uint64, count uint64) []rlp.RawValue {
292+
var rlpHeaders []rlp.RawValue
293+
if count == 0 {
294+
return rlpHeaders
295+
}
296+
i := number
297+
if count-1 > number {
298+
// It's ok to request block 0, 1 item
299+
count = number + 1
300+
}
301+
limit, _ := db.Ancients()
302+
// First read live blocks
303+
if i >= limit {
304+
// If we need to read live blocks, we need to figure out the hash first
305+
hash := ReadCanonicalHash(db, number)
306+
for ; i >= limit && count > 0; i-- {
307+
if data, _ := db.Get(headerKey(i, hash)); len(data) > 0 {
308+
rlpHeaders = append(rlpHeaders, data)
309+
// Get the parent hash for next query
310+
hash = types.HeaderParentHashFromRLP(data)
311+
} else {
312+
break // Maybe got moved to ancients
313+
}
314+
count--
315+
}
316+
}
317+
if count == 0 {
318+
return rlpHeaders
319+
}
320+
// read remaining from ancients
321+
max := count * 700
322+
data, err := db.AncientRange(freezerHeaderTable, i+1-count, count, max)
323+
if err == nil && uint64(len(data)) == count {
324+
// the data is on the order [h, h+1, .., n] -- reordering needed
325+
for i := range data {
326+
rlpHeaders = append(rlpHeaders, data[len(data)-1-i])
327+
}
328+
}
329+
return rlpHeaders
330+
}
331+
282332
// ReadHeaderRLP retrieves a block header in its raw RLP database encoding.
283333
func ReadHeaderRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue {
284334
var data []byte

core/rawdb/accessors_chain_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,3 +883,67 @@ func BenchmarkDecodeRLPLogs(b *testing.B) {
883883
}
884884
})
885885
}
886+
887+
func TestHeadersRLPStorage(t *testing.T) {
888+
// Have N headers in the freezer
889+
frdir, err := ioutil.TempDir("", "")
890+
if err != nil {
891+
t.Fatalf("failed to create temp freezer dir: %v", err)
892+
}
893+
defer os.Remove(frdir)
894+
895+
db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false)
896+
if err != nil {
897+
t.Fatalf("failed to create database with ancient backend")
898+
}
899+
defer db.Close()
900+
// Create blocks
901+
var chain []*types.Block
902+
var pHash common.Hash
903+
for i := 0; i < 100; i++ {
904+
block := types.NewBlockWithHeader(&types.Header{
905+
Number: big.NewInt(int64(i)),
906+
Extra: []byte("test block"),
907+
UncleHash: types.EmptyUncleHash,
908+
TxHash: types.EmptyRootHash,
909+
ReceiptHash: types.EmptyRootHash,
910+
ParentHash: pHash,
911+
})
912+
chain = append(chain, block)
913+
pHash = block.Hash()
914+
}
915+
var receipts []types.Receipts = make([]types.Receipts, 100)
916+
// Write first half to ancients
917+
WriteAncientBlocks(db, chain[:50], receipts[:50], big.NewInt(100))
918+
// Write second half to db
919+
for i := 50; i < 100; i++ {
920+
WriteCanonicalHash(db, chain[i].Hash(), chain[i].NumberU64())
921+
WriteBlock(db, chain[i])
922+
}
923+
checkSequence := func(from, amount int) {
924+
headersRlp := ReadHeaderRange(db, uint64(from), uint64(amount))
925+
if have, want := len(headersRlp), amount; have != want {
926+
t.Fatalf("have %d headers, want %d", have, want)
927+
}
928+
for i, headerRlp := range headersRlp {
929+
var header types.Header
930+
if err := rlp.DecodeBytes(headerRlp, &header); err != nil {
931+
t.Fatal(err)
932+
}
933+
if have, want := header.Number.Uint64(), uint64(from-i); have != want {
934+
t.Fatalf("wrong number, have %d want %d", have, want)
935+
}
936+
}
937+
}
938+
checkSequence(99, 20) // Latest block and 19 parents
939+
checkSequence(99, 50) // Latest block -> all db blocks
940+
checkSequence(99, 51) // Latest block -> one from ancients
941+
checkSequence(99, 52) // Latest blocks -> two from ancients
942+
checkSequence(50, 2) // One from db, one from ancients
943+
checkSequence(49, 1) // One from ancients
944+
checkSequence(49, 50) // All ancient ones
945+
checkSequence(99, 100) // All blocks
946+
checkSequence(0, 1) // Only genesis
947+
checkSequence(1, 1) // Only block 1
948+
checkSequence(1, 2) // Genesis + block 1
949+
}

core/types/block.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,21 @@ func (b *Block) Hash() common.Hash {
389389
}
390390

391391
type Blocks []*Block
392+
393+
// HeaderParentHashFromRLP returns the parentHash of an RLP-encoded
394+
// header. If 'header' is invalid, the zero hash is returned.
395+
func HeaderParentHashFromRLP(header []byte) common.Hash {
396+
// parentHash is the first list element.
397+
listContent, _, err := rlp.SplitList(header)
398+
if err != nil {
399+
return common.Hash{}
400+
}
401+
parentHash, _, err := rlp.SplitString(listContent)
402+
if err != nil {
403+
return common.Hash{}
404+
}
405+
if len(parentHash) != 32 {
406+
return common.Hash{}
407+
}
408+
return common.BytesToHash(parentHash)
409+
}

core/types/block_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,64 @@ func makeBenchBlock() *Block {
281281
}
282282
return NewBlock(header, txs, uncles, receipts, newHasher())
283283
}
284+
285+
func TestRlpDecodeParentHash(t *testing.T) {
286+
// A minimum one
287+
want := common.HexToHash("0x112233445566778899001122334455667788990011223344556677889900aabb")
288+
if rlpData, err := rlp.EncodeToBytes(Header{ParentHash: want}); err != nil {
289+
t.Fatal(err)
290+
} else {
291+
if have := HeaderParentHashFromRLP(rlpData); have != want {
292+
t.Fatalf("have %x, want %x", have, want)
293+
}
294+
}
295+
// And a maximum one
296+
// | Difficulty | dynamic| *big.Int | 0x5ad3c2c71bbff854908 (current mainnet TD: 76 bits) |
297+
// | Number | dynamic| *big.Int | 64 bits |
298+
// | Extra | dynamic| []byte | 65+32 byte (clique) |
299+
// | BaseFee | dynamic| *big.Int | 64 bits |
300+
mainnetTd := new(big.Int)
301+
mainnetTd.SetString("5ad3c2c71bbff854908", 16)
302+
if rlpData, err := rlp.EncodeToBytes(Header{
303+
ParentHash: want,
304+
Difficulty: mainnetTd,
305+
Number: new(big.Int).SetUint64(math.MaxUint64),
306+
Extra: make([]byte, 65+32),
307+
BaseFee: new(big.Int).SetUint64(math.MaxUint64),
308+
}); err != nil {
309+
t.Fatal(err)
310+
} else {
311+
if have := HeaderParentHashFromRLP(rlpData); have != want {
312+
t.Fatalf("have %x, want %x", have, want)
313+
}
314+
}
315+
// Also test a very very large header.
316+
{
317+
// The rlp-encoding of the heder belowCauses _total_ length of 65540,
318+
// which is the first to blow the fast-path.
319+
h := Header{
320+
ParentHash: want,
321+
Extra: make([]byte, 65041),
322+
}
323+
if rlpData, err := rlp.EncodeToBytes(h); err != nil {
324+
t.Fatal(err)
325+
} else {
326+
if have := HeaderParentHashFromRLP(rlpData); have != want {
327+
t.Fatalf("have %x, want %x", have, want)
328+
}
329+
}
330+
}
331+
{
332+
// Test some invalid erroneous stuff
333+
for i, rlpData := range [][]byte{
334+
nil,
335+
common.FromHex("0x"),
336+
common.FromHex("0x01"),
337+
common.FromHex("0x3031323334"),
338+
} {
339+
if have, want := HeaderParentHashFromRLP(rlpData), (common.Hash{}); have != want {
340+
t.Fatalf("invalid %d: have %x, want %x", i, have, want)
341+
}
342+
}
343+
}
344+
}

eth/downloader/downloader_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,32 @@ func (dlp *downloadTesterPeer) Head() (common.Hash, *big.Int) {
154154
return head.Hash(), dlp.chain.GetTd(head.Hash(), head.NumberU64())
155155
}
156156

157+
func unmarshalRlpHeaders(rlpdata []rlp.RawValue) []*types.Header {
158+
var headers = make([]*types.Header, len(rlpdata))
159+
for i, data := range rlpdata {
160+
var h types.Header
161+
if err := rlp.DecodeBytes(data, &h); err != nil {
162+
panic(err)
163+
}
164+
headers[i] = &h
165+
}
166+
return headers
167+
}
168+
157169
// RequestHeadersByHash constructs a GetBlockHeaders function based on a hashed
158170
// origin; associated with a particular peer in the download tester. The returned
159171
// function can be used to retrieve batches of headers from the particular peer.
160172
func (dlp *downloadTesterPeer) RequestHeadersByHash(origin common.Hash, amount int, skip int, reverse bool, sink chan *eth.Response) (*eth.Request, error) {
161173
// Service the header query via the live handler code
162-
headers := eth.ServiceGetBlockHeadersQuery(dlp.chain, &eth.GetBlockHeadersPacket{
174+
rlpHeaders := eth.ServiceGetBlockHeadersQuery(dlp.chain, &eth.GetBlockHeadersPacket{
163175
Origin: eth.HashOrNumber{
164176
Hash: origin,
165177
},
166178
Amount: uint64(amount),
167179
Skip: uint64(skip),
168180
Reverse: reverse,
169181
}, nil)
170-
182+
headers := unmarshalRlpHeaders(rlpHeaders)
171183
// If a malicious peer is simulated withholding headers, delete them
172184
for hash := range dlp.withholdHeaders {
173185
for i, header := range headers {
@@ -203,15 +215,15 @@ func (dlp *downloadTesterPeer) RequestHeadersByHash(origin common.Hash, amount i
203215
// function can be used to retrieve batches of headers from the particular peer.
204216
func (dlp *downloadTesterPeer) RequestHeadersByNumber(origin uint64, amount int, skip int, reverse bool, sink chan *eth.Response) (*eth.Request, error) {
205217
// Service the header query via the live handler code
206-
headers := eth.ServiceGetBlockHeadersQuery(dlp.chain, &eth.GetBlockHeadersPacket{
218+
rlpHeaders := eth.ServiceGetBlockHeadersQuery(dlp.chain, &eth.GetBlockHeadersPacket{
207219
Origin: eth.HashOrNumber{
208220
Number: origin,
209221
},
210222
Amount: uint64(amount),
211223
Skip: uint64(skip),
212224
Reverse: reverse,
213225
}, nil)
214-
226+
headers := unmarshalRlpHeaders(rlpHeaders)
215227
// If a malicious peer is simulated withholding headers, delete them
216228
for hash := range dlp.withholdHeaders {
217229
for i, header := range headers {

eth/handler_eth_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"github.com/ethereum/go-ethereum/p2p"
3939
"github.com/ethereum/go-ethereum/p2p/enode"
4040
"github.com/ethereum/go-ethereum/params"
41+
"github.com/ethereum/go-ethereum/rlp"
4142
)
4243

4344
// testEthHandler is a mock event handler to listen for inbound network requests
@@ -560,15 +561,17 @@ func testCheckpointChallenge(t *testing.T, syncmode downloader.SyncMode, checkpo
560561
// Create a block to reply to the challenge if no timeout is simulated.
561562
if !timeout {
562563
if empty {
563-
if err := remote.ReplyBlockHeaders(request.RequestId, []*types.Header{}); err != nil {
564+
if err := remote.ReplyBlockHeadersRLP(request.RequestId, []rlp.RawValue{}); err != nil {
564565
t.Fatalf("failed to answer challenge: %v", err)
565566
}
566567
} else if match {
567-
if err := remote.ReplyBlockHeaders(request.RequestId, []*types.Header{response}); err != nil {
568+
responseRlp, _ := rlp.EncodeToBytes(response)
569+
if err := remote.ReplyBlockHeadersRLP(request.RequestId, []rlp.RawValue{responseRlp}); err != nil {
568570
t.Fatalf("failed to answer challenge: %v", err)
569571
}
570572
} else {
571-
if err := remote.ReplyBlockHeaders(request.RequestId, []*types.Header{{Number: response.Number}}); err != nil {
573+
responseRlp, _ := rlp.EncodeToBytes(types.Header{Number: response.Number})
574+
if err := remote.ReplyBlockHeadersRLP(request.RequestId, []rlp.RawValue{responseRlp}); err != nil {
572575
t.Fatalf("failed to answer challenge: %v", err)
573576
}
574577
}

eth/protocols/eth/handler.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ const (
3535
// softResponseLimit is the target maximum size of replies to data retrievals.
3636
softResponseLimit = 2 * 1024 * 1024
3737

38-
// estHeaderSize is the approximate size of an RLP encoded block header.
39-
estHeaderSize = 500
40-
4138
// maxHeadersServe is the maximum number of block headers to serve. This number
4239
// is there to limit the number of disk lookups.
4340
maxHeadersServe = 1024

0 commit comments

Comments
 (0)