Skip to content

Commit cdcf9d9

Browse files
committed
rpc: add support for debug_setHead
1 parent 55a7cf8 commit cdcf9d9

File tree

11 files changed

+369
-24
lines changed

11 files changed

+369
-24
lines changed

cmd/rpcdaemon/rpcservices/eth_backend.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,14 @@ func (back *RemoteBackend) RemoveTrustedPeer(ctx context.Context, request *remot
431431
return result, nil
432432
}
433433

434+
func (back *RemoteBackend) SetHead(ctx context.Context, request *remoteproto.SetHeadRequest) (*remoteproto.SetHeadReply, error) {
435+
result, err := back.remoteEthBackend.SetHead(ctx, request)
436+
if err != nil {
437+
return nil, fmt.Errorf("ETHBACKENDClient.SetHead() error: %w", err)
438+
}
439+
return result, nil
440+
}
441+
434442
func (back *RemoteBackend) Peers(ctx context.Context) ([]*p2p.PeerInfo, error) {
435443
rpcPeers, err := back.remoteEthBackend.Peers(ctx, &emptypb.Empty{})
436444
if err != nil {

db/rawdb/rawtemporaldb/accessors_commitment.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ import (
1010
)
1111

1212
func CanUnwindToBlockNum(tx kv.TemporalTx) (uint64, error) {
13-
minUnwindale, err := changeset.ReadLowestUnwindableBlock(tx)
13+
minUnwindable, err := changeset.ReadLowestUnwindableBlock(tx)
1414
if err != nil {
1515
return 0, err
1616
}
17-
if minUnwindale == math.MaxUint64 { // no unwindable block found
18-
minUnwindale, err = commitmentdb.LatestBlockNumWithCommitment(tx)
19-
log.Warn("no unwindable block found from changesets, falling back to latest with commitment", "block", minUnwindale, "err", err)
20-
return minUnwindale, err
17+
if minUnwindable == math.MaxUint64 { // no unwindable block found
18+
minUnwindable, err = commitmentdb.LatestBlockNumWithCommitment(tx)
19+
log.Warn("no unwindable block found from changesets, falling back to latest with commitment", "block", minUnwindable, "err", err)
20+
return minUnwindable, err
2121
}
22-
if minUnwindale > 0 {
23-
minUnwindale-- // UnwindTo is exclusive, i.e. (unwindPoint,tip] get unwound
22+
if minUnwindable > 0 {
23+
minUnwindable-- // UnwindTo is exclusive, i.e. (unwindPoint,tip] get unwound
2424
}
25-
return minUnwindale, nil
25+
return minUnwindable, nil
2626
}
2727

2828
func CanUnwindBeforeBlockNum(blockNum uint64, tx kv.TemporalTx) (unwindableBlockNum uint64, ok bool, err error) {

execution/execmodule/set_head.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2026 The Erigon Authors
2+
// This file is part of Erigon.
3+
//
4+
// Erigon is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Erigon is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package execmodule
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/erigontech/erigon/common"
24+
"github.com/erigontech/erigon/db/kv"
25+
"github.com/erigontech/erigon/db/kv/rawdbv3"
26+
"github.com/erigontech/erigon/db/rawdb"
27+
"github.com/erigontech/erigon/db/rawdb/rawtemporaldb"
28+
"github.com/erigontech/erigon/db/state/execctx"
29+
"github.com/erigontech/erigon/execution/stagedsync"
30+
"github.com/erigontech/erigon/execution/stagedsync/stages"
31+
)
32+
33+
func getLatestBlockNumber(tx kv.Tx) (uint64, error) {
34+
forkchoiceHeadHash := rawdb.ReadForkchoiceHead(tx)
35+
if forkchoiceHeadHash != (common.Hash{}) {
36+
forkchoiceHeadNum := rawdb.ReadHeaderNumber(tx, forkchoiceHeadHash)
37+
if forkchoiceHeadNum != nil {
38+
return *forkchoiceHeadNum, nil
39+
}
40+
}
41+
42+
blockNum, err := stages.GetStageProgress(tx, stages.Execution)
43+
if err != nil {
44+
return 0, fmt.Errorf("getting latest block number: %w", err)
45+
}
46+
47+
return blockNum, nil
48+
}
49+
50+
// SetHead rewinds the local chain to the specified block number by unwinding
51+
// all staged sync stages. This is the core implementation used by debug_setHead.
52+
func (e *ExecModule) SetHead(ctx context.Context, targetBlock uint64) error {
53+
if !e.semaphore.TryAcquire(1) {
54+
return fmt.Errorf("execution module is busy")
55+
}
56+
defer e.semaphore.Release(1)
57+
58+
tx, err := e.db.BeginTemporalRw(ctx)
59+
if err != nil {
60+
return fmt.Errorf("failed to begin rw transaction: %w", err)
61+
}
62+
defer tx.Rollback()
63+
64+
// Get the current head block number
65+
currentHead, err := getLatestBlockNumber(tx)
66+
if err != nil {
67+
return fmt.Errorf("failed to get current head: %w", err)
68+
}
69+
70+
if targetBlock > currentHead {
71+
return fmt.Errorf("cannot set head to a future block: target %d, current head %d", targetBlock, currentHead)
72+
}
73+
74+
if targetBlock == currentHead {
75+
return nil // already at the target
76+
}
77+
78+
// Check if we can unwind that far back
79+
minUnwindableBlock, err := rawtemporaldb.CanUnwindToBlockNum(tx)
80+
if err != nil {
81+
return fmt.Errorf("failed to check minimum unwindable block: %w", err)
82+
}
83+
if targetBlock < minUnwindableBlock {
84+
return fmt.Errorf("cannot unwind to block %d: minimum unwindable block is %d", targetBlock, minUnwindableBlock)
85+
}
86+
87+
// Verify the target block exists in the canonical chain
88+
targetHash, ok, err := e.blockReader.CanonicalHash(ctx, tx, targetBlock)
89+
if err != nil {
90+
return fmt.Errorf("failed to get canonical hash for block %d: %w", targetBlock, err)
91+
}
92+
if !ok {
93+
return fmt.Errorf("block %d not found in canonical chain", targetBlock)
94+
}
95+
96+
// Create SharedDomains context for the unwind
97+
sd, err := execctx.NewSharedDomains(ctx, tx, e.logger)
98+
if err != nil {
99+
return fmt.Errorf("failed to create shared domains: %w", err)
100+
}
101+
defer sd.Close()
102+
103+
// Set the unwind point and run the unwind
104+
if err := e.executionPipeline.UnwindTo(targetBlock, stagedsync.StagedUnwind, tx); err != nil {
105+
return fmt.Errorf("failed to set unwind point: %w", err)
106+
}
107+
108+
if err := e.hook.BeforeRun(tx, true); err != nil {
109+
return fmt.Errorf("hook BeforeRun failed: %w", err)
110+
}
111+
112+
if err := e.executionPipeline.RunUnwind(sd, tx); err != nil {
113+
return fmt.Errorf("failed to run unwind: %w", err)
114+
}
115+
116+
// Truncate TxNums above the target block
117+
if err := rawdbv3.TxNums.Truncate(tx, targetBlock+1); err != nil {
118+
return fmt.Errorf("failed to truncate tx nums: %w", err)
119+
}
120+
121+
// Update the head block hash
122+
rawdb.WriteHeadBlockHash(tx, targetHash)
123+
124+
// Update stage progress for headers and bodies
125+
if err := stages.SaveStageProgress(tx, stages.Headers, targetBlock); err != nil {
126+
return fmt.Errorf("failed to save headers stage progress: %w", err)
127+
}
128+
if err := stages.SaveStageProgress(tx, stages.Bodies, targetBlock); err != nil {
129+
return fmt.Errorf("failed to save bodies stage progress: %w", err)
130+
}
131+
if err := stages.SaveStageProgress(tx, stages.BlockHashes, targetBlock); err != nil {
132+
return fmt.Errorf("failed to save block hashes stage progress: %w", err)
133+
}
134+
135+
// Flush and commit
136+
if err := sd.Flush(ctx, tx); err != nil {
137+
return fmt.Errorf("failed to flush shared domains: %w", err)
138+
}
139+
sd.Close()
140+
141+
if err := tx.Commit(); err != nil {
142+
return fmt.Errorf("failed to commit transaction: %w", err)
143+
}
144+
145+
e.logger.Info("SetHead: successfully rewound chain", "targetBlock", targetBlock, "previousHead", currentHead)
146+
return nil
147+
}

node/eth/backend.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,6 +1501,10 @@ func (s *Ethereum) RemoveTrustedPeer(ctx context.Context, req *remoteproto.Remov
15011501
return &remoteproto.RemovePeerReply{Success: true}, nil
15021502
}
15031503

1504+
func (s *Ethereum) SetHead(ctx context.Context, targetBlock uint64) error {
1505+
return s.execModule.SetHead(ctx, targetBlock)
1506+
}
1507+
15041508
// Protocols returns all the currently configured
15051509
// network protocols to start.
15061510
func (s *Ethereum) Protocols() []p2p.Protocol {

node/privateapi/ethbackend.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type EthBackend interface {
8585
RemovePeer(ctx context.Context, url *remoteproto.RemovePeerRequest) (*remoteproto.RemovePeerReply, error)
8686
AddTrustedPeer(ctx context.Context, url *remoteproto.AddPeerRequest) (*remoteproto.AddPeerReply, error)
8787
RemoveTrustedPeer(ctx context.Context, url *remoteproto.RemovePeerRequest) (*remoteproto.RemovePeerReply, error)
88+
SetHead(ctx context.Context, targetBlock uint64) error
8889
}
8990

9091
func NewEthBackendServer(ctx context.Context, eth EthBackend, db kv.TemporalRwDB, notifications *shards.Notifications, blockReader services.FullBlockReader,
@@ -449,6 +450,13 @@ func (s *EthBackendServer) RemoveTrustedPeer(ctx context.Context, req *remotepro
449450
return s.eth.RemoveTrustedPeer(ctx, req)
450451
}
451452

453+
func (s *EthBackendServer) SetHead(ctx context.Context, req *remoteproto.SetHeadRequest) (*remoteproto.SetHeadReply, error) {
454+
if err := s.eth.SetHead(ctx, req.BlockNumber); err != nil {
455+
return nil, err
456+
}
457+
return &remoteproto.SetHeadReply{}, nil
458+
}
459+
452460
func (s *EthBackendServer) SubscribeLogs(server remoteproto.ETHBACKEND_SubscribeLogsServer) (err error) {
453461
if s.logsFilter != nil {
454462
return s.logsFilter.subscribeLogs(server)

rpc/jsonrpc/daemon.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func APIList(db kv.TemporalRoDB, eth rpchelper.ApiBackend, txPool txpoolproto.Tx
5454
erigonImpl := NewErigonAPI(base, db, eth)
5555
txpoolImpl := NewTxPoolAPI(base, db, txPool)
5656
netImpl := NewNetAPIImpl(eth)
57-
debugImpl := NewPrivateDebugAPI(base, db, cfg.Gascap, cfg.GethCompatibility)
57+
debugImpl := NewPrivateDebugAPI(base, db, eth, cfg.Gascap, cfg.GethCompatibility)
5858
traceImpl := NewTraceAPI(base, db, cfg)
5959
web3Impl := NewWeb3APIImpl(eth)
6060
adminImpl := NewAdminAPI(eth)

rpc/jsonrpc/debug_api.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/erigontech/erigon/execution/state"
3636
tracersConfig "github.com/erigontech/erigon/execution/tracing/tracers/config"
3737
"github.com/erigontech/erigon/execution/types/accounts"
38+
"github.com/erigontech/erigon/node/gointerfaces/remoteproto"
3839
"github.com/erigontech/erigon/rpc"
3940
"github.com/erigontech/erigon/rpc/ethapi"
4041
"github.com/erigontech/erigon/rpc/jsonstream"
@@ -65,6 +66,7 @@ type PrivateDebugAPI interface {
6566
GetRawReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]hexutil.Bytes, error)
6667
GetBadBlocks(ctx context.Context) ([]map[string]any, error)
6768
GetRawTransaction(ctx context.Context, hash common.Hash) (hexutil.Bytes, error)
69+
SetHead(ctx context.Context, number hexutil.Uint64) error
6870
FreeOSMemory()
6971
SetGCPercent(v int) int
7072
SetMemoryLimit(limit int64) int64
@@ -76,21 +78,51 @@ type PrivateDebugAPI interface {
7678
type DebugAPIImpl struct {
7779
*BaseAPI
7880
db kv.TemporalRoDB
81+
ethBackend rpchelper.ApiBackend
7982
GasCap uint64
8083
gethCompatibility bool // Geth-compatible storage iteration order for debug_storageRangeAt
8184
}
8285

8386
// NewPrivateDebugAPI returns PrivateDebugAPIImpl instance
84-
func NewPrivateDebugAPI(base *BaseAPI, db kv.TemporalRoDB, gascap uint64, gethCompatibility bool) *DebugAPIImpl {
87+
func NewPrivateDebugAPI(base *BaseAPI, db kv.TemporalRoDB, ethBackend rpchelper.ApiBackend, gascap uint64, gethCompatibility bool) *DebugAPIImpl {
8588
return &DebugAPIImpl{
8689
BaseAPI: base,
8790
db: db,
91+
ethBackend: ethBackend,
8892
GasCap: gascap,
8993
gethCompatibility: gethCompatibility,
9094
}
9195
}
9296

93-
// storageRangeAt implements debug_storageRangeAt. Returns information about a range of storage locations (if any) for the given address.
97+
// SetHead implements debug_setHead. Rewinds the local chain to the specified block number.
98+
func (api *DebugAPIImpl) SetHead(ctx context.Context, number hexutil.Uint64) error {
99+
blockNum := number.Uint64()
100+
101+
tx, err := api.db.BeginTemporalRo(ctx)
102+
if err != nil {
103+
return err
104+
}
105+
defer tx.Rollback()
106+
107+
currentHead, err := rpchelper.GetLatestBlockNumber(tx)
108+
if err != nil {
109+
return err
110+
}
111+
if blockNum > currentHead {
112+
return fmt.Errorf("block number %d is in the future: current head is %d", blockNum, currentHead)
113+
}
114+
115+
if err := api.BaseAPI.checkPruneHistory(ctx, tx, blockNum); err != nil {
116+
return err
117+
}
118+
119+
tx.Rollback() // release read tx before the backend opens write tx
120+
121+
_, err = api.ethBackend.SetHead(ctx, &remoteproto.SetHeadRequest{BlockNumber: blockNum})
122+
return err
123+
}
124+
125+
// StorageRangeAt implements debug_storageRangeAt. Returns information about a range of storage locations (if any) for the given address.
94126
func (api *DebugAPIImpl) StorageRangeAt(ctx context.Context, blockHash common.Hash, txIndex uint64, contractAddress common.Address, keyStart hexutil.Bytes, maxResult int) (StorageRangeResult, error) {
95127
tx, err := api.db.BeginTemporalRo(ctx)
96128
if err != nil {

0 commit comments

Comments
 (0)