diff --git a/.golangci.yml b/.golangci.yml index 962242adaf..aae7da6562 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,7 @@ # This file configures github.com/golangci/golangci-lint. version: '2' run: - go: '1.26.1' + go: '1.26.2' tests: true linters: default: none diff --git a/Dockerfile b/Dockerfile index dc1517c7ce..32098fced0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ─── BUILDER STAGE ─────────────────────────────────────────────────────────────── -FROM golang:1.26.1-alpine AS builder +FROM golang:1.26.2-alpine AS builder ARG BOR_DIR=/var/lib/bor/ ENV BOR_DIR=$BOR_DIR diff --git a/Dockerfile.alltools b/Dockerfile.alltools index 1ac856f090..0fe1dc4a57 100644 --- a/Dockerfile.alltools +++ b/Dockerfile.alltools @@ -1,5 +1,5 @@ # Build Geth in a stock Go builder container -FROM golang:1.26.1-alpine AS builder +FROM golang:1.26.2-alpine AS builder RUN apk add --no-cache make gcc musl-dev linux-headers git diff --git a/Makefile b/Makefile index 099b61f083..622d1d6045 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ lint: lint-deps: rm -f ./build/bin/golangci-lint - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./build/bin v2.11.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./build/bin v2.11.4 .PHONY: vulncheck diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index 768125cbd1..db168eb7c1 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -1,6 +1,6 @@ module github.com/ethereum/go-ethereum/cmd/keeper -go 1.26.1 +go 1.26.2 require ( github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260104020744-7268a54d0358 diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum index e67813369a..43778f130f 100644 --- a/cmd/keeper/go.sum +++ b/cmd/keeper/go.sum @@ -165,11 +165,11 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 5b44c3f021..444c327134 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -350,7 +350,7 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine and processes withdrawals on top. -func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) []*types.Receipt { +func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) ([]*types.Receipt, error) { if !beacon.IsPoSHeader(header) { return beacon.ethone.Finalize(chain, header, state, body, receipts) } @@ -362,7 +362,7 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) } // No block reward which is issued by consensus layer instead. - return receipts + return receipts, nil } // FinalizeAndAssemble implements consensus.Engine, setting the final state and @@ -384,7 +384,10 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea } } // Finalize and assemble the block. - receipts = beacon.Finalize(chain, header, state, body, receipts) + receipts, err := beacon.Finalize(chain, header, state, body, receipts) + if err != nil { + return nil, nil, 0, err + } // Assign the final state root to header. start := time.Now() diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 2b0ca4fcb7..242ebc9bfd 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1173,28 +1173,27 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, w // Finalize implements consensus.Engine, ensuring no uncles are set, nor block // rewards given. -func (c *Bor) Finalize(chain consensus.ChainHeaderReader, header *types.Header, wrappedState vm.StateDB, body *types.Body, receipts []*types.Receipt) []*types.Receipt { - headerNumber := header.Number.Uint64() +func (c *Bor) Finalize(chain consensus.ChainHeaderReader, header *types.Header, wrappedState vm.StateDB, body *types.Body, receipts []*types.Receipt) ([]*types.Receipt, error) { + // Reject the block if it has withdrawals or requests if body.Withdrawals != nil || header.WithdrawalsHash != nil { - return nil + return nil, consensus.ErrUnexpectedWithdrawals } if header.RequestsHash != nil { - return nil + return nil, consensus.ErrUnexpectedRequests } var ( + headerNumber = header.Number.Uint64() stateSyncData []*types.StateSyncData err error ) - if IsSprintStart(headerNumber, c.config.CalculateSprint(headerNumber)) { start := time.Now() cx := statefull.ChainContext{Chain: chain, Bor: c} // check and commit span if !c.config.IsRio(header.Number) { if err := c.checkAndCommitSpan(wrappedState, header, cx); err != nil { - log.Error("Error while committing span", "error", err) - return nil + return nil, fmt.Errorf("error while committing span: %w", err) } } @@ -1202,8 +1201,7 @@ func (c *Bor) Finalize(chain consensus.ChainHeaderReader, header *types.Header, // commit states stateSyncData, err = c.CommitStates(wrappedState, header, cx) if err != nil { - log.Error("Error while committing states", "error", err) - return nil + return nil, fmt.Errorf("%w: error while committing states: %w", core.ErrStateSyncProcessing, err) } } // Get the underlying state for updating consensus time @@ -1215,31 +1213,42 @@ func (c *Bor) Finalize(chain consensus.ChainHeaderReader, header *types.Header, // the wrapped state here as it may have a hooked state db instance which can help // in tracing if it's enabled. if err = c.changeContractCodeIfNeeded(headerNumber, wrappedState); err != nil { - log.Error("Error changing contract code", "error", err) - return nil + return nil, fmt.Errorf("error changing contract code: %w", err) } - if len(stateSyncData) > 0 && c.config != nil && c.config.IsMadhugiri(header.Number) { - if len(body.Transactions) > 0 { - // Craft a state-sync tx to validate it against the tx in block body - stateSyncTx := types.NewTx(&types.StateSyncTx{ - StateSyncData: stateSyncData, - }) - lastTx := body.Transactions[len(body.Transactions)-1] - if stateSyncTx.Hash() != lastTx.Hash() { - log.Error("Invalid state-sync tx in block body", "got", lastTx.Hash(), "want", stateSyncTx.Hash()) - return receipts - } - if lastTx.Type() == types.StateSyncTxType { - receipts = insertStateSyncTransactionAndCalculateReceipt(lastTx, header, body, wrappedState, receipts) - } - } - } else { - // set state sync - hc := chain.(*core.HeaderChain) + // Set state-sync in any case + if hc, ok := chain.(*core.HeaderChain); ok { hc.SetStateSync(stateSyncData) } - return receipts + + if len(stateSyncData) == 0 { + return receipts, nil + } + + txs := body.Transactions + isMadhugiri := c.config != nil && c.config.IsMadhugiri(header.Number) + + // Pre-Madhugiri, state-sync transactions were not included in block body so we can safely return + if !isMadhugiri { + return receipts, nil + } + + // Reject the block as heimdall suggests presence of state-sync event(s) but state-sync + // transaction is missing from block body. + if len(txs) == 0 || txs[len(txs)-1].Type() != types.StateSyncTxType { + return nil, fmt.Errorf("%w: block body missing state-sync transaction, heimdall reported %d event(s)", core.ErrStateSyncMismatch, len(stateSyncData)) + } + + // Craft a state-sync tx to validate it against the tx in block body + stateSyncTx := types.NewTx(&types.StateSyncTx{ + StateSyncData: stateSyncData, + }) + lastTx := txs[len(txs)-1] + if stateSyncTx.Hash() != lastTx.Hash() { + return nil, fmt.Errorf("%w: hash mismatch, got %s want %s", core.ErrStateSyncMismatch, lastTx.Hash(), stateSyncTx.Hash()) + } + receipts = insertStateSyncTransactionAndCalculateReceipt(lastTx, header, body, wrappedState, receipts) + return receipts, nil } func insertStateSyncTransactionAndCalculateReceipt(stateSyncTx *types.Transaction, header *types.Header, body *types.Body, state vm.StateDB, receipts []*types.Receipt) []*types.Receipt { diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 4db081af2c..86e8707893 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -282,7 +282,8 @@ func TestGenesisContractChange(t *testing.T) { ParentHash: root, Number: big.NewInt(num), } - b.Finalize(chain.HeaderChain(), h, statedb, &types.Body{Withdrawals: nil, Transactions: nil, Uncles: nil}, nil) + _, err = b.Finalize(chain.HeaderChain(), h, statedb, &types.Body{Withdrawals: nil, Transactions: nil, Uncles: nil}, nil) + require.NoError(t, err) // write state to database root, err := statedb.Commit(0, false, true) @@ -2313,8 +2314,9 @@ func TestFinalize_WithdrawalsRejection(t *testing.T) { statedb := newStateDBForTest(t, genesis.Root) body := &types.Body{Withdrawals: []*types.Withdrawal{{Validator: 1}}} - result := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) - require.Nil(t, result) // returns nil on withdrawals + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) + require.Nil(t, result) + require.ErrorIs(t, err, consensus.ErrUnexpectedWithdrawals) } func TestFinalize_RequestsHashRejection(t *testing.T) { @@ -2330,8 +2332,9 @@ func TestFinalize_RequestsHashRejection(t *testing.T) { statedb := newStateDBForTest(t, genesis.Root) body := &types.Body{} - result := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) - require.Nil(t, result) // returns nil on requests hash + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) + require.Nil(t, result) + require.ErrorIs(t, err, consensus.ErrUnexpectedRequests) } func TestNew(t *testing.T) { t.Parallel() @@ -2699,9 +2702,10 @@ func TestFinalize_NonSprintBlock(t *testing.T) { h := &types.Header{Number: big.NewInt(5), ParentHash: genesis.Hash(), Time: genesis.Time + 10, GasLimit: genesis.GasLimit} body := &types.Body{} - result := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) // For non-sprint blocks, Finalize returns the receipts (possibly nil) // It should not error + require.NoError(t, err) require.Nil(t, result) // nil receipts for non-sprint with no prior receipts } @@ -2720,7 +2724,8 @@ func TestFinalize_SprintBlockWithoutHeimdall(t *testing.T) { h := &types.Header{Number: big.NewInt(16), ParentHash: genesis.Hash(), Time: genesis.Time + 32, GasLimit: genesis.GasLimit} body := &types.Body{} - result := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, nil) + require.NoError(t, err) require.Nil(t, result) // nil receipts expected } func TestFetchAndCommitSpan_WithHeimdallClient(t *testing.T) { @@ -3185,8 +3190,9 @@ func TestFinalize_SprintBlockWithCommitSpan(t *testing.T) { body := &types.Body{} inputReceipts := make([]*types.Receipt, 0) - receipts := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) + receipts, err := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) // Should succeed (no HeimdallClient so CommitStates is skipped) + require.NoError(t, err) require.NotNil(t, receipts) } func TestCalcDifficulty_WithSnapshot(t *testing.T) { @@ -3464,7 +3470,8 @@ func TestFinalize_NonSprintBlockNoStateSync(t *testing.T) { body := &types.Body{} inputReceipts := make([]*types.Receipt, 0) - receipts := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) + receipts, err := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) + require.NoError(t, err) require.NotNil(t, receipts) } func TestVerifySeal_BhilaiNonPrimaryFutureBlock(t *testing.T) { @@ -3645,9 +3652,212 @@ func TestFinalize_SprintWithHeimdallCommitStates(t *testing.T) { body := &types.Body{} inputReceipts := make([]*types.Receipt, 0) - receipts := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) + receipts, err := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) + require.NoError(t, err) + require.NotNil(t, receipts) +} + +// madhugiriBorConfig returns a BorConfig with Madhugiri enabled from genesis, +// used for tests that exercise state-sync transaction validation in block bodies. +func madhugiriBorConfig() *params.BorConfig { + return ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 16}, + Period: map[string]uint64{"0": 2}, + IndoreBlock: big.NewInt(0), + MadhugiriBlock: big.NewInt(0), + StateSyncConfirmationDelay: map[string]uint64{"0": 0}, + RioBlock: big.NewInt(1000000), + } +} + +// setupMadhugiriStateSyncTest creates a Bor engine with Madhugiri enabled, a mock Heimdall +// client returning the provided events, and a sprint-start header at block 16. It returns +// the chain, engine, header, and genesis header for use in Finalize tests. +func setupMadhugiriStateSyncTest(t *testing.T, events []*clerk.EventRecordWithTime, heimdallOverride IHeimdallClient) (*core.BlockChain, *Bor, *types.Header, *types.Header) { + t.Helper() + + addr1 := common.HexToAddress("0x1") + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + mockGC := &mockGenesisContractForCommitStatesIndore{lastStateID: 0, gasUsed: 100} + + borCfg := madhugiriBorConfig() + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(time.Now().Unix())-200) + b.GenesisContractsClient = mockGC + + if heimdallOverride != nil { + b.SetHeimdallClient(heimdallOverride) + } else { + b.SetHeimdallClient(&mockHeimdallClient{ + span: &borTypes.Span{ + Id: 0, StartBlock: 0, EndBlock: 255, BorChainId: "1", + ValidatorSet: stakeTypes.ValidatorSet{ + Validators: []*stakeTypes.Validator{{ValId: 1, Signer: addr1.Hex(), VotingPower: 1}}, + }, + SelectedProducers: []stakeTypes.Validator{{ValId: 1, Signer: addr1.Hex(), VotingPower: 1}}, + }, + events: events, + }) + } + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + + h := &types.Header{ + Number: big.NewInt(16), // sprint start (16 % 16 == 0) + ParentHash: genesis.Hash(), + Time: uint64(time.Now().Unix()), + } + + return chain, b, h, genesis +} + +// defaultStateSyncEvents returns a single state-sync event for testing. +func defaultStateSyncEvents() []*clerk.EventRecordWithTime { + return []*clerk.EventRecordWithTime{{ + EventRecord: clerk.EventRecord{ + ID: 1, Contract: common.HexToAddress("0x1001"), Data: []byte{0x01}, ChainID: "1", + }, + Time: time.Now().Add(-60 * time.Second), + }} +} + +// matchingStateSyncTx returns a StateSyncTx that matches the data returned by +// CommitStates when processing defaultStateSyncEvents. +func matchingStateSyncTx() *types.Transaction { + return types.NewTx(&types.StateSyncTx{ + StateSyncData: []*types.StateSyncData{{ + ID: 1, + Contract: common.HexToAddress("0x1001"), + Data: []byte{0x01}, + TxHash: common.Hash{}, + }}, + }) +} + +// TestFinalize_StateSyncMismatch_EmptyBody tests that a block which applies state-sync +// transactions to state but doesn't have the transaction in block body (post Madhugiri) +// is rejected with `ErrStateSyncMismatch` error. +func TestFinalize_StateSyncMismatch_EmptyBody(t *testing.T) { + t.Parallel() + + chain, b, h, genesis := setupMadhugiriStateSyncTest(t, defaultStateSyncEvents(), nil) + statedb := newStateDBForTest(t, genesis.Root) + + body := &types.Body{} + receipts := make([]*types.Receipt, 0) + + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) + require.ErrorIs(t, err, core.ErrStateSyncMismatch) + require.ErrorContains(t, err, "block body missing state-sync transaction") + require.Nil(t, result) +} + +// TestFinalize_StateSyncMismatch_EmptyBody tests that a block which applies state-sync +// transactions to state but last transaction in block body is not StateSyncTxType is +// rejected with `ErrStateSyncMismatch` error. +func TestFinalize_StateSyncMismatch_LastTxNotStateSyncType(t *testing.T) { + t.Parallel() + + chain, b, h, genesis := setupMadhugiriStateSyncTest(t, defaultStateSyncEvents(), nil) + statedb := newStateDBForTest(t, genesis.Root) + + // A legacy transaction — its Type() is not StateSyncTxType + addr := common.HexToAddress("0x1") + legacyTx := types.NewTx(&types.LegacyTx{Nonce: 0, To: &addr}) + + body := &types.Body{Transactions: []*types.Transaction{legacyTx}} + receipts := make([]*types.Receipt, 0) + + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) + require.ErrorIs(t, err, core.ErrStateSyncMismatch) + require.ErrorContains(t, err, "block body missing state-sync transaction") + require.Nil(t, result) +} + +// TestFinalize_StateSyncMismatch_WrongHash tests that a block body containing a StateSyncTx +// with different data than what Heimdall returned is rejected with ErrStateSyncMismatch. +func TestFinalize_StateSyncMismatch_WrongHash(t *testing.T) { + t.Parallel() + + chain, b, h, genesis := setupMadhugiriStateSyncTest(t, defaultStateSyncEvents(), nil) + statedb := newStateDBForTest(t, genesis.Root) + + // Construct a StateSyncTx with different data than what Heimdall returns + wrongTx := types.NewTx(&types.StateSyncTx{ + StateSyncData: []*types.StateSyncData{{ + ID: 1, + Contract: common.HexToAddress("0x1001"), + Data: []byte{0x99, 0x99}, // different from []byte{0x01} + TxHash: common.Hash{}, + }}, + }) + + body := &types.Body{Transactions: []*types.Transaction{wrongTx}} + receipts := make([]*types.Receipt, 0) + + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) + require.ErrorIs(t, err, core.ErrStateSyncMismatch) + require.ErrorContains(t, err, "hash mismatch") + require.Nil(t, result) +} + +// TestFinalize_ValidStateSyncTx tests the happy path: Madhugiri is enabled, Heimdall reports +// state-sync events, and the block body contains the correct matching StateSyncTx. Finalize +// should succeed and append a state-sync receipt. +func TestFinalize_ValidStateSyncTx(t *testing.T) { + t.Parallel() + + chain, b, h, genesis := setupMadhugiriStateSyncTest(t, defaultStateSyncEvents(), nil) + statedb := newStateDBForTest(t, genesis.Root) + + body := &types.Body{Transactions: []*types.Transaction{matchingStateSyncTx()}} + inputReceipts := make([]*types.Receipt, 0) + + receipts, err := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) + require.NoError(t, err) + require.NotNil(t, receipts) + // Finalize should have appended the state-sync receipt + require.Len(t, receipts, 1, "expected one state-sync receipt") +} + +// TestFinalize_StateSyncProcessingError tests that when CommitStates fails (e.g. LastStateId +// returns an error), Finalize returns ErrStateSyncProcessing. Note: CommitStates swallows +// StateSyncEvents fetch errors and returns empty data, so ErrStateSyncProcessing is only +// reachable through genesis contract failures like LastStateId. +func TestFinalize_StateSyncProcessingError(t *testing.T) { + t.Parallel() + + chain, b, h, genesis := setupMadhugiriStateSyncTest(t, defaultStateSyncEvents(), nil) + // Override genesis contract with one that fails, triggering CommitStates error + b.GenesisContractsClient = &failingGenesisContract{} + statedb := newStateDBForTest(t, genesis.Root) + + body := &types.Body{} + receipts := make([]*types.Receipt, 0) + + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) + require.ErrorIs(t, err, core.ErrStateSyncProcessing) + require.ErrorContains(t, err, "error while committing states") + require.Nil(t, result) +} + +// TestFinalize_NoStateSyncEvents_EmptyBodyOK tests that when Heimdall returns no state-sync +// events, an empty block body is accepted (no StateSyncTx required). +func TestFinalize_NoStateSyncEvents_EmptyBodyOK(t *testing.T) { + t.Parallel() + + // Empty events slice — no state sync to process + chain, b, h, genesis := setupMadhugiriStateSyncTest(t, []*clerk.EventRecordWithTime{}, nil) + statedb := newStateDBForTest(t, genesis.Root) + + body := &types.Body{} + inputReceipts := make([]*types.Receipt, 0) + + receipts, err := b.Finalize(chain.HeaderChain(), h, statedb, body, inputReceipts) + require.NoError(t, err) require.NotNil(t, receipts) + require.Len(t, receipts, 0, "no state-sync receipt expected when there are no events") } + func TestSnapshot_HeaderTraversal(t *testing.T) { t.Parallel() @@ -4156,8 +4366,9 @@ func TestFinalize_WithBlockAlloc(t *testing.T) { body := &types.Body{} receipts := make([]*types.Receipt, 0) - result := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) // Should process without error - exercises changeContractCodeIfNeeded + require.NoError(t, err) require.NotNil(t, result) } func TestCommitStates_WithOverrideStateSyncRecordsInRange(t *testing.T) { @@ -4376,8 +4587,9 @@ func TestFinalize_CheckAndCommitSpanError(t *testing.T) { receipts := make([]*types.Receipt, 0) // checkAndCommitSpan -> FetchAndCommitSpan -> CommitSpan should fail, - // which means Finalize returns nil - result := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) + // which means Finalize returns an error + result, err := b.Finalize(chain.HeaderChain(), h, statedb, body, receipts) + require.Error(t, err) require.Nil(t, result) } diff --git a/consensus/bor/contract/client.go b/consensus/bor/contract/client.go index 1ab8a9a754..1965c3b157 100644 --- a/consensus/bor/contract/client.go +++ b/consensus/bor/contract/client.go @@ -125,7 +125,7 @@ func (gc *GenesisContractsClient) LastStateId(state *state.StateDB, number uint6 // BOR: Do a 'CallWithState' so that we can fetch the last state ID from a given (incoming) // state instead of local(canonical) chain's state. - result, err := gc.ethAPI.CallWithState(context.Background(), ethapi.TransactionArgs{ + result, err := gc.ethAPI.CallWithState(ethapi.WithBorInternalCall(context.Background()), ethapi.TransactionArgs{ Gas: &SystemTxGas, To: &toAddress, Data: &msgData, diff --git a/consensus/bor/heimdall/span/spanner.go b/consensus/bor/heimdall/span/spanner.go index f63da09638..1b3be771ed 100644 --- a/consensus/bor/heimdall/span/spanner.go +++ b/consensus/bor/heimdall/span/spanner.go @@ -69,7 +69,7 @@ func (c *ChainSpanner) GetCurrentSpan(ctx context.Context, headerHash common.Has msgData := (hexutil.Bytes)(data) toAddress := c.validatorContractAddress - result, err := c.ethAPI.CallWithState(ctx, ethapi.TransactionArgs{ + result, err := c.ethAPI.CallWithState(ethapi.WithBorInternalCall(ctx), ethapi.TransactionArgs{ Gas: &contract.SystemTxGas, To: &toAddress, Data: &msgData, @@ -168,7 +168,7 @@ func (c *ChainSpanner) getSpanByBlock(ctx context.Context, blockNrOrHash rpc.Blo spanMsgData := (hexutil.Bytes)(spanData) - spanResult, err := c.ethAPI.Call(ctx, ethapi.TransactionArgs{ + spanResult, err := c.ethAPI.Call(ethapi.WithBorInternalCall(ctx), ethapi.TransactionArgs{ Gas: &contract.SystemTxGas, To: &toAddress, Data: &spanMsgData, @@ -194,7 +194,7 @@ func (c *ChainSpanner) getProducersBySpanAndIndexMethod(ctx context.Context, blo producerMsgData := (hexutil.Bytes)(producerData) - result, err := c.ethAPI.Call(ctx, ethapi.TransactionArgs{ + result, err := c.ethAPI.Call(ethapi.WithBorInternalCall(ctx), ethapi.TransactionArgs{ Gas: &contract.SystemTxGas, To: &toAddress, Data: &producerMsgData, @@ -220,7 +220,7 @@ func (c *ChainSpanner) getFirstEndBlock(ctx context.Context, blockNrOrHash rpc.B firstEndBlockMsgData := (hexutil.Bytes)(firstEndBlockData) - firstEndBlockResult, err := c.ethAPI.Call(ctx, ethapi.TransactionArgs{ + firstEndBlockResult, err := c.ethAPI.Call(ethapi.WithBorInternalCall(ctx), ethapi.TransactionArgs{ Gas: &contract.SystemTxGas, To: &toAddress, Data: &firstEndBlockMsgData, @@ -249,7 +249,7 @@ func (c *ChainSpanner) getBorValidatorsWithoutId(ctx context.Context, blockNrOrH // call msgData := (hexutil.Bytes)(data) - result, err := c.ethAPI.Call(ctx, ethapi.TransactionArgs{ + result, err := c.ethAPI.Call(ethapi.WithBorInternalCall(ctx), ethapi.TransactionArgs{ Gas: &contract.SystemTxGas, To: &toAddress, Data: &msgData, diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 6201a00d76..e03214b42a 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -627,9 +627,9 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header // Finalize implements consensus.Engine. There is no post-transaction // consensus rules in clique, do nothing here. -func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) []*types.Receipt { +func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) ([]*types.Receipt, error) { // No block rewards in PoA, so the state remains as is - return receipts + return receipts, nil } // FinalizeAndAssemble implements consensus.Engine, ensuring no uncles are set, @@ -639,7 +639,10 @@ func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header * return nil, nil, 0, errors.New("clique does not support withdrawals") } // Finalize block - receipts = c.Finalize(chain, header, state, body, receipts) + receipts, err := c.Finalize(chain, header, state, body, receipts) + if err != nil { + return nil, nil, 0, err + } // Assign the final state root to header. start := time.Now() diff --git a/consensus/consensus.go b/consensus/consensus.go index fcf82150a6..1062f6d13d 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -91,7 +91,7 @@ type Engine interface { // // Note: The state database might be updated to reflect any consensus rules // that happen at finalization (e.g. block rewards). - Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) []*types.Receipt + Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) ([]*types.Receipt, error) // FinalizeAndAssemble runs any post-transaction state modifications (e.g. block // rewards or process withdrawals) and assembles the final block. diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index 7c2c0097d3..58a5509e31 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -506,10 +506,10 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine, accumulating the block and uncle rewards. -func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) []*types.Receipt { +func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, receipts []*types.Receipt) ([]*types.Receipt, error) { // Accumulate any block and uncle rewards accumulateRewards(chain.Config(), state, header, body.Uncles) - return receipts + return receipts, nil } // FinalizeAndAssemble implements consensus.Engine, accumulating the block and @@ -519,7 +519,9 @@ func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea return nil, nil, 0, errors.New("ethash does not support withdrawals") } // Finalize block - ethash.Finalize(chain, header, state, body, receipts) + if _, err := ethash.Finalize(chain, header, state, body, receipts); err != nil { + return nil, nil, 0, err + } // Assign the final state root to header. start := time.Now() diff --git a/core/error.go b/core/error.go index ac3c113b28..5dc8c14c45 100644 --- a/core/error.go +++ b/core/error.go @@ -172,10 +172,20 @@ var ( // Bor related errors var ( - // ErrStateSyncProcessing should be used when state-sync isn't applied correctly - // in bor consensus. It can be either due to - // - Error in fetching event from heimdall - // - Error in processing state-sync event in EVM - // - Invalid state-sync tx data in block body post Madhugiri HF - ErrStateSyncProcessing = errors.New("unable to process state-sync tx") + // ErrStateSyncProcessing is returned when the node fails to fetch or apply + // state-sync events from Heimdall during block finalization. This is an + // operational error — Heimdall may be temporarily unavailable or returning + // errors, and the operation may succeed on retry. + ErrStateSyncProcessing = errors.New("unable to process state-sync") + + // ErrStateSyncMismatch is returned when the locally computed state-sync result + // does not match the block body or receipts. This includes: + // - missing state-sync transaction in block body (post-Madhugiri) + // - state-sync transaction hash mismatch between block body and locally constructed one + // - receipt count mismatch after state-sync receipt insertion + // + // Unlike ErrStateSyncProcessing, this indicates a disagreement between the block + // producer and the local node's view of Heimdall state — the block was produced + // with different state-sync data than what this node computed. + ErrStateSyncMismatch = errors.New("state-sync mismatch") ) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 2e5bfceb52..6a676c27c9 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -427,14 +427,17 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat // Finalize the block, applying any consensus engine specific extras (e.g. block rewards), apply // state sync event (if any), and append the receipt. receiptsCountBeforeFinalize := len(receipts) - receipts = p.chain.Engine().Finalize(p.bc.hc, header, statedb, block.Body(), receipts) + receipts, err = p.chain.Engine().Finalize(p.bc.hc, header, statedb, block.Body(), receipts) + if err != nil { + return nil, err + } // apply state sync logs if config.Bor != nil && config.Bor.IsMadhugiri(block.Number()) { - // In case of any errors in state-sync tx processing, the number of receipts won't match - // the number of transactions in the block body. + // Defense-in-depth: if insertStateSyncTransactionAndCalculateReceipt silently failed + // to add the receipt, the count will be off. if len(block.Transactions()) != len(receipts) { - return nil, fmt.Errorf("err in bor.Finalize: %w", ErrStateSyncProcessing) + return nil, fmt.Errorf("%w: receipt count mismatch, txs=%d receipts=%d", ErrStateSyncMismatch, len(block.Transactions()), len(receipts)) } appliedNewStateSyncReceipt := receiptsCountBeforeFinalize+1 == len(receipts) diff --git a/core/state_processor.go b/core/state_processor.go index 4c6bd9d181..afc5039fe9 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -72,6 +72,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg blockNumber = block.Number() allLogs []*types.Log gp = new(GasPool).AddGas(block.GasLimit()) + err error ) // Set an empty context if nil @@ -154,14 +155,17 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg // Finalize the block, applying any consensus engine specific extras (e.g. block rewards), apply // state sync event (if any), and append the receipt. receiptsCountBeforeFinalize := len(receipts) - receipts = p.chain.Engine().Finalize(p.chain, header, statedb, block.Body(), receipts) + receipts, err = p.chain.Engine().Finalize(p.chain, header, statedb, block.Body(), receipts) + if err != nil { + return nil, err + } // apply state sync logs if p.chainConfig().Bor != nil && p.chainConfig().Bor.IsMadhugiri(block.Number()) { - // In case of any errors in state-sync tx processing, the number of receipts won't match - // the number of transactions in the block body. + // Defense-in-depth: if insertStateSyncTransactionAndCalculateReceipt silently failed + // to add the receipt, the count will be off. if len(block.Transactions()) != len(receipts) { - return nil, fmt.Errorf("err in bor.Finalize: %w", ErrStateSyncProcessing) + return nil, fmt.Errorf("%w: receipt count mismatch, txs=%d receipts=%d", ErrStateSyncMismatch, len(block.Transactions()), len(receipts)) } appliedNewStateSyncReceipt := receiptsCountBeforeFinalize+1 == len(receipts) diff --git a/eth/tracers/data.csv b/eth/tracers/data.csv index ea81ea7ab5..15466a0903 100644 --- a/eth/tracers/data.csv +++ b/eth/tracers/data.csv @@ -7,34 +7,34 @@ TransactionIndex, Incarnation, VersionTxIdx, VersionInc, Path, Operation 0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write -1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read -1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read +1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read +1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read +1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write -1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read -2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write -2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write +2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write +2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read -3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write +3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write +4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 686b0b800b..fca2f46e63 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -760,7 +760,6 @@ func ExampleRevertErrorData() { } func TestSimulateV1(t *testing.T) { - t.Skip("eth_simulateV1 is disabled on Bor — unskip when re-enabling") backend, _, err := newTestBackend(nil) if err != nil { t.Fatalf("Failed to create test backend: %v", err) @@ -826,7 +825,6 @@ func TestSimulateV1(t *testing.T) { } func TestSimulateV1WithBlockOverrides(t *testing.T) { - t.Skip("eth_simulateV1 is disabled on Bor — unskip when re-enabling") backend, _, err := newTestBackend(nil) if err != nil { t.Fatalf("Failed to create test backend: %v", err) @@ -889,7 +887,6 @@ func TestSimulateV1WithBlockOverrides(t *testing.T) { } func TestSimulateV1WithStateOverrides(t *testing.T) { - t.Skip("eth_simulateV1 is disabled on Bor — unskip when re-enabling") backend, _, err := newTestBackend(nil) if err != nil { t.Fatalf("Failed to create test backend: %v", err) @@ -957,7 +954,6 @@ func TestSimulateV1WithStateOverrides(t *testing.T) { } func TestSimulateV1WithBlockNumberOrHash(t *testing.T) { - t.Skip("eth_simulateV1 is disabled on Bor — unskip when re-enabling") backend, _, err := newTestBackend(nil) if err != nil { t.Fatalf("Failed to create test backend: %v", err) diff --git a/go.mod b/go.mod index f618297e80..fc650b5dbe 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/ethereum/go-ethereum // Note: Change the go image version in Dockerfile if you change this. -go 1.26.1 +go 1.26.2 require ( github.com/0xPolygon/crand v1.0.3 diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4d19da2854..badd5a3759 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -913,10 +913,9 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S // this makes sure resources are cleaned up. defer cancel() - // Note: Don't put a cap on gas if it's a system tx (coming from bor consensus). The - // easiest way to enforce this is to set globalGasCap for this call to 0 which will - // use the max possible limit. - if isBorSystemTx(b.ChainConfig().Bor, args.To) { + // Only bypass the RPC gas cap for system contract calls that originate from + // internal consensus code (marked via WithBorInternalCall ctx). + if isBorInternalCall(ctx) && isBorSystemTx(b.ChainConfig().Bor, args.To) { globalGasCap = 0 } gp := new(core.GasPool) @@ -1069,13 +1068,21 @@ func (api *BlockChainAPI) CallWithState(ctx context.Context, args TransactionArg // Note, this function doesn't make any changes in the state/blockchain and is // useful to execute and retrieve values. func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrOrHash *rpc.BlockNumberOrHash) ([]*simBlockResult, error) { - return nil, errors.New("eth_simulateV1 is not supported on Bor") - //nolint:govet // Unreachable code kept intentionally so it can be re-enabled easily. if len(opts.BlockStateCalls) == 0 { return nil, &invalidParamsError{message: "empty input"} } else if len(opts.BlockStateCalls) > maxSimulateBlocks { return nil, &clientLimitExceededError{message: "too many blocks"} } + var totalCalls int + for _, block := range opts.BlockStateCalls { + if len(block.Calls) > maxSimulateCallsPerBlock { + return nil, &clientLimitExceededError{message: fmt.Sprintf("too many calls in block: %d > %d", len(block.Calls), maxSimulateCallsPerBlock)} + } + totalCalls += len(block.Calls) + if totalCalls > maxSimulateTotalCalls { + return nil, &clientLimitExceededError{message: fmt.Sprintf("too many calls: %d > %d", totalCalls, maxSimulateTotalCalls)} + } + } if blockNrOrHash == nil { n := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) blockNrOrHash = &n @@ -1084,17 +1091,12 @@ func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrO if state == nil || err != nil { return nil, err } - gasCap := api.b.RPCGasCap() - if gasCap == 0 { - gasCap = gomath.MaxUint64 - } sim := &simulator{ - b: api.b, - state: state, - base: base, - chainConfig: api.b.ChainConfig(), - // Each tx and all the series of txes shouldn't consume more gas than cap - gp: new(core.GasPool).AddGas(gasCap), + b: api.b, + state: state, + base: base, + chainConfig: api.b.ChainConfig(), + budget: newGasBudget(api.b.RPCGasCap()), traceTransfers: opts.TraceTransfers, validate: opts.Validation, fullTx: opts.ReturnFullTransactions, @@ -2795,6 +2797,10 @@ func (api *DebugAPI) AccountAt(ctx context.Context, blockHash common.Hash, txInd } for idx := uint64(0); idx <= lastTxIdx && idx < uint64(len(block.Transactions())); idx++ { + if err := ctx.Err(); err != nil { + return nil, err + } + tx := block.Transactions()[idx] // Skip state-sync transactions if tx.Type() == types.StateSyncTxType { diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index c4e00c3280..ce26d7db3a 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -23,7 +23,6 @@ import ( "encoding/json" "errors" "fmt" - "math" "math/big" "os" "path/filepath" @@ -1562,22 +1561,7 @@ func TestCall(t *testing.T) { } } -func TestSimulateV1Disabled(t *testing.T) { - t.Parallel() - - genesis := &core.Genesis{ - Config: params.TestChainConfig, - Alloc: types.GenesisAlloc{}, - } - b := newTestBackend(t, 1, genesis, ethash.NewFaker(), nil) - api := NewBlockChainAPI(b) - result, err := api.SimulateV1(context.Background(), simOpts{}, nil) - require.Nil(t, result) - require.EqualError(t, err, "eth_simulateV1 is not supported on Bor") -} - func TestSimulateV1(t *testing.T) { - t.Skip("eth_simulateV1 is disabled on Bor — unskip when re-enabling") t.Parallel() // Initialize test accounts var ( @@ -2970,7 +2954,7 @@ func TestSimulateV1ChainLinkage(t *testing.T) { state: stateDB, base: baseHeader, chainConfig: backend.ChainConfig(), - gp: new(core.GasPool).AddGas(math.MaxUint64), + budget: newGasBudget(0), traceTransfers: false, validate: false, fullTx: false, @@ -3029,7 +3013,6 @@ func TestSimulateV1ChainLinkage(t *testing.T) { } func TestSimulateV1TxSender(t *testing.T) { - t.Skip("eth_simulateV1 is disabled on Bor — unskip when re-enabling") var ( sender = common.Address{0xaa, 0xaa} sender2 = common.Address{0xaa, 0xab} @@ -3056,7 +3039,7 @@ func TestSimulateV1TxSender(t *testing.T) { state: stateDB, base: baseHeader, chainConfig: backend.ChainConfig(), - gp: new(core.GasPool).AddGas(math.MaxUint64), + budget: newGasBudget(0), traceTransfers: false, validate: false, fullTx: true, @@ -5403,6 +5386,20 @@ func TestSubmitHashrate(t *testing.T) { } } +type testBackendCancelAccountAtReplay struct { + *testBackend + cancel context.CancelFunc + canceled bool +} + +func (b *testBackendCancelAccountAtReplay) GetEVM(ctx context.Context, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockContext *vm.BlockContext) *vm.EVM { + if !b.canceled { + b.canceled = true + b.cancel() + } + return b.testBackend.GetEVM(ctx, state, header, vmConfig, blockContext) +} + func TestAccountAt(t *testing.T) { t.Parallel() @@ -5529,4 +5526,16 @@ func TestAccountAt(t *testing.T) { t.Errorf("Expected zero nonce for non-existent account, got %d", result.Nonce) } }) + + t.Run("context cancellation during replay", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + wrapped := &testBackendCancelAccountAtReplay{ + testBackend: backend, + cancel: cancel, + } + api := NewDebugAPI(wrapped) + + _, err := api.AccountAt(ctx, blockHash, 0, addr) + require.ErrorIs(t, err, context.Canceled) + }) } diff --git a/internal/ethapi/bor_api.go b/internal/ethapi/bor_api.go index c92c62524c..a5cc581bec 100644 --- a/internal/ethapi/bor_api.go +++ b/internal/ethapi/bor_api.go @@ -47,6 +47,24 @@ const ( GetLogsMaxBlockRange = 1000 ) +// borInternalCallKey is an unexported type used as a context key. +// Because the type is unexported, no package outside internal/ethapi +// can construct a value of this type, making it impossible for +// external RPC callers to forge this context value. +type borInternalCallKey struct{} + +// WithBorInternalCall marks a context as originating from an internal +// consensus call. This allows Call to differentiate internal calls. +func WithBorInternalCall(ctx context.Context) context.Context { + return context.WithValue(ctx, borInternalCallKey{}, true) +} + +// isBorInternalCall returns true if the context was marked as an internal call. +func isBorInternalCall(ctx context.Context) bool { + v, _ := ctx.Value(borInternalCallKey{}).(bool) + return v +} + // isBorSystemTx checks if the tx is for bor genesis contract addresses or not func isBorSystemTx(borCfg *params.BorConfig, to *common.Address) bool { if borCfg == nil { @@ -927,6 +945,10 @@ func (api *BorAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*types.L // Iterate blocks from the beginning to the end for blockNum := begin; blockNum <= end; blockNum++ { + if err := ctx.Err(); err != nil { + return nil, err + } + block, receipts, err := api.getBlockAndReceipts(ctx, blockNum) if err != nil { return nil, err @@ -1020,6 +1042,10 @@ func (api *BorAPI) GetLatestLogs(ctx context.Context, crit FilterCriteria, logOp // Iterate blocks from the end to the beginning for blockNum := end; blockNum >= begin && blockNum <= end; blockNum-- { + if err := ctx.Err(); err != nil { + return nil, err + } + // Check the block count limit if blockCount > 0 && blocksScanned >= blockCount { break diff --git a/internal/ethapi/bor_api_test.go b/internal/ethapi/bor_api_test.go index 5679b4c98a..ee87539ae9 100644 --- a/internal/ethapi/bor_api_test.go +++ b/internal/ethapi/bor_api_test.go @@ -31,12 +31,14 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/internal/ethapi/override" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" @@ -2270,6 +2272,23 @@ func (b *testBackendWithProtocolVersion) ProtocolVersion() uint { return b.protocolVersion } +type testBackendWithCancelAfterFirstBlockLookup struct { + *testBackend + cancel context.CancelFunc + lookupCount int +} + +func (b *testBackendWithCancelAfterFirstBlockLookup) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + block, err := b.testBackend.BlockByNumber(ctx, number) + if number >= 0 { + b.lookupCount++ + if b.lookupCount == 1 { + b.cancel() + } + } + return block, err +} + func TestBorGetLatestLogs(t *testing.T) { t.Parallel() @@ -3162,6 +3181,56 @@ func TestGetLogsBlockRangeLimit(t *testing.T) { }) } +func TestGetLogsRespectsContextCancellationDuringScan(t *testing.T) { + t.Parallel() + + genesis := &core.Genesis{ + Config: params.AllEthashProtocolChanges, + Alloc: types.GenesisAlloc{}, + } + base := newTestBackend(t, 10, genesis, ethash.NewFaker(), nil) + ctx, cancel := context.WithCancel(context.Background()) + backend := &testBackendWithCancelAfterFirstBlockLookup{ + testBackend: base, + cancel: cancel, + } + api := NewBorAPI(backend) + + crit := FilterCriteria{ + FromBlock: big.NewInt(0), + ToBlock: big.NewInt(10), + } + + _, err := api.GetLogs(ctx, crit) + require.ErrorIs(t, err, context.Canceled) +} + +func TestGetLatestLogsRespectsContextCancellationDuringScan(t *testing.T) { + t.Parallel() + + genesis := &core.Genesis{ + Config: params.AllEthashProtocolChanges, + Alloc: types.GenesisAlloc{}, + } + base := newTestBackend(t, 10, genesis, ethash.NewFaker(), nil) + ctx, cancel := context.WithCancel(context.Background()) + backend := &testBackendWithCancelAfterFirstBlockLookup{ + testBackend: base, + cancel: cancel, + } + api := NewBorAPI(backend) + + crit := FilterCriteria{ + FromBlock: big.NewInt(0), + ToBlock: big.NewInt(10), + } + blockCount := uint64(10) + opts := LogFilterOptions{BlockCount: &blockCount} + + _, err := api.GetLatestLogs(ctx, crit, opts) + require.ErrorIs(t, err, context.Canceled) +} + // TestGetLogsLogCopySafety verifies that returned logs are copies, // not shared pointers to cached receipt data. func TestGetLogsLogCopySafety(t *testing.T) { @@ -4119,3 +4188,81 @@ func TestGetBlockByNumber_BorExtraFlag_PreCancun(t *testing.T) { _, ok := result["decodedExtra"] require.False(t, ok, "decodedExtra should not be present for pre-Cancun blocks") } + +// TestSystemTxGasCapBypass verifies that the RPC gas cap is only bypassed for +// internal consensus calls. No external RPC call should be able to bypass it. +func TestSystemTxGasCapBypass(t *testing.T) { + t.Parallel() + + // Bytecode that returns the gas available to the call. + // GAS PUSH1(0) MSTORE PUSH1(32) PUSH1(0) RETURN + gasReportBytes := hexutil.Bytes(common.FromHex("5a60005260206000f3")) + gasReportCode := &gasReportBytes + + systemAddr := common.HexToAddress("0x0000000000000000000000000000000000001000") + normalAddr := common.HexToAddress("0x0000000000000000000000000000000000009999") + + genesis := &core.Genesis{ + Config: params.MergedTestChainConfig, + Alloc: types.GenesisAlloc{}, + } + api := NewBlockChainAPI(newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + b.SetPoS() + })) + + latest := rpc.LatestBlockNumber + rpcGasCap := api.b.RPCGasCap() + tests := []struct { + name string + internal bool + to common.Address + wantCapped bool + }{ + { + name: "external RPC to system contract must be gas-capped", + internal: false, + to: systemAddr, + wantCapped: true, + }, + { + name: "internal consensus call to system contract bypasses gas cap", + internal: true, + to: systemAddr, + wantCapped: false, + }, + { + name: "external RPC to normal address is gas-capped", + internal: false, + to: normalAddr, + wantCapped: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if tt.internal { + ctx = WithBorInternalCall(ctx) + } + + blockNr := rpc.BlockNumberOrHashWithNumber(latest) + result, err := api.Call(ctx, TransactionArgs{ + To: &tt.to, + }, &blockNr, &override.StateOverride{ + tt.to: override.OverrideAccount{ + Code: gasReportCode, + }, + }, nil) + require.NoError(t, err) + + gasAvailable := new(big.Int).SetBytes(result) + if tt.wantCapped { + require.True(t, gasAvailable.Uint64() <= rpcGasCap, + "gas should be capped to RPCGasCap (%d), got %s", rpcGasCap, gasAvailable) + } else { + require.True(t, gasAvailable.Uint64() > rpcGasCap, + "gas should bypass RPCGasCap (%d), got %s", rpcGasCap, gasAvailable) + } + }) + } +} diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index 5443f9901c..610d8da44d 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/big" "time" @@ -44,6 +45,14 @@ const ( // in a single request. maxSimulateBlocks = 256 + // maxSimulateCallsPerBlock is the maximum number of calls allowed in a + // single simulated block. + maxSimulateCallsPerBlock = 5000 + + // maxSimulateTotalCalls is the maximum total number of calls allowed + // across all simulated blocks in a single request. + maxSimulateTotalCalls = 10000 + // timestampIncrement is the default increment between block timestamps. timestampIncrement = 12 ) @@ -60,6 +69,7 @@ type simCallResult struct { ReturnValue hexutil.Bytes `json:"returnData"` Logs []*types.Log `json:"logs"` GasUsed hexutil.Uint64 `json:"gasUsed"` + MaxUsedGas hexutil.Uint64 `json:"maxUsedGas"` Status hexutil.Uint64 `json:"status"` Error *callError `json:"error,omitempty"` } @@ -169,6 +179,39 @@ func (m *simChainHeadReader) GetHeaderByHash(hash common.Hash) *types.Header { return header } +// gasBudget tracks the remaining gas allowed across all simulated blocks. +// It enforces the RPC-level gas cap to prevent DoS. +type gasBudget struct { + remaining uint64 +} + +// newGasBudget creates a gas budget with the given cap. +// A cap of 0 is treated as unlimited. +func newGasBudget(cap uint64) *gasBudget { + if cap == 0 { + cap = math.MaxUint64 + } + return &gasBudget{remaining: cap} +} + +// cap returns the given gas value clamped to the remaining budget. +func (b *gasBudget) cap(gas uint64) uint64 { + if gas > b.remaining { + return b.remaining + } + return gas +} + +// consume deducts the given amount from the budget. +// Returns an error if the amount exceeds the remaining budget. +func (b *gasBudget) consume(amount uint64) error { + if amount > b.remaining { + return fmt.Errorf("RPC gas cap exhausted: need %d, remaining %d", amount, b.remaining) + } + b.remaining -= amount + return nil +} + // simulator is a stateful object that simulates a series of blocks. // it is not safe for concurrent use. type simulator struct { @@ -176,7 +219,7 @@ type simulator struct { state *state.StateDB base *types.Header chainConfig *params.ChainConfig - gp *core.GasPool + budget *gasBudget traceTransfers bool validate bool fullTx bool @@ -259,6 +302,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, return nil, nil, nil, err } var ( + gp = new(core.GasPool).AddGas(blockContext.GasLimit) gasUsed, blobGasUsed uint64 txes = make([]*types.Transaction, len(block.Calls)) callResults = make([]simCallResult, len(block.Calls)) @@ -296,7 +340,8 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, if err := ctx.Err(); err != nil { return nil, nil, nil, err } - if err := sim.sanitizeCall(&call, sim.state, header, blockContext, &gasUsed); err != nil { + gasCapped, err := sim.sanitizeCall(&call, sim.state, header, gp) + if err != nil { return nil, nil, nil, err } var ( @@ -309,7 +354,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, sim.state.SetTxContext(txHash, i) // EoA check is always skipped, even in validation mode. msg := call.ToMessage(header.BaseFee, !sim.validate) - result, err := applyMessageWithEVM(ctx, evm, msg, timeout, sim.gp) + result, err := applyMessageWithEVM(ctx, evm, msg, timeout, gp) if err != nil { txErr := txValidationError(err) return nil, nil, nil, txErr @@ -324,8 +369,14 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, gasUsed += result.UsedGas receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, blockContext.Time, tx, gasUsed, root) blobGasUsed += receipts[i].BlobGasUsed + + // Enforce the cross-block gas budget. + if err := sim.budget.consume(result.UsedGas); err != nil { + return nil, nil, nil, err + } + logs := tracer.Logs() - callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas)} + callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas), MaxUsedGas: hexutil.Uint64(result.MaxUsedGas)} if result.Failed() { callRes.Status = hexutil.Uint64(types.ReceiptStatusFailed) if errors.Is(result.Err, vm.ErrExecutionReverted) { @@ -333,7 +384,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, revertErr := newRevertError(result.Revert()) callRes.Error = &callError{Message: revertErr.Error(), Code: errCodeReverted, Data: revertErr.ErrorData().(string)} } else { - callRes.Error = &callError{Message: result.Err.Error(), Code: errCodeVMError} + msg := result.Err.Error() + if gasCapped { + msg += " (gas limit was capped by the RPC server's global gas cap)" + } + callRes.Error = &callError{Message: msg, Code: errCodeVMError} } } else { callRes.Status = hexutil.Uint64(types.ReceiptStatusSuccessful) @@ -395,23 +450,25 @@ func repairLogs(calls []simCallResult, hash common.Hash) { } } -func (sim *simulator) sanitizeCall(call *TransactionArgs, state vm.StateDB, header *types.Header, blockContext vm.BlockContext, gasUsed *uint64) error { +func (sim *simulator) sanitizeCall(call *TransactionArgs, state vm.StateDB, header *types.Header, gp *core.GasPool) (bool, error) { if call.Nonce == nil { nonce := state.GetNonce(call.from()) call.Nonce = (*hexutil.Uint64)(&nonce) } // Let the call run wild unless explicitly specified. + remaining := gp.Gas() if call.Gas == nil { - remaining := blockContext.GasLimit - *gasUsed call.Gas = (*hexutil.Uint64)(&remaining) } - if *gasUsed+uint64(*call.Gas) > blockContext.GasLimit { - return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: %d >= %d", gasUsed, blockContext.GasLimit)} - } - if err := call.CallDefaults(sim.gp.Gas(), header.BaseFee, sim.chainConfig.ChainID); err != nil { - return err + if remaining < uint64(*call.Gas) { + return false, &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: remaining: %d, required: %d", remaining, *call.Gas)} } - return nil + // Clamp to the cross-block gas budget. + gas := sim.budget.cap(uint64(*call.Gas)) + gasCapped := gas < uint64(*call.Gas) + call.Gas = (*hexutil.Uint64)(&gas) + + return gasCapped, call.CallDefaults(0, header.BaseFee, sim.chainConfig.ChainID) } func (sim *simulator) activePrecompiles(base *types.Header) vm.PrecompiledContracts { diff --git a/internal/ethapi/simulate_test.go b/internal/ethapi/simulate_test.go index 8553bf5f9e..fee7c000ab 100644 --- a/internal/ethapi/simulate_test.go +++ b/internal/ethapi/simulate_test.go @@ -84,6 +84,7 @@ func TestSimulateSanitizeBlockOrder(t *testing.T) { sim := &simulator{ base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp}, chainConfig: params.TestChainConfig, // In real usage, sanitizeChain is always called with chainConfig + budget: newGasBudget(0), } res, err := sim.sanitizeChain(tc.blocks) if err != nil { diff --git a/params/version.go b/params/version.go index f56e584e02..1a7e3d3dfb 100644 --- a/params/version.go +++ b/params/version.go @@ -25,7 +25,7 @@ import ( const ( VersionMajor = 2 // Major version component of the current release VersionMinor = 7 // Minor version component of the current release - VersionPatch = 1 // Patch version component of the current release + VersionPatch = 2 // Patch version component of the current release VersionMeta = "" // Version metadata to append to the version string ) diff --git a/tests/bor/bor_test.go b/tests/bor/bor_test.go index a7eac3e37f..ba1b099ab4 100644 --- a/tests/bor/bor_test.go +++ b/tests/bor/bor_test.go @@ -2157,7 +2157,7 @@ func TestInvalidStateSyncInBlockBody(t *testing.T) { // shouldn't be applied and an error should be returned while inserting the block. _, err := chain.InsertChain([]*types.Block{block}, false) require.Error(t, err, "insert chain successed for block with invalid state-sync tx in body") - require.ErrorIs(t, err, core.ErrStateSyncProcessing, "received incorrect error for invalid state-sync tx in block body") + require.ErrorIs(t, err, core.ErrStateSyncMismatch, "received incorrect error for invalid state-sync tx in block body") } // TestDynamicGasLimit_LowBaseFee tests that when base fee is below the target-buffer,