Skip to content

internal/era: New EraE implementation#32157

Merged
lightclient merged 47 commits intoethereum:masterfrom
shazam8253:era2implementation
Feb 9, 2026
Merged

internal/era: New EraE implementation#32157
lightclient merged 47 commits intoethereum:masterfrom
shazam8253:era2implementation

Conversation

@shazam8253
Copy link
Copy Markdown
Contributor

@shazam8253 shazam8253 commented Jul 7, 2025

Here is a draft for the New EraE implementation. The code follows along with the spec listed at this link.

Copy link
Copy Markdown
Member

@MariusVanDerWijden MariusVanDerWijden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint is failing, you can check on your machine with make lint

Comment thread internal/era2/accumulator.go Outdated
Comment thread internal/era2/builder2.go Outdated
Comment thread internal/era2/builder2.go Outdated
Comment thread internal/era2/era2.go Outdated
@@ -0,0 +1,429 @@
package era2
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

header

Copy link
Copy Markdown
Member

@lightclient lightclient left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a bunch of comments here - we still need to figure out how to call this module since era2 isn't very elegant. In the meantime should rename builder2.go to just builder.go and era2.go to era.go.

Comment thread internal/era2/builder2.go Outdated
Comment thread internal/era2/builder2.go Outdated
}
}

func (b *Builder) Add(header types.Header, body types.Body, receipts types.Receipts, blockhash common.Hash, blocknum uint64, td *big.Int, proof *Proof) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blockhash and blocknum are already available via header, so no need to duplicate them

Comment thread internal/era2/builder2.go Outdated
}
}

func (b *Builder) Add(header types.Header, body types.Body, receipts types.Receipts, blockhash common.Hash, blocknum uint64, td *big.Int, proof *Proof) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would separate this into Add and AddRLP

Comment thread internal/era2/builder2.go Outdated
Comment on lines +83 to +86
headersRLP [][]byte
bodiesRLP [][]byte
receiptsRLP [][]byte
proofsRLP [][]byte
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would remove RLP suffix since it is pretty explanatory by the type [][]byte

Comment thread internal/era2/builder2.go Outdated
Comment thread internal/era2/era2.go Outdated
"github.com/klauspost/compress/snappy"
)

