Skip to content
Merged
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 core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -1741,3 +1741,11 @@ func (s *StateDB) GetEncodedBlockAccessList(block *types.Block) *types.BlockAcce

return &blockAccessList
}

Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

Missing documentation for the GetDirtyAccounts function. Public methods should have documentation explaining their purpose and return values.

Suggested change
// GetDirtyAccounts returns the set of accounts that have pending mutations
// in the current state, as a slice of their addresses.

Copilot uses AI. Check for mistakes.
func (s *StateDB) GetDirtyAccounts() []common.Address {
accounts := make([]common.Address, 0, len(s.mutations))
for account := range s.mutations {
accounts = append(accounts, account)
}
return accounts
}
207 changes: 207 additions & 0 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,213 @@ func (api *BlockChainAPI) EstimateGas(ctx context.Context, args TransactionArgs,
return DoEstimateGas(ctx, api.b, args, bNrOrHash, overrides, blockOverrides, api.b.RPCGasCap())
}

// GetDiffAccounts returns changed accounts in a specific block number.
func (api *BlockChainAPI) GetDiffAccounts(ctx context.Context, blockNr rpc.BlockNumber) ([]common.Address, error) {
if api.b.Chain() == nil {
return nil, errors.New("blockchain does not support get diff accounts")
}

// Replay the block when diff layer not found, it is very slow.
block, err := api.b.BlockByNumber(ctx, blockNr)
if err != nil || block == nil {
return nil, fmt.Errorf("block not found for block number %d", blockNr)
}
_, statedb, err := api.replay(ctx, block, nil)
if err != nil {
return nil, err
}
return statedb.GetDirtyAccounts(), nil
}

Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

Missing documentation for the needToReplay helper function. This is a complex helper function with non-trivial logic that should have documentation explaining its purpose, parameters, and return values.

Suggested change
// needToReplay determines whether a full state replay is required for the given block
// in order to accurately compute the balance changes of the specified accounts.
//
// It inspects the block's transactions and their receipts, aggregating the gas costs and
// transferred values for any account in the provided list, both as senders and recipients.
// If receipts are missing or inconsistent with the block's transactions, or if a sender
// cannot be derived, an error is returned and the helper signals that replay is not
// possible based solely on receipts.
//
// Parameters:
// - ctx: context used when querying receipts or other chain data, allowing cancellation.
// - block: the block whose transactions and receipts are inspected.
// - accounts: the set of accounts for which potential balance changes are evaluated.
//
// Returns:
// - bool: true if a state replay is required to obtain accurate account changes, false if
// replay is not needed or cannot be determined from receipts alone.
// - error: non-nil if receipts are inconsistent or if any unexpected failure occurs while
// processing the block.

Copilot uses AI. Check for mistakes.
func (api *BlockChainAPI) needToReplay(ctx context.Context, block *types.Block, accounts []common.Address) (bool, error) {
receipts, err := api.b.GetReceipts(ctx, block.Hash())
if err != nil || len(receipts) != len(block.Transactions()) {
return false, fmt.Errorf("receipt incorrect for block number (%d): %v", block.NumberU64(), err)
}

accountSet := make(map[common.Address]struct{}, len(accounts))
spendValueMap := make(map[common.Address]*big.Int, len(accounts))
receiveValueMap := make(map[common.Address]*big.Int, len(accounts))

for _, account := range accounts {
accountSet[account] = struct{}{}
spendValueMap[account] = big.NewInt(0)
receiveValueMap[account] = big.NewInt(0)
}

signer := types.MakeSigner(api.b.ChainConfig(), block.Number(), block.Time())
for index, tx := range block.Transactions() {
receipt := receipts[index]
from, err := types.Sender(signer, tx)
if err != nil {
return false, fmt.Errorf("get sender for tx failed: %v", err)
}

if _, exists := accountSet[from]; exists {
gasUsed := new(big.Int).SetUint64(receipt.GasUsed)
gasCost := new(big.Int).Mul(gasUsed, tx.GasPrice())
spendValueMap[from].Add(spendValueMap[from], gasCost)
if receipt.Status == types.ReceiptStatusSuccessful {
spendValueMap[from].Add(spendValueMap[from], tx.Value())
}
}

if tx.To() == nil {
continue
}
if _, exists := accountSet[*tx.To()]; exists && receipt.Status == types.ReceiptStatusSuccessful {
receiveValueMap[*tx.To()].Add(receiveValueMap[*tx.To()], tx.Value())
}
}

parent, err := api.b.BlockByHash(ctx, block.ParentHash())
if err != nil || parent == nil {
return false, fmt.Errorf("block not found for block number %d", block.NumberU64()-1)
}
parentState, err := api.b.Chain().StateAt(parent.Root())
if err != nil {
return false, fmt.Errorf("statedb not found for block number (%d): %v", block.NumberU64()-1, err)
}
currentState, err := api.b.Chain().StateAt(block.Root())
if err != nil {
return false, fmt.Errorf("statedb not found for block number (%d): %v", block.NumberU64(), err)
}
for _, account := range accounts {
parentBalance := parentState.GetBalance(account).ToBig()
currentBalance := currentState.GetBalance(account).ToBig()
theoryDiff := new(big.Int).Sub(receiveValueMap[account], spendValueMap[account])
actualDiff := new(big.Int).Sub(currentBalance, parentBalance)
if theoryDiff.Cmp(actualDiff) != 0 {
// log.Debug("Balance mismatch detected", "addr", account, "theory", theoryDiff, "actual", actualDiff)
return true, nil
}
}

return false, nil
}

