Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"WebFetch(domain:gist.githubusercontent.com)"
]
}
}
8 changes: 8 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Summary
<one paragraph: what changed and why. If the change has measurable impact, add a small table or numbers (e.g. perf delta on a kurtosis devnet, mutation kill rate). Link the JIRA / GH issue inline if there is one — not required.>

## Executed tests
<what was actually run beyond CI's standard unit / integration / e2e gates: kurtosis scenarios, chaos runs, manual checks against Amoy / mainnet RPCs, devnet upgrades, etc. Include output or pointers to where the run lives.>

## Rollout notes
<consensus-affecting? requires coordinated upgrade? backwards-compatible? operator-facing change?>
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# AGENTS.md — erigon

## Team standards

At the start of every session, fetch the latest PoS team standards from:

`<https://gist.githubusercontent.com/marcello33/370756d88be9c4cc8459b6afe969085c/raw/team-standards.md>`

These rules apply to every change in this repo unless this repo's
`.claude/rules/` explicitly overrides them.


## Repo-specific guidance

Refer to this repo's `README.md` for architecture and commands. Team
standards (above) apply to all changes. `CLAUDE.md` is a thin
`@AGENTS.md` import — both runtimes converge on this file.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
10 changes: 10 additions & 0 deletions core/state/intra_block_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,16 @@ func (sdb *IntraBlockState) setState(addr common.Address, key common.Hash, value
return nil
}

// SetStateOverride applies an eth_call stateDiff slot as base state. Not for block execution.
func (sdb *IntraBlockState) SetStateOverride(addr common.Address, key common.Hash, value uint256.Int) error {
stateObject, err := sdb.GetOrNewStateObject(addr)
if err != nil {
return err
}
stateObject.setCommittedStorage(key, value)
return nil
}

