internal/era: random access to header and receipts

This commit is contained in:
Sina Mahmoodi 2025-04-01 18:38:34 +02:00 committed by lightclient
parent 9f83e9e673
commit e4e4c5538b
No known key found for this signature in database
GPG key ID: 657913021EF45A6A
2 changed files with 219 additions and 0 deletions

View file

@ -126,6 +126,29 @@ func (e *Era) Close() error {
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) {
if e.m.start > num || e.m.start+e.m.count <= num {
return nil, errors.New("out-of-bounds")
@ -154,6 +177,42 @@ func (e *Era) GetBlockByNumber(num uint64) (*types.Block, error) {
return types.NewBlockWithHeader(&header).WithBody(body), nil
}
// GetReceipts returns the receipts for the given block number.
func (e *Era) GetReceipts(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 header entry
headerLength, err := e.s.LengthAt(off)
if err != nil {
return nil, err
}
off += headerLength
// Skip body entry
bodyLength, err := e.s.LengthAt(off)
if err != nil {
return nil, err
}
off += bodyLength
// 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.
func (e *Era) Accumulator() (common.Hash, error) {
entry, err := e.s.Find(TypeAccumulator)

View file

@ -21,9 +21,17 @@ import (
"io"
"math/big"
"os"
"path/filepath"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require"
)
type testchain struct {
@ -144,3 +152,155 @@ func TestEraFilename(t *testing.T) {
}
}
}
func genTestChain(t *testing.T) (*core.Genesis, []*types.Block, []types.Receipts) {
privateKey, err := crypto.GenerateKey()
require.NoError(t, err)
var (
address = crypto.PubkeyToAddress(privateKey.PublicKey)
genesis = &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
address: {
Balance: big.NewInt(10000000000000000), // 10 ETH
},
},
GasLimit: 1000000,
Difficulty: big.NewInt(1),
}
)
_, chain, receipts := core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), 3, func(i int, gen *core.BlockGen) {
// Add a transfer transaction
to := common.HexToAddress("0x5678")
tx := types.NewTx(&types.LegacyTx{
Nonce: uint64(i * 2),
To: &to,
Value: big.NewInt(100000000000000), // 0.1 ETH
Gas: 21000,
GasPrice: big.NewInt(1000000000), // 1 Gwei
})
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(genesis.Config.ChainID), privateKey)
require.NoError(t, err)
gen.AddTx(signedTx)
// Add a contract creation transaction
tx = types.NewTx(&types.LegacyTx{
Nonce: uint64(i*2 + 1),
Value: big.NewInt(0),
Gas: 100000,
GasPrice: big.NewInt(1000000000), // 1 Gwei
// Simple contract that returns empty data
Data: []byte{
0x60, 0x00, // PUSH1 0
0x60, 0x00, // PUSH1 0
0x52, // MSTORE
0x60, 0x20, // PUSH1 32
0x60, 0x00, // PUSH1 0
0xf3, // RETURN
},
})
signedTx, err = types.SignTx(tx, types.NewEIP155Signer(genesis.Config.ChainID), privateKey)
require.NoError(t, err)
gen.AddTx(signedTx)
})
return genesis, chain, receipts
}
// exportChain creates a temporary era file with the given chain.
func exportChain(t *testing.T, genesis *core.Genesis, chain []*types.Block, receipts []types.Receipts) (string, string) {
tmpDir, err := os.MkdirTemp("", "era-test-*")
require.NoError(t, err)
const fileName = "test.era1"
tmpFile, err := os.Create(filepath.Join(tmpDir, fileName))
require.NoError(t, err)
builder := NewBuilder(tmpFile)
// Add blocks to era
for i, block := range chain {
td := new(big.Int).Add(genesis.Difficulty, big.NewInt(int64(i+1)))
headerData, err := rlp.EncodeToBytes(block.Header())
require.NoError(t, err)
bodyData, err := rlp.EncodeToBytes(block.Body())
require.NoError(t, err)
receiptsData, err := rlp.EncodeToBytes(receipts[i])
require.NoError(t, err)
err = builder.AddRLP(headerData, bodyData, receiptsData, block.NumberU64(), block.Hash(), td, block.Difficulty())
require.NoError(t, err)
}
_, err = builder.Finalize()
require.NoError(t, err)
err = tmpFile.Close()
require.NoError(t, err)
return tmpDir, fileName
}
func TestEraFunctions(t *testing.T) {
genesis, blocks, receipts := genTestChain(t)
testdir, filename := exportChain(t, genesis, blocks, receipts)
defer os.RemoveAll(testdir)
// Open the era file
era, err := Open(filepath.Join(testdir, filename))
require.NoError(t, err)
defer era.Close()
t.Run("GetHeaderByNumber", func(t *testing.T) {
header, err := era.GetHeaderByNumber(era.Start())
require.NoError(t, err)
haveJson, err := rlp.EncodeToBytes(header)
require.NoError(t, err)
wantJson, err := rlp.EncodeToBytes(blocks[0].Header())
require.NoError(t, err)
require.Equal(t, wantJson, haveJson)
header, err = era.GetHeaderByNumber(era.Start() + era.Count() - 1)
require.NoError(t, err)
haveJson, err = rlp.EncodeToBytes(header)
require.NoError(t, err)
wantJson, err = rlp.EncodeToBytes(blocks[2].Header())
require.NoError(t, err)
require.Equal(t, wantJson, haveJson)
})
t.Run("GetBlockByNumber", func(t *testing.T) {
block, err := era.GetBlockByNumber(era.Start())
require.NoError(t, err)
haveJson, err := rlp.EncodeToBytes(block)
require.NoError(t, err)
wantJson, err := rlp.EncodeToBytes(blocks[0])
require.NoError(t, err)
require.Equal(t, wantJson, haveJson)
block, err = era.GetBlockByNumber(era.Start() + 1)
require.NoError(t, err)
haveJson, err = rlp.EncodeToBytes(block)
require.NoError(t, err)
wantJson, err = rlp.EncodeToBytes(blocks[1])
require.NoError(t, err)
require.Equal(t, wantJson, haveJson)
})
t.Run("GetReceipts", func(t *testing.T) {
rcpts, err := era.GetReceipts(era.Start())
require.NoError(t, err)
require.Equal(t, 2, len(rcpts)) // Should have 2 receipts
require.Equal(t, receipts[0][0].CumulativeGasUsed, rcpts[0].CumulativeGasUsed)
require.Equal(t, receipts[0][1].CumulativeGasUsed, rcpts[1].CumulativeGasUsed)
require.Equal(t, receipts[0][0].Status, rcpts[0].Status)
require.Equal(t, receipts[0][1].Status, rcpts[0].Status)
})
t.Run("OutOfBounds", func(t *testing.T) {
_, err := era.GetHeaderByNumber(era.Start() - 1)
require.Error(t, err)
require.Equal(t, "out-of-bounds", err.Error())
_, err = era.GetHeaderByNumber(era.Start() + era.Count())
require.Error(t, err)
require.Equal(t, "out-of-bounds", err.Error())
})
}