internal/era: random access to header and receipts (#31544)

Co-authored-by: lightclient <lightclient@protonmail.com>

Add GetHeaderByNumber and GetReceiptsByNumber to allow more efficient API request filling from Era files.
This commit is contained in:
Sina M 2025-04-04 14:07:46 +02:00 committed by GitHub
parent ff365afc63
commit 77dc1acafa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 112 additions and 17 deletions

View file

@ -219,3 +219,15 @@ func (r *Reader) FindAll(want uint16) ([]*Entry, error) {
off += int64(headerSize + length) off += int64(headerSize + length)
} }
} }
// SkipN skips `n` entries starting from `offset` and returns the new offset.
func (r *Reader) SkipN(offset int64, n uint64) (int64, error) {
for i := uint64(0); i < n; i++ {
length, err := r.LengthAt(offset)
if err != nil {
return 0, err
}
offset += length
}
return offset, nil
}

View file

@ -70,7 +70,7 @@ func ReadDir(dir, network string) ([]string, error) {
} }
parts := strings.Split(entry.Name(), "-") parts := strings.Split(entry.Name(), "-")
if len(parts) != 3 || parts[0] != network { if len(parts) != 3 || parts[0] != network {
// invalid era1 filename, skip // Invalid era1 filename, skip.
continue continue
} }
if epoch, err := strconv.ParseUint(parts[1], 10, 64); err != nil { if epoch, err := strconv.ParseUint(parts[1], 10, 64); err != nil {
@ -126,6 +126,29 @@ func (e *Era) Close() error {
return e.f.Close() return e.f.Close()
} }
// GetHeaderByNumber returns the header for the given block number.
func (e *Era) GetHeaderByNumber(num uint64) (*types.Header, error) {
if e.m.start > num || e.m.start+e.m.count <= num {
return nil, errors.New("out-of-bounds")
}
off, err := e.readOffset(num)
if err != nil {
return nil, err
}
// Read and decompress header.
r, _, err := newSnappyReader(e.s, TypeCompressedHeader, off)
if err != nil {
return nil, err
}
var header types.Header
if err := rlp.Decode(r, &header); err != nil {
return nil, err
}
return &header, nil
}
// GetBlockByNumber returns the block for the given block number.
func (e *Era) GetBlockByNumber(num uint64) (*types.Block, error) { func (e *Era) GetBlockByNumber(num uint64) (*types.Block, error) {
if e.m.start > num || e.m.start+e.m.count <= num { if e.m.start > num || e.m.start+e.m.count <= num {
return nil, errors.New("out-of-bounds") return nil, errors.New("out-of-bounds")
@ -154,6 +177,34 @@ func (e *Era) GetBlockByNumber(num uint64) (*types.Block, error) {
return types.NewBlockWithHeader(&header).WithBody(body), nil return types.NewBlockWithHeader(&header).WithBody(body), nil
} }
// GetReceiptsByNumber returns the receipts for the given block number.
func (e *Era) GetReceiptsByNumber(num uint64) (types.Receipts, error) {
if e.m.start > num || e.m.start+e.m.count <= num {
return nil, errors.New("out-of-bounds")
}
off, err := e.readOffset(num)
if err != nil {
return nil, err
}
// Skip over header and body.
off, err = e.s.SkipN(off, 2)
if err != nil {
return nil, err
}
// Read and decompress receipts.
r, _, err := newSnappyReader(e.s, TypeCompressedReceipts, off)
if err != nil {
return nil, err
}
var receipts types.Receipts
if err := rlp.Decode(r, &receipts); err != nil {
return nil, err
}
return receipts, nil
}
// Accumulator reads the accumulator entry in the Era1 file. // Accumulator reads the accumulator entry in the Era1 file.
func (e *Era) Accumulator() (common.Hash, error) { func (e *Era) Accumulator() (common.Hash, error) {
entry, err := e.s.Find(TypeAccumulator) entry, err := e.s.Find(TypeAccumulator)
@ -187,13 +238,10 @@ func (e *Era) InitialTD() (*big.Int, error) {
} }
off += n off += n
// Skip over next two records. // Skip over header and body.
for i := 0; i < 2; i++ { off, err = e.s.SkipN(off, 2)
length, err := e.s.LengthAt(off) if err != nil {
if err != nil { return nil, err
return nil, err
}
off += length
} }
// Read total difficulty after first block. // Read total difficulty after first block.

View file

@ -18,12 +18,15 @@ package era
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"math/big" "math/big"
"os" "os"
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
) )
type testchain struct { type testchain struct {
@ -48,9 +51,9 @@ func TestEra1Builder(t *testing.T) {
chain = testchain{} chain = testchain{}
) )
for i := 0; i < 128; i++ { for i := 0; i < 128; i++ {
chain.headers = append(chain.headers, []byte{byte('h'), byte(i)}) chain.headers = append(chain.headers, mustEncode(&types.Header{Number: big.NewInt(int64(i))}))
chain.bodies = append(chain.bodies, []byte{byte('b'), byte(i)}) chain.bodies = append(chain.bodies, mustEncode(&types.Body{Transactions: []*types.Transaction{types.NewTransaction(0, common.Address{byte(i)}, nil, 0, nil, nil)}}))
chain.receipts = append(chain.receipts, []byte{byte('r'), byte(i)}) chain.receipts = append(chain.receipts, mustEncode(&types.Receipts{{CumulativeGasUsed: uint64(i)}}))
chain.tds = append(chain.tds, big.NewInt(int64(i))) chain.tds = append(chain.tds, big.NewInt(int64(i)))
} }
@ -91,13 +94,25 @@ func TestEra1Builder(t *testing.T) {
t.Fatalf("unexpected error %v", it.Error()) t.Fatalf("unexpected error %v", it.Error())
} }
// Check headers. // Check headers.
header, err := io.ReadAll(it.Header) rawHeader, err := io.ReadAll(it.Header)
if err != nil {
t.Fatalf("error reading header from iterator: %v", err)
}
if !bytes.Equal(rawHeader, chain.headers[i]) {
t.Fatalf("mismatched header: want %s, got %s", chain.headers[i], rawHeader)
}
header, err := e.GetHeaderByNumber(i)
if err != nil { if err != nil {
t.Fatalf("error reading header: %v", err) t.Fatalf("error reading header: %v", err)
} }
if !bytes.Equal(header, chain.headers[i]) { encHeader, err := rlp.EncodeToBytes(header)
t.Fatalf("mismatched header: want %s, got %s", chain.headers[i], header) if err != nil {
t.Fatalf("error encoding header: %v", err)
} }
if !bytes.Equal(encHeader, chain.headers[i]) {
t.Fatalf("mismatched header: want %s, got %s", chain.headers[i], encHeader)
}
// Check bodies. // Check bodies.
body, err := io.ReadAll(it.Body) body, err := io.ReadAll(it.Body)
if err != nil { if err != nil {
@ -106,13 +121,25 @@ func TestEra1Builder(t *testing.T) {
if !bytes.Equal(body, chain.bodies[i]) { if !bytes.Equal(body, chain.bodies[i]) {
t.Fatalf("mismatched body: want %s, got %s", chain.bodies[i], body) t.Fatalf("mismatched body: want %s, got %s", chain.bodies[i], body)
} }
// Check receipts. // Check receipts.
receipts, err := io.ReadAll(it.Receipts) rawReceipts, err := io.ReadAll(it.Receipts)
if err != nil {
t.Fatalf("error reading receipts from iterator: %v", err)
}
if !bytes.Equal(rawReceipts, chain.receipts[i]) {
t.Fatalf("mismatched receipts: want %s, got %s", chain.receipts[i], rawReceipts)
}
receipts, err := e.GetReceiptsByNumber(i)
if err != nil { if err != nil {
t.Fatalf("error reading receipts: %v", err) t.Fatalf("error reading receipts: %v", err)
} }
if !bytes.Equal(receipts, chain.receipts[i]) { encReceipts, err := rlp.EncodeToBytes(receipts)
t.Fatalf("mismatched receipts: want %s, got %s", chain.receipts[i], receipts) if err != nil {
t.Fatalf("error encoding receipts: %v", err)
}
if !bytes.Equal(encReceipts, chain.receipts[i]) {
t.Fatalf("mismatched receipts: want %s, got %s", chain.receipts[i], encReceipts)
} }
// Check total difficulty. // Check total difficulty.
@ -144,3 +171,11 @@ func TestEraFilename(t *testing.T) {
} }
} }
} }
func mustEncode(obj any) []byte {
b, err := rlp.EncodeToBytes(obj)
if err != nil {
panic(fmt.Sprintf("failed in encode obj: %v", err))
}
return b
}