// SetStorage replaces the entire storage for the specified account with given
// storage. This function should only be used for debugging.
func (sdb *IntraBlockState) SetStorage(addr common.Address, storage Storage) error {
Expand Down
8 changes: 8 additions & 0 deletions core/state/state_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ func (so *stateObject) setState(key common.Hash, value uint256.Int) {
so.dirtyStorage[key] = value
}

// setCommittedStorage makes stateDiff visible as both current and original storage.
func (so *stateObject) setCommittedStorage(key common.Hash, value uint256.Int) {
// Drop replayed writes so the override wins after callMany-style replay.
delete(so.dirtyStorage, key)
so.originStorage[key] = value
so.blockOriginStorage[key] = value
}

// updateStotage writes cached storage modifications into the object's storage trie.
func (so *stateObject) updateStotage(stateWriter StateWriter) error {
for key, value := range so.dirtyStorage {
Expand Down
33 changes: 33 additions & 0 deletions core/state/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,39 @@ func TestSnapshot(t *testing.T) {
require.Equal(t, uint256.Int{}, value)
}

func TestSetStateOverrideReplacesDirtyStorage(t *testing.T) {
t.Parallel()
_, tx, domains := NewTestRwTx(t)

err := rawdbv3.TxNums.Append(tx, 1, 1)
require.NoError(t, err)

state := New(NewReaderV3(domains.AsGetter(tx)))

addr := toAddr([]byte("aa"))
key := common.BigToHash(uint256.NewInt(1).ToBig())
replayed := *uint256.NewInt(1)
override := *uint256.NewInt(2)

require.NoError(t, state.SetState(addr, key, replayed))
require.NoError(t, state.FinalizeTx(&chain.Rules{}, NewNoopWriter()))

var value uint256.Int
require.NoError(t, state.GetState(addr, key, &value))
require.Equal(t, replayed, value)

require.NoError(t, state.SetStateOverride(addr, key, override))
require.NoError(t, state.GetState(addr, key, &value))
require.Equal(t, override, value)
require.NoError(t, state.GetCommittedState(addr, key, &value))
require.Equal(t, override, value)

obj, err := state.getStateObject(addr)
require.NoError(t, err)
_, dirty := obj.dirtyStorage[key]
require.False(t, dirty)
}

func TestSnapshotEmpty(t *testing.T) {
t.Parallel()
_, tx, domains := NewTestRwTx(t)
Expand Down
166 changes: 166 additions & 0 deletions core/vm/runtime/state_override_warm_reset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package runtime

import (
"encoding/binary"
"testing"

"github.com/holiman/uint256"

"github.com/erigontech/erigon-lib/common"
"github.com/erigontech/erigon/core/state"
"github.com/erigontech/erigon/core/vm"
)

func TestEthCallStateDiffOverride_WarmResetSSTORE(t *testing.T) {
t.Parallel()

// Warm slot 1, SSTORE slot 1 := 2, and return the gas delta around SSTORE.
code := []byte{
byte(vm.PUSH1), 0x01,
byte(vm.SLOAD),
byte(vm.POP),
byte(vm.GAS),
byte(vm.PUSH1), 0x02,
byte(vm.PUSH1), 0x01,
byte(vm.SSTORE),
byte(vm.GAS),
byte(vm.SWAP1),
byte(vm.SUB),
byte(vm.PUSH1), 0x00,
byte(vm.MSTORE),
byte(vm.PUSH1), 0x20,
byte(vm.PUSH1), 0x00,
byte(vm.RETURN),
}

addr := common.HexToAddress("0xaa")
slot := common.BigToHash(uint256.NewInt(1).ToBig())
one := *uint256.NewInt(1)

// stateDiff must be visible as the original value, or SSTORE takes the cheap dirty path.
const sstoreResetWarm = 2900 // EIP-2200 reset (warm): SstoreResetGas - ColdSloadCost.
const sstoreDirty = 100 // EIP-2200 dirty update: WarmStorageReadCost.
const sandwich = 3 + 3 + 2 // PUSH1 + PUSH1 + GAS around the inner SSTORE.

measure := func(seed func(s *state.IntraBlockState)) uint64 {
db := testTemporalDB(t)
tx, domains := testTemporalTxSD(t, db)
s := state.New(state.NewReaderV3(domains.AsGetter(tx)))
s.SetCode(addr, code)
seed(s)
ret, _, err := Call(addr, nil, &Config{State: s})
if err != nil {
t.Fatalf("Call: %v", err)
}
if len(ret) != 32 {
t.Fatalf("ret len: %d", len(ret))
}
return binary.BigEndian.Uint64(ret[24:])
}

t.Run("broken/raw SetState matches Erigon pre-fix", func(t *testing.T) {
got := measure(func(s *state.IntraBlockState) {
if err := s.SetState(addr, slot, one); err != nil {
t.Fatalf("SetState: %v", err)
}
})
if want := uint64(sstoreDirty + sandwich); got != want {
t.Fatalf("got %d, want %d", got, want)
}
})

t.Run("fixed/SetStateOverride matches Bor/geth", func(t *testing.T) {
got := measure(func(s *state.IntraBlockState) {
if err := s.SetStateOverride(addr, slot, one); err != nil {
t.Fatalf("SetStateOverride: %v", err)
}
})
if want := uint64(sstoreResetWarm + sandwich); got != want {
t.Fatalf("got %d, want %d", got, want)
}
})

t.Run("delta is the EIP-2200 reset/dirty spread", func(t *testing.T) {
broken := measure(func(s *state.IntraBlockState) { _ = s.SetState(addr, slot, one) })
fixed := measure(func(s *state.IntraBlockState) { _ = s.SetStateOverride(addr, slot, one) })
if fixed-broken != sstoreResetWarm-sstoreDirty {
t.Fatalf("spread %d, want %d (broken=%d fixed=%d)",
fixed-broken, sstoreResetWarm-sstoreDirty, broken, fixed)
}
})
}

// Regression: stateDiff must win over a replay-dirtied slot.
// eth_callMany applies overrides after replay, and FinalizeTx leaves dirtyStorage in memory.
func TestEthCallStateDiffOverride_BeatsReplayDirty(t *testing.T) {
t.Parallel()

code := []byte{
byte(vm.PUSH1), 0x01,
byte(vm.SLOAD),
byte(vm.PUSH1), 0x00,
byte(vm.MSTORE),
byte(vm.PUSH1), 0x20,
byte(vm.PUSH1), 0x00,
byte(vm.RETURN),
}
addr := common.HexToAddress("0xaa")
slot := common.BigToHash(uint256.NewInt(1).ToBig())
replay := *uint256.NewInt(0xdead)
override := *uint256.NewInt(0x42)

db := testTemporalDB(t)
tx, domains := testTemporalTxSD(t, db)
s := state.New(state.NewReaderV3(domains.AsGetter(tx)))
s.SetCode(addr, code)

if err := s.SetState(addr, slot, replay); err != nil {
t.Fatalf("seed replay: %v", err)
}
if err := s.SetStateOverride(addr, slot, override); err != nil {
t.Fatalf("SetStateOverride: %v", err)
}

var got uint256.Int
if err := s.GetCommittedState(addr, slot, &got); err != nil {
t.Fatalf("GetCommittedState: %v", err)
}
if got.Cmp(&override) != 0 {
t.Fatalf("GetCommittedState: got %s, want %s", got.Hex(), override.Hex())
}
if err := s.GetState(addr, slot, &got); err != nil {
t.Fatalf("GetState: %v", err)
}
if got.Cmp(&override) != 0 {
t.Fatalf("GetState: got %s, want %s", got.Hex(), override.Hex())
}

ret, _, err := Call(addr, nil, &Config{State: s})
if err != nil {
t.Fatalf("Call: %v", err)
}
if len(ret) != 32 {
t.Fatalf("ret len: %d", len(ret))
}
var sload uint256.Int
sload.SetBytes(ret)
if sload.Cmp(&override) != 0 {
t.Fatalf("SLOAD: got %s, want %s", sload.Hex(), override.Hex())
}
}
2 changes: 1 addition & 1 deletion db/state/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1002,7 +1002,7 @@ func (at *AggregatorRoTx) PruneSmallBatches(ctx context.Context, timeout time.Du
furiousPrune := timeout > 5*time.Hour
aggressivePrune := !furiousPrune && timeout >= 1*time.Minute

var pruneLimit uint64 = 100
var pruneLimit uint64 = uint64(dbg.EnvInt("ERIGON_PRUNE_LIMIT", 100))
if furiousPrune {
pruneLimit = 1_000_000
}
Expand Down
2 changes: 1 addition & 1 deletion db/version/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var (
const (
Major = 3 // Major version component of the current release
Minor = 6 // Minor version component of the current release
Micro = 0 // Micro version component of the current release
Micro = 1 // Micro version component of the current release
Modifier = "" // Modifier component of the current release
DefaultSnapshotGitBranch = "main" // Branch of erigontech/erigon-snapshot to use in OtterSync
VersionKeyCreated = "ErigonVersionCreated"
Expand Down
2 changes: 2 additions & 0 deletions execution/chain/chain_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ type BorConfig interface {
IsMadhugiriPro(num uint64) bool
GetMadhugiriBlock() *big.Int
GetMadhugiriProBlock() *big.Int
IsDandeli(num uint64) bool
GetDandeliBlock() *big.Int
IsLisovo(num uint64) bool
IsLisovoPro(num uint64) bool
GetLisovoBlock() *big.Int
Expand Down
2 changes: 1 addition & 1 deletion execution/stagedsync/exec3.go
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ Loop:
timeStart := time.Now()

// allow greedy prune on non-chain-tip
pruneTimeout := 250 * time.Millisecond
pruneTimeout := time.Duration(dbg.EnvInt("ERIGON_PRUNE_TIMEOUT_MS", 250)) * time.Millisecond
if initialCycle {
pruneTimeout = 10 * time.Hour

Expand Down
4 changes: 2 additions & 2 deletions execution/stagedsync/stage_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,12 +436,12 @@ func PruneExecutionStage(s *PruneState, tx kv.RwTx, cfg ExecuteBlockCfg, ctx con
// - stop prune when `tx.SpaceDirty()` is big
// - and set ~500ms timeout
// because on slow disks - prune is slower. but for now - let's tune for nvme first, and add `tx.SpaceDirty()` check later https://github.com/erigontech/erigon/issues/11635
quickPruneTimeout := 500 * time.Millisecond
quickPruneTimeout := time.Duration(dbg.EnvInt("ERIGON_PRUNE_CHANGESETS_TIMEOUT_MS", 500)) * time.Millisecond

if s.ForwardProgress > cfg.syncCfg.MaxReorgDepth && !cfg.syncCfg.AlwaysGenerateChangesets {
// (chunkLen is 8Kb) * (1_000 chunks) = 8mb
// Some blocks on bor-mainnet have 400 chunks of diff = 3mb
var pruneDiffsLimitOnChainTip = 1_000
var pruneDiffsLimitOnChainTip = dbg.EnvInt("ERIGON_PRUNE_CHANGESETS_LIMIT", 1000)
pruneTimeout := quickPruneTimeout
if s.CurrentSyncCycle.IsInitialCycle {
pruneDiffsLimitOnChainTip = math.MaxInt
Expand Down
7 changes: 5 additions & 2 deletions execution/types/state_sync_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,11 @@ func (tx *StateSyncTx) RawSignatureValues() (*uint256.Int, *uint256.Int, *uint25
func (tx *StateSyncTx) EncodingSize() int {
var b bytes.Buffer
_ = tx.encode(&b)
data := make([]byte, 1+b.Len())
return rlp.StringLen(data)
// Return envelope size (type byte + encoded payload) without the outer
// string prefix. EncodingSizeGenericList adds the prefix itself, so
// including it here would double-count and produce an oversized RLP
// frame, causing "value size exceeds available input length" error on peers.
return 1 + b.Len()
}

// EncodeRLP implements rlp.Encoder for database storage.
Expand Down
Loading
Loading