type DiffAccountsInTx struct {
TxHash common.Hash
Accounts map[common.Address]*big.Int
}

type DiffAccountsInBlock struct {
Number uint64
BlockHash common.Hash
Comment on lines +1287 to +1293
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

Missing documentation for the DiffAccountsInTx struct fields. Public structs should have documentation for each exported field explaining their purpose and format.

Suggested change
TxHash common.Hash
Accounts map[common.Address]*big.Int
}
type DiffAccountsInBlock struct {
Number uint64
BlockHash common.Hash
// TxHash is the hash of the transaction whose account balance differences are recorded.
TxHash common.Hash
// Accounts maps account addresses to their net balance change (in wei) caused by the transaction.
// A positive value indicates a gain in balance; a negative value indicates a loss.
Accounts map[common.Address]*big.Int
}
type DiffAccountsInBlock struct {
// Number is the number of the block for which account differences are reported.
Number uint64
// BlockHash is the hash of the block corresponding to this diff information.
BlockHash common.Hash
// Transactions contains per-transaction account balance differences within the block.

Copilot uses AI. Check for mistakes.
Comment on lines +1287 to +1293
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

Missing documentation for the DiffAccountsInBlock struct fields. Public structs should have documentation for each exported field explaining their purpose and format.

Suggested change
TxHash common.Hash
Accounts map[common.Address]*big.Int
}
type DiffAccountsInBlock struct {
Number uint64
BlockHash common.Hash
// TxHash is the hash of the transaction whose account balance diffs are reported.
TxHash common.Hash
// Accounts maps each tracked account address to its balance difference caused by the transaction.
Accounts map[common.Address]*big.Int
}
type DiffAccountsInBlock struct {
// Number is the height of the block that was replayed.
Number uint64
// BlockHash is the hash of the block whose transactions were replayed.
BlockHash common.Hash
// Transactions contains the per-transaction account balance diffs within the block.

Copilot uses AI. Check for mistakes.
Transactions []DiffAccountsInTx
}

Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

Missing documentation for the replay helper function. This is a complex function that performs block replay and computes account diffs. It should have documentation explaining its purpose, parameters, and return values.

Suggested change
// replay re-executes the given block on top of its parent state and computes
// per-transaction balance diffs for the specified accounts.
//
// The ctx parameter controls cancellation of the replay operation. The block
// parameter is the block to be replayed, and accounts is the list of account
// addresses for which balance changes should be tracked.
//
// It returns:
// - *DiffAccountsInBlock containing the per-transaction balance deltas for
// the requested accounts within the given block;
// - *state.StateDB representing the post-state after successfully replaying
// the block;
// - error if the parent block or its state cannot be retrieved, or if the
// replay itself fails.