type meta struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type meta struct {
type metadata struct {

Comment thread internal/era2/era2.go Outdated
type meta struct {
start uint64 // start block number
count uint64 // number of blocks in the era
compcount uint64 // number of properties
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
compcount uint64 // number of properties
components uint64 // number of properties

Comment thread internal/era2/era2.go Outdated
start uint64 // start block number
count uint64 // number of blocks in the era
compcount uint64 // number of properties
filelen int64 // length of the file in bytes
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
filelen int64 // length of the file in bytes
length int64 // length of the file in bytes

Comment thread internal/era2/era2.go Outdated
mu *sync.Mutex
headeroff, bodyoff, receiptsoff, tdoff, proofsoff []uint64 // offsets for each entry type
indstart int64
rootheader uint64 // offset of the root header in the file if present
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the use of this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when reading each era file I load in the index table into cache to make lookup faster, indstart is where the byte where the index table starts, and the root header is where the accumulator root is present when reading so it can seek there quickly since it is its own e2store object. The mutex should be removed though, forgot to do so very early on when I didn't understand what the file was doing I thought I wouldn't want it to read and write at the same time so put a lock.

Comment thread internal/era2/era2.go Outdated
m meta // metadata for the era2 file
mu *sync.Mutex
headeroff, bodyoff, receiptsoff, tdoff, proofsoff []uint64 // offsets for each entry type
indstart int64
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this?

@lightclient
Copy link
Copy Markdown
Member

Also, please write a description for your PRs and try to fix the CI errors so your code is all green.

@lightclient lightclient self-assigned this Jul 10, 2025
Comment thread internal/era2/era.go Outdated

func (*BlockProofHistoricalSummariesDeneb) Variant() proofvar { return proofDeneb }

func proofVariantOf(p Proof) proofvar {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func proofVariantOf(p Proof) proofvar {
func variantOf(p Proof) proofvar {

Comment thread internal/era2/builder.go Outdated
@@ -57,41 +57,33 @@ const (

type proofvar uint16
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type proofvar uint16
type variant uint16

We know that the variant is for Proofs, should be at most a comment, not part of the variable name, otherwise you end up with the types like mev-boost :D

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha will make the change :)

Copy link
Copy Markdown
Member

@lightclient lightclient left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a lot of style nits, but the main problems we'll need to work through are:

  • utilize proof interface more, avoid handling variant values directly
  • remove unneeded struct fields
  • avoid building entire era file in memory, write incremental work to disk

Comment thread internal/era2/builder.go Outdated
buff buffer
off offsets

prooftype variant
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed. If you store the proofs as the interface object you will be able to determine the variant at anytime / alternatively if it makes better sense to store the bytes, then the bytes will already have included the variant type so it won't matter anymore.

Comment thread internal/era2/builder.go Outdated
off offsets

prooftype variant
tdsint []*big.Int
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed and tds in buffer should be big.Ints. Serialize them all at once during finalize.

Comment thread internal/era2/builder.go Outdated
type Builder struct {
w *e2store.Writer
buf *bytes.Buffer
sn *snappy.Writer
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sn *snappy.Writer
snappy *snappy.Writer

Comment thread internal/era2/builder.go Outdated
Comment thread internal/era2/builder.go Outdated
Comment thread internal/era2/era.go Outdated
}

// retrieves the raw body frame in bytes of a specific block
func (e *Era) GetRawBodyFrameByNumber(blockNum uint64) ([]byte, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by "frame" here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the raw bytes that are written, without snappy decoding it and rlp decoding

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should decode the snappy here (and other methods like this) and return the RLP bytes

Comment thread internal/era2/era.go Outdated
return io.ReadAll(r)
}

// retrieves the raw receipts frame in bytes of a specific block
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// retrieves the raw receipts frame in bytes of a specific block
// GetRawReceiptsFrameByNumber retrieves the raw receipts frame in bytes of a specific block.

Comment thread internal/era2/era.go Outdated
return io.ReadAll(r)
}

// retrieves the raw proof frame in bytes of a specific block proof
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// retrieves the raw proof frame in bytes of a specific block proof
// GetRawProofFrameByNumber retrieves the raw proof frame in bytes of a specific block proof.

Comment thread internal/era2/era.go Outdated
return io.ReadAll(r)
}

// loads in the index table containing all offsets and caches it
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// loads in the index table containing all offsets and caches it
// loadIndex loads in the index table containing all offsets and caches it.

Comment thread internal/era2/era.go Outdated
// Getter methods to calculate offset of a specific component in the file.
func (e *Era) headerOff(num uint64) (uint64, error) { return e.indexOffset(num, compHeader) }
func (e *Era) bodyOff(num uint64) (uint64, error) { return e.indexOffset(num, compBody) }
func (e *Era) rcptOff(num uint64) (uint64, error) { return e.indexOffset(num, compReceipts) }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (e *Era) rcptOff(num uint64) (uint64, error) { return e.indexOffset(num, compReceipts) }
func (e *Era) receiptOff(num uint64) (uint64, error) { return e.indexOffset(num, compReceipts) }

Really no reason to ever abbreviate by taking out vowels in golang.

Comment thread cmd/utils/cmd.go Outdated
// following the Era format.
func ExportHistory(bc *core.BlockChain, dir string, first, last, step uint64) error {
log.Info("Exporting blockchain history", "dir", dir)
func ExportHistory(bc *core.BlockChain, dir string, first, last, step uint64, f ExportFormat) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not use an enum to dictate the output format, this should be done via the method naming, e.g.

ExportHistoryEra1(..)
ExportHistoryEraE(..)

Comment thread cmd/utils/cmd.go Outdated
Comment on lines +433 to +445
if f == Era1 {
filename = era.Filename
newBuilder = func(w io.Writer) any { return era.NewBuilder(w) }
add = func(b any, blk *types.Block, rcpt types.Receipts, td *big.Int) error {
return b.(*era.Builder).Add(blk, rcpt, td)
}
} else {
filename = era2.Filename
newBuilder = func(w io.Writer) any { return era2.NewBuilder(w) }
add = func(b any, blk *types.Block, rcpt types.Receipts, td *big.Int) error {
return b.(*era2.Builder).Add(*blk.Header(), *blk.Body(), rcpt, td, nil)
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of impressive, but also is what an Interface is for :)

If you want different builders with the same methods (like Add) you can create the interface type and implement the method for both.

Comment thread cmd/utils/cmd.go Outdated
receipts := bc.GetReceiptsByHash(block.Hash())
if receipts == nil {
return fmt.Errorf("export failed on #%d: receipts not found", n)
rcpt := bc.GetReceiptsByHash(blk.Hash())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rcpt := bc.GetReceiptsByHash(blk.Hash())
receipts := bc.GetReceiptsByHash(blk.Hash())

Comment thread cmd/utils/cmd.go Outdated

for j := uint64(0); j < step && batch+j <= last; j++ {
n := batch + j
blk := bc.GetBlockByNumber(n)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
blk := bc.GetBlockByNumber(n)
block := bc.GetBlockByNumber(n)

Comment thread cmd/utils/cmd.go Outdated
)
if block == nil {
return fmt.Errorf("export failed on #%d: not found", n)
bldr := newBuilder(f)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bldr := newBuilder(f)
builder := newBuilder(f)

Comment thread internal/era2/builder.go Outdated
tds []*big.Int
}

// The offsets holds the offsets of the different block components in the e2store file. Eventually these offsets will be used to write the index table at the end of the file.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// The offsets holds the offsets of the different block components in the e2store file. Eventually these offsets will be used to write the index table at the end of the file.
// offsets holds the offsets of the different block components in the e2store file. Eventually these offsets will be used to write the index table at the end of the file.

Comment thread internal/era2/builder.go Outdated
Comment on lines +95 to +97
buf *bytes.Buffer

buff buffer
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we still have buf and buff, I think we discussed removing buf since it isn't used until the end?

Comment thread internal/era2/builder.go Outdated
// Add writes a block entry, its reciepts, and optionally its proofs as well into the e2store file.
func (b *Builder) Add(header types.Header, body types.Body, receipts types.Receipts, td *big.Int, proof Proof) error {
if len(b.buff.headers) == 0 { // first block determines wether proofs are expected
b.expectsProofs = proof != nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think you need to track this explicitly, just check if b.buff.proofs != nil and if proof == nil or vice versa. only special case is first block, which you allow for the b.buff.proofs to be nil even if proof is non-nil.

Comment thread internal/era2/builder.go Outdated
if err != nil {
return common.Hash{}, fmt.Errorf("compute accumulator: %w", err)
}
if n, err := b.w.Write(TypeAccumulatorRoot, accRoot[:]); err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this still needs to be addressed

Comment thread internal/era2/era.go Outdated
}

// retrieves the raw body frame in bytes of a specific block
func (e *Era) GetRawBodyFrameByNumber(blockNum uint64) ([]byte, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should decode the snappy here (and other methods like this) and return the RLP bytes

Comment thread cmd/utils/era_interface.go Outdated
Comment on lines +15 to +21
type Iterator interface {
Next() bool
Number() uint64
Block() (*types.Block, error)
Receipts() (types.Receipts, error)
Error() error
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface looks right, but it's in the wrong place. We should define it in the era package. I will take a stab at this.

Comment thread cmd/utils/era_interface.go Outdated
Comment on lines +48 to +57
func (era1Format) Filename(n string, e int, h common.Hash) string { return era.Filename(n, e, h) }
func (era1Format) NewBuilder(w io.Writer) Builder { return &era1Builder{era.NewBuilder(w)} }
func (era1Format) ReadDir(dir, net string) ([]string, error) { return era.ReadDir(dir, net) }
func (era1Format) NewIterator(f *os.File) (Iterator, error) {
e, err := era.From(f)
if err != nil {
return nil, err
}
return era.NewIterator(e)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you shouldn't need to redefine these methods to satisfy the interface - if the type implements the interface methods then it can be accepted anywhere the interface is accepted

Comment thread cmd/utils/cmd.go Outdated
// starting from genesis. The assumption is held that the provided chain
// segment in Era1 file should all be canonical and verified.
func ImportHistory(chain *core.BlockChain, dir string, network string) error {
func ImportHistory(chain *core.BlockChain, dir string, network string, format Format) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So on this method and ExportHistory we can avoid using this Format type by just passing in the functions we need. For example, here we need a way to read all the entries and a way to create an iterator from an open file. If we just pass those two functions in, we can create an if statement in the cli handling so that we can reuse the function.

The issue with format is it is kind of a superfluous type.

@MariusVanDerWijden MariusVanDerWijden self-assigned this Aug 26, 2025
@s1na s1na force-pushed the era2implementation branch from 1c247de to b0dadc3 Compare January 19, 2026 21:52
@s1na s1na changed the title Draft: New EraE implementation internal/era: New EraE implementation Jan 20, 2026
Comment thread cmd/geth/chaincmd.go Outdated
if err != nil {

var (
format = ctx.String(utils.EraFormatFlag.Get(ctx))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
format = ctx.String(utils.EraFormatFlag.Get(ctx))
format = ctx.String(utils.EraFormatFlag.Name)

@s1na
Copy link
Copy Markdown
Contributor

s1na commented Jan 22, 2026

Also seen this panic:

INFO [01-22|22:13:26.001] export progress                          exported=1,441,792 elapsed=3m9.464s
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x13b779a]

goroutine 1 [running]:
math/big.(*Int).Set(...)
        math/big/int.go:97
github.com/ethereum/go-ethereum/internal/era/execdb.(*Builder).AddRLP(0xc00b68a8f0, {0xc007c2be00, 0x1fc, 0x1fc}, {0xc054b7e0f0, 0x3, 0x3}, {0xc054b7e0f3, 0x1, 0x1}, ...)
        github.com/ethereum/go-ethereum/internal/era/execdb/builder.go:158 +0x6fa
github.com/ethereum/go-ethereum/internal/era/execdb.(*Builder).Add(0xc00b68a8f0, 0xc054959cb0, {0x32ada80, 0x0, 0xc00089b808?}, 0x0)
        github.com/ethereum/go-ethereum/internal/era/execdb/builder.go:119 +0x6ed
github.com/ethereum/go-ethereum/cmd/utils.ExportHistory.func1({0xc053389ba0, 0x1f}, 0x20889d0, 0x162000, 0xc053b7cae8, 0xc00089b808, 0xc008d739c0, {0x7fff1bb876da, 0x3}, 0x20889d8, ...)
        github.com/ethereum/go-ethereum/cmd/utils/cmd.go:493 +0x7ce
github.com/ethereum/go-ethereum/cmd/utils.ExportHistory(0xc00089b808, {0x7fff1bb876da, 0x3}, 0x0, 0x4f25cf, 0x20889d0, 0x20889d8)
        github.com/ethereum/go-ethereum/cmd/utils/cmd.go:516 +0x890
main.exportHistory(0xc00047afc0)
        github.com/ethereum/go-ethereum/cmd/geth/chaincmd.go:583 +0x485
github.com/ethereum/go-ethereum/internal/flags.MigrateGlobalFlags.func2.1(0xc00047afc0)
        github.com/ethereum/go-ethereum/internal/flags/helpers.go:90 +0x34
github.com/urfave/cli/v2.(*Command).Run(0x3269ce0, 0xc00047afc0, {0xc0000c9270, 0x5, 0x5})
        github.com/urfave/cli/v2@v2.27.5/command.go:276 +0x7be
github.com/urfave/cli/v2.(*Command).Run(0xc00030c2c0, 0xc0001d9380, {0xc00003c080, 0x8, 0x8})
        github.com/urfave/cli/v2@v2.27.5/command.go:269 +0xa45
github.com/urfave/cli/v2.(*App).RunContext(0xc000316200, {0x22c4e10, 0x32ada80}, {0xc00003c080, 0x8, 0x8})
        github.com/urfave/cli/v2@v2.27.5/app.go:333 +0x5a5
github.com/urfave/cli/v2.(*App).Run(...)
        github.com/urfave/cli/v2@v2.27.5/app.go:307
main.main()
        github.com/ethereum/go-ethereum/cmd/geth/main.go:287 +0x45

lightclient
lightclient previously approved these changes Jan 30, 2026
Copy link
Copy Markdown
Member

@lightclient lightclient left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

lightclient
lightclient previously approved these changes Jan 30, 2026
Copy link
Copy Markdown
Contributor

@s1na s1na left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@lightclient
Copy link
Copy Markdown
Member

Export on mainnet completed in ~22 hours, import in ~62 hours. Definitely would be good to look into speeding up import some, but overall this is ready to come into geth. Thanks @shazam8253 for your hard work on this last summer - sorry it took so long to finally get in! Thank you @s1na for pushing it over the edge. I've been treading water here for too long.

@lightclient lightclient merged commit c9b7ae4 into ethereum:master Feb 9, 2026
7 of 8 checks passed
@lightclient lightclient added this to the 1.17.0 milestone Feb 9, 2026
@shazam8253
Copy link
Copy Markdown
Contributor Author

@lightclient @s1na Thank you guys for pushing this through, super happy to see this! Had a great time this summer, and hopefully when I have more bandwidth I can make a PR again soon.

@s1na s1na mentioned this pull request Feb 10, 2026
5 tasks
gballet pushed a commit that referenced this pull request Mar 11, 2026
This PR allows users to prune their nodes up to the Prague fork. It
indirectly depends on #32157 and can't really be merged before eraE
files are widely available for download.

The `--history.chain` flag becomes mandatory for `prune-history`
command. Here I've listed all the edge cases that can happen and how we
behave:

## prune-history Behavior

| From        | To           | Result                   |
|-------------|--------------|--------------------------|
| full        | postmerge    | ✅ prunes                |
| full        | postprague   | ✅ prunes                |
| postmerge   | postprague   | ✅ prunes further        |
| postprague  | postmerge    | ❌ can't unprune         |
| any         | all          | ❌ use import-history    |


## Node Startup Behavior

| DB State | Flag | Result |

|-------------|--------------|----------------------------------------------------------------|
| fresh | postprague | ✅ syncs from Prague |
| full | postprague | ❌ "run prune-history first" |
| postmerge | postprague | ❌ "run prune-history first" |
| postprague | postmerge | ❌ "can't unprune, use import-history or fix
flag" |
| pruned | all | ✅ accepts known prune points |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants