Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1c03eb7
execution/state: reset only the log slots a transaction wrote
AskAlexSharov Aug 4, 2026
0cfc2ff
execution/state: move the log buffers into their own type
AskAlexSharov Aug 4, 2026
38ab6af
execution/state: name it logArena, and stop scanning entries on reset
AskAlexSharov Aug 5, 2026
7468ce6
execution/types: drop the block-level log copy helper
AskAlexSharov Aug 5, 2026
aad6b39
execution/state: drop oversized log Data by position, keep the entry
AskAlexSharov Aug 5, 2026
efec152
execution/state: keep oversized log Data, evict it first under pressure
AskAlexSharov Aug 5, 2026
12f0b82
execution/state: cover the budget paths, trim the comments
AskAlexSharov Aug 5, 2026
10d8e1f
execution/state: state the protocol ceiling next to the log budgets
AskAlexSharov Aug 5, 2026
7e435e1
execution/state: raise the reusable log entry budget to 16384
AskAlexSharov Aug 5, 2026
883e075
execution/state: move the log tests next to the arena, note EIP-7825
AskAlexSharov Aug 5, 2026
0c45cc3
execution/state: put the sizing rationale on each log budget
AskAlexSharov Aug 5, 2026
ab04465
execution/state: record that the log budgets are sized for a block
AskAlexSharov Aug 5, 2026
90e6d66
execution/state: recycle log entries through a pool, not by tx index
AskAlexSharov Aug 5, 2026
a242baa
execution/state: review fixes on the log arena
AskAlexSharov Aug 5, 2026
b46e1dc
execution/state: revert the tx group only when it is the last one
AskAlexSharov Aug 5, 2026
855f951
execution/state: name the assert stamps genByTx
AskAlexSharov Aug 5, 2026
85a402e
execution/state: say why the entry budget sits below the protocol cei…
AskAlexSharov Aug 5, 2026
d75f691
execution/state: derive the log budgets from the gas constants
AskAlexSharov Aug 5, 2026
972c98e
execution/state: let maxLogsPerTxn hold the pool to a tenth of it
AskAlexSharov Aug 5, 2026
222237c
execution/state: size the log pool from maxLogsPerTxn directly
AskAlexSharov Aug 5, 2026
d38ccc6
execution/state: cut the comments the constants now state themselves
AskAlexSharov Aug 5, 2026
eb6d857
execution/state: keep an outlier transaction's entries, drop only its…
AskAlexSharov Aug 5, 2026
0d2ba2d
execution/state: keep the address out of the worst-case benchmark loop
AskAlexSharov Aug 5, 2026
576e57a
execution/state: drop the GetLogs assert, it cannot tell the two case…
AskAlexSharov Aug 5, 2026
2d3a5b1
save
AskAlexSharov Aug 5, 2026
667e565
execution/state: benchmark the block-level reset shape
AskAlexSharov Aug 5, 2026
a9c7709
execution/state: hold the logs in one run, not grouped by tx index
AskAlexSharov Aug 5, 2026
182d5f6
Merge remote-tracking branch 'origin/main' into alex/slow_reset_log_37
AskAlexSharov Aug 5, 2026
5a31cd0
execution/state, execution/types: keep the streaming log hash, flat
AskAlexSharov Aug 5, 2026
3b80eca
execution/state: review fixes — always take, own bound for the run
AskAlexSharov Aug 5, 2026
23fe3d3
execution/state: pin what one arena may hold, whatever the traffic
AskAlexSharov Aug 5, 2026
178c9c6
execution/state: hoist the run into a local so the index checks go
AskAlexSharov Aug 5, 2026
e35c914
execution/state: correct the log-entry ownership wording left over fr…
AskAlexSharov Aug 7, 2026
57896d6
Merge branch 'main' into alex/slow_reset_log_37
AskAlexSharov Aug 7, 2026
9d57200
execution/state: make the past-transaction log check unconditional
AskAlexSharov Aug 7, 2026
4236270
Merge remote-tracking branch 'origin/main' into HEAD
AskAlexSharov Aug 12, 2026
ffe2866
Merge branch 'main' into alex/log_arena_tail_check_37
AskAlexSharov Aug 12, 2026
63b85dc
Merge branch 'main' into alex/log_arena_tail_check_37
AskAlexSharov Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions execution/state/log_arena.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"slices"

"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/dbg"
"github.com/erigontech/erigon/common/hexutil"
"github.com/erigontech/erigon/execution/protocol/params"
"github.com/erigontech/erigon/execution/types"
Expand All @@ -37,8 +36,8 @@ const (
maxLogBytesPerTxn = int(params.MaxTxnGasLimit / params.LogDataGas) // 2MB

// Fractions of those, because the pool never shrinks: the whole ceiling would
// park 13MB of entries per arena, while a tenth still holds the p99 block of
// 1706 logs that a caller resetting per block pools in one go.
// park 13MB of entries per arena, while a tenth still holds the ~1700 logs of
// a p99 block that a caller resetting per block pools in one go.
maxPooledLogEntries = maxLogsPerTxn / 10
maxPooledLogBytes = maxLogBytesPerTxn / 2

Expand All @@ -49,7 +48,7 @@ const (

// Slots the arena keeps between resets. The run is a block for a caller that
// resets per block, so this is bounded by the memory it costs rather than by
// a transaction's budget: 32KB of pointers, holding the p99 block of 1706.
// a transaction's budget: 32KB of pointers, holding a p99 block's ~1700.
maxRetainedLogSlots = 32 * 1024 / 8
)

Expand Down Expand Up @@ -150,14 +149,15 @@ func (a *logArena) revertLast(txIndex int) {
}

// forTx returns the entries txIndex emitted, owned by the arena. They are held
// in one run rather than grouped, and a transaction's own are the tail of it,
// so any other transaction reads as empty.
// in one run rather than grouped, and a transaction's own are the tail of it, so
// a transaction newer than the tail reads as empty and an older one panics.
Comment on lines +152 to +153

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.

forTx compares only against the current run: after reset() a past-tx read answers empty again, and once the next block logs at the same index a stale reader gets that block's logs back — no panic in either case. Worth scoping the promise:

Suggested change
// in one run rather than grouped, and a transaction's own are the tail of it, so
// a transaction newer than the tail reads as empty and an older one panics.
// in one run rather than grouped, and a transaction's own are the tail of it, so
// a transaction newer than the tail reads as empty and an older one panics
// while the run still holds a newer entry.

func (a *logArena) forTx(txIndex int) types.Logs {
entries := a.entries
i := len(entries)
if dbg.AssertEnabled && i > 0 && txIndex < int(entries[i-1].TxIndex) {
if i > 0 && txIndex < int(entries[i-1].TxIndex) {
// Newer than the tail is a transaction that emitted nothing, which is
// how most of them end. Older is a caller reading what it has left.
// how most of them end. Older is a caller reading what it has left:
// answering empty would read as "emitted no logs", so say so instead.
panic(fmt.Sprintf("logs of tx %d asked for, the run has reached tx %d",
txIndex, int(entries[i-1].TxIndex)))
}
Expand Down
25 changes: 19 additions & 6 deletions execution/state/log_arena_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,12 +655,11 @@ func TestLogIndexIsBlockWide(t *testing.T) {
require.Equal(t, hexutil.Uint(0), ibs.GetRawLogs(0)[0].Index, "next block restarts at zero")
}

// Reading a transaction the run has moved past answers empty, which a receipt
// records as "emitted no logs". A transaction that simply emitted nothing is
// newer than the tail, not older, so it stays legal.
func TestGetLogsOfPastTxAsserts(t *testing.T) {
defer func(prev bool) { dbg.AssertEnabled = prev }(dbg.AssertEnabled)
dbg.AssertEnabled = true
// Reading a transaction the run has moved past would answer empty, which a
// receipt records as "emitted no logs" - so it panics instead. A transaction that
// simply emitted nothing is newer than the tail, not older, so it stays legal.
func TestGetLogsOfPastTxPanics(t *testing.T) {
t.Parallel()

ibs := New(nil)
ibs.SetTxContext(1, 0)
Expand All @@ -673,6 +672,20 @@ func TestGetLogsOfPastTxAsserts(t *testing.T) {
require.Panics(t, func() { ibs.GetRawLogs(0) }, "the run has moved past tx 0")
}

// The past-transaction check guards production, not just assert builds: a caller
// that reads what the run has left must not be handed the tail's logs as if they
// were its own. Not parallel - it writes the global assert flag.
Comment on lines +675 to +677

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.

Ungated forTx can't hand a past reader the tail's logs: entries are stamped with their TxIndex, so a strictly-older read matches nothing and the answer is empty — the "emitted no logs" failure the sibling test's docstring and the PR body describe. Suggest pinning the docstring to that mechanism:

Suggested change
// The past-transaction check guards production, not just assert builds: a caller
// that reads what the run has left must not be handed the tail's logs as if they
// were its own. Not parallel - it writes the global assert flag.
// The past-transaction check guards production, not just assert builds: a caller
// that reads what the run has left would otherwise be answered empty, which a
// receipt records as "emitted no logs". Not parallel - it writes the global
// assert flag.

func TestGetLogsOfPastTxPanicsWithoutAsserts(t *testing.T) {

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.

Optional: with dbg.AssertEnabled gone from the checked path, this and TestGetLogsOfPastTxPanics execute identical code and differ only in an ambient flag the code never reads. They could fold into one serial test that forces the flag false (deterministic under local ERIGON_ASSERT=true runs too). Keeping both as an explicit unconditionality pin is also fine — feel free to drop this one.

defer func(prev bool) { dbg.AssertEnabled = prev }(dbg.AssertEnabled)
dbg.AssertEnabled = false

ibs := New(nil)
ibs.SetTxContext(1, 3)
ibs.AddLog(&types.Log{Address: common.HexToAddress("0x1")})

require.Panics(t, func() { ibs.GetRawLogs(2) }, "tx 2 is older than the tail")
}

// Whatever the traffic looks like, one arena holds a bounded amount: the pool,
// and the one array the run left behind. This is the invariant a shape-specific
// leak breaks — logs parked per tx index, an array kept because one block was
Expand Down
Loading