Copilot uses AI. Check for mistakes.
func (api *BlockChainAPI) replay(ctx context.Context, block *types.Block, accounts []common.Address) (*DiffAccountsInBlock, *state.StateDB, error) {
result := &DiffAccountsInBlock{
Number: block.NumberU64(),
BlockHash: block.Hash(),
Transactions: make([]DiffAccountsInTx, 0),
}

parent, err := api.b.BlockByHash(ctx, block.ParentHash())
if err != nil || parent == nil {
return nil, nil, fmt.Errorf("block not found for block number %d", block.NumberU64()-1)
}
statedb, err := api.b.Chain().StateAt(parent.Root())
if err != nil {
return nil, nil, fmt.Errorf("state not found for block number (%d): %v", block.NumberU64()-1, err)
}

accountSet := make(map[common.Address]struct{}, len(accounts))
for _, account := range accounts {
accountSet[account] = struct{}{}
}

// Recompute transactions.
signer := types.MakeSigner(api.b.ChainConfig(), block.Number(), block.Time())
for _, tx := range block.Transactions() {
// Skip data empty tx and to is one of the interested accounts tx.
skip := false
if len(tx.Data()) == 0 {
skip = true
} else if to := tx.To(); to != nil {
if _, exists := accountSet[*to]; exists {
skip = true
}
}

diffTx := DiffAccountsInTx{
TxHash: tx.Hash(),
Accounts: make(map[common.Address]*big.Int, len(accounts)),
}

if !skip {
// Record account balance
for _, account := range accounts {
diffTx.Accounts[account] = statedb.GetBalance(account).ToBig()
}
}

// Apply transaction
msg, _ := core.TransactionToMessage(tx, signer, parent.Header().BaseFee)
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

The error from TransactionToMessage is silently ignored. While other similar code in the codebase also ignores this error, in the context of a replay function that's computing account differences, a failure to convert a transaction could lead to incorrect results. Consider handling this error explicitly.

Suggested change
msg, _ := core.TransactionToMessage(tx, signer, parent.Header().BaseFee)
msg, err := core.TransactionToMessage(tx, signer, parent.Header().BaseFee)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert transaction %#x to message: %v", tx.Hash(), err)
}

Copilot uses AI. Check for mistakes.
context := core.NewEVMBlockContext(block.Header(), api.b.Chain(), nil)
evm := vm.NewEVM(context, statedb, api.b.ChainConfig(), vm.Config{})

if posa, ok := api.b.Engine().(consensus.PoSA); ok {
if isSystem, _ := posa.IsSystemTransaction(tx, block.Header()); isSystem {
balance := statedb.GetBalance(consensus.SystemAddress)
if balance.Cmp(common.U2560) > 0 {
statedb.SetBalance(consensus.SystemAddress, uint256.NewInt(0), tracing.BalanceChangeUnspecified)
statedb.AddBalance(block.Header().Coinbase, balance, tracing.BalanceChangeUnspecified)
}
}
}

if _, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil {
return nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err)
}
statedb.Finalise(evm.ChainConfig().IsEIP158(block.Number()))

if !skip {
// Compute account balance diff.
for _, account := range accounts {
diffTx.Accounts[account] = new(big.Int).Sub(statedb.GetBalance(account).ToBig(), diffTx.Accounts[account])
if diffTx.Accounts[account].Cmp(big.NewInt(0)) == 0 {
delete(diffTx.Accounts, account)
}
}

if len(diffTx.Accounts) != 0 {
result.Transactions = append(result.Transactions, diffTx)
}
}
}

return result, statedb, nil
}

// GetDiffAccountsWithScope returns detailed changes of some interested accounts in a specific block number.
func (api *BlockChainAPI) GetDiffAccountsWithScope(ctx context.Context, blockNr rpc.BlockNumber, accounts []common.Address) (*DiffAccountsInBlock, error) {
if api.b.Chain() == nil {
return nil, errors.New("blockchain not support get diff accounts")
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

Inconsistent error message format. This error uses errors.New() while the similar error on line 1204 uses fmt.Errorf(). For consistency, both should use the same approach, preferably fmt.Errorf() to match the codebase pattern.

Suggested change
return nil, errors.New("blockchain not support get diff accounts")
return nil, fmt.Errorf("blockchain not support get diff accounts")

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

The error message is grammatically incorrect. It should read "blockchain does not support get diff accounts" instead of "blockchain not support get diff accounts".

Suggested change
return nil, errors.New("blockchain not support get diff accounts")
return nil, errors.New("blockchain does not support get diff accounts")

Copilot uses AI. Check for mistakes.
}

block, err := api.b.BlockByNumber(ctx, blockNr)
if err != nil || block == nil {
return nil, fmt.Errorf("block not found for block number %d", blockNr)
}

needReplay, err := api.needToReplay(ctx, block, accounts)
if err != nil {
return nil, err
}
if !needReplay {
return &DiffAccountsInBlock{
Number: uint64(blockNr),
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

Type mismatch in Number field assignment. The Number field is of type uint64, but blockNr is of type rpc.BlockNumber (which is int64). This should use block.NumberU64() instead of uint64(blockNr) to ensure consistency and correctness, especially when blockNr might represent special block numbers like LatestBlockNumber (-1).

Suggested change
Number: uint64(blockNr),
Number: block.NumberU64(),

Copilot uses AI. Check for mistakes.
BlockHash: block.Hash(),
Transactions: make([]DiffAccountsInTx, 0),
}, nil
}

result, _, err := api.replay(ctx, block, accounts)
return result, err
}

// RPCMarshalHeader converts the given header to the RPC output .
func RPCMarshalHeader(head *types.Header) map[string]interface{} {
result := map[string]interface{}{
Expand Down