diff --git a/core/state/statedb.go b/core/state/statedb.go index 40e5efd61d..c14e89e434 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1741,3 +1741,11 @@ func (s *StateDB) GetEncodedBlockAccessList(block *types.Block) *types.BlockAcce return &blockAccessList } + +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 +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index d54f34a839..ce41991628 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -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 +} + +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 + Transactions []DiffAccountsInTx +} + +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) + 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") + } + + 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), + 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{}{