erigon-pulse/cmd/rpcdaemon/commands/otterscan_api.go
Nicolas Gotchac 961a0070cc
Fix trace error in Polygon | Pass Engin to the Base API (#6131)
So there is an issue with tracing certain blocks/transactions on
Polygon, for example:
```
> '{"method": "trace_transaction","params":["0xb198d93f640343a98f90d93aa2b74b4fc5c64f3a649f1608d2bfd1004f9dee0e"],"id":1,"jsonrpc":"2.0"}'
```
gives the error `first run for txIndex 1 error: insufficient funds for
gas * price + value: address 0x10AD27A96CDBffC90ab3b83bF695911426A69f5E
have 16927727762862809 want 17594166808296934`

The reason is that this transaction is from the author of the block,
which doesn't have enough ETH to pay for the gas fee + tx value if he's
not the block author receiving transactions fees.

The issue is that currently the APIs are using `ethash.NewFaker()`
Engine for running traces, etc. which doesn't know how to get the author
for a specific block (which is consensus dependant); as it was noting in
several TODO comments.

The fix is to pass the Engine to the BaseAPI, which can then be used to
create the right Block Context. I chose to split the current Engine
interface in 2, with Reader and Writer, so that the BaseAPI only
receives the Reader one, which might be safer (even though it's only
used for getting the block Author).
2022-12-04 12:17:39 +07:00

520 lines
16 KiB
Go

package commands
import (
"context"
"errors"
"fmt"
"math/big"
"sync"
"github.com/holiman/uint256"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/common/hexutil"
"github.com/ledgerwatch/erigon/consensus/ethash"
"github.com/ledgerwatch/erigon/core"
"github.com/ledgerwatch/erigon/core/rawdb"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/core/vm"
"github.com/ledgerwatch/erigon/params"
"github.com/ledgerwatch/erigon/rpc"
"github.com/ledgerwatch/erigon/turbo/adapter/ethapi"
"github.com/ledgerwatch/erigon/turbo/rpchelper"
"github.com/ledgerwatch/erigon/turbo/transactions"
)
// API_LEVEL Must be incremented every time new additions are made
const API_LEVEL = 8
type TransactionsWithReceipts struct {
Txs []*RPCTransaction `json:"txs"`
Receipts []map[string]interface{} `json:"receipts"`
FirstPage bool `json:"firstPage"`
LastPage bool `json:"lastPage"`
}
type OtterscanAPI interface {
GetApiLevel() uint8
GetInternalOperations(ctx context.Context, hash common.Hash) ([]*InternalOperation, error)
SearchTransactionsBefore(ctx context.Context, addr common.Address, blockNum uint64, pageSize uint16) (*TransactionsWithReceipts, error)
SearchTransactionsAfter(ctx context.Context, addr common.Address, blockNum uint64, pageSize uint16) (*TransactionsWithReceipts, error)
GetBlockDetails(ctx context.Context, number rpc.BlockNumber) (map[string]interface{}, error)
GetBlockDetailsByHash(ctx context.Context, hash common.Hash) (map[string]interface{}, error)
GetBlockTransactions(ctx context.Context, number rpc.BlockNumber, pageNumber uint8, pageSize uint8) (map[string]interface{}, error)
HasCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (bool, error)
TraceTransaction(ctx context.Context, hash common.Hash) ([]*TraceEntry, error)
GetTransactionError(ctx context.Context, hash common.Hash) (hexutil.Bytes, error)
GetTransactionBySenderAndNonce(ctx context.Context, addr common.Address, nonce uint64) (*common.Hash, error)
GetContractCreator(ctx context.Context, addr common.Address) (*ContractCreatorData, error)
}
type OtterscanAPIImpl struct {
*BaseAPI
db kv.RoDB
}
func NewOtterscanAPI(base *BaseAPI, db kv.RoDB) *OtterscanAPIImpl {
return &OtterscanAPIImpl{
BaseAPI: base,
db: db,
}
}
func (api *OtterscanAPIImpl) GetApiLevel() uint8 {
return API_LEVEL
}
// TODO: dedup from eth_txs.go#GetTransactionByHash
func (api *OtterscanAPIImpl) getTransactionByHash(ctx context.Context, tx kv.Tx, hash common.Hash) (types.Transaction, *types.Block, common.Hash, uint64, uint64, error) {
// https://infura.io/docs/ethereum/json-rpc/eth-getTransactionByHash
blockNum, ok, err := api.txnLookup(ctx, tx, hash)
if err != nil {
return nil, nil, common.Hash{}, 0, 0, err
}
if !ok {
return nil, nil, common.Hash{}, 0, 0, nil
}
block, err := api.blockByNumberWithSenders(tx, blockNum)
if err != nil {
return nil, nil, common.Hash{}, 0, 0, err
}
if block == nil {
return nil, nil, common.Hash{}, 0, 0, nil
}
blockHash := block.Hash()
var txnIndex uint64
var txn types.Transaction
for i, transaction := range block.Transactions() {
if transaction.Hash() == hash {
txn = transaction
txnIndex = uint64(i)
break
}
}
// Add GasPrice for the DynamicFeeTransaction
// var baseFee *big.Int
// if chainConfig.IsLondon(blockNum) && blockHash != (common.Hash{}) {
// baseFee = block.BaseFee()
// }
// if no transaction was found then we return nil
if txn == nil {
return nil, nil, common.Hash{}, 0, 0, nil
}
return txn, block, blockHash, blockNum, txnIndex, nil
}
func (api *OtterscanAPIImpl) runTracer(ctx context.Context, tx kv.Tx, hash common.Hash, tracer vm.Tracer) (*core.ExecutionResult, error) {
txn, block, _, _, txIndex, err := api.getTransactionByHash(ctx, tx, hash)
if err != nil {
return nil, err
}
if txn == nil {
return nil, fmt.Errorf("transaction %#x not found", hash)
}
chainConfig, err := api.chainConfig(tx)
if err != nil {
return nil, err
}
engine := api.engine()
msg, blockCtx, txCtx, ibs, _, err := transactions.ComputeTxEnv(ctx, engine, block, chainConfig, api._blockReader, tx, txIndex, api._agg, api.historyV3(tx))
if err != nil {
return nil, err
}
var vmConfig vm.Config
if tracer == nil {
vmConfig = vm.Config{}
} else {
vmConfig = vm.Config{Debug: true, Tracer: tracer}
}
vmenv := vm.NewEVM(blockCtx, txCtx, ibs, chainConfig, vmConfig)
result, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), true, false /* gasBailout */)
if err != nil {
return nil, fmt.Errorf("tracing failed: %v", err)
}
return result, nil
}
func (api *OtterscanAPIImpl) GetInternalOperations(ctx context.Context, hash common.Hash) ([]*InternalOperation, error) {
tx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
tracer := NewOperationsTracer(ctx)
if _, err := api.runTracer(ctx, tx, hash, tracer); err != nil {
return nil, err
}
return tracer.Results, nil
}
// Search transactions that touch a certain address.
//
// It searches back a certain block (excluding); the results are sorted descending.
//
// The pageSize indicates how many txs may be returned. If there are less txs than pageSize,
// they are just returned. But it may return a little more than pageSize if there are more txs
// than the necessary to fill pageSize in the last found block, i.e., let's say you want pageSize == 25,
// you already found 24 txs, the next block contains 4 matches, then this function will return 28 txs.
func (api *OtterscanAPIImpl) SearchTransactionsBefore(ctx context.Context, addr common.Address, blockNum uint64, pageSize uint16) (*TransactionsWithReceipts, error) {
dbtx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer dbtx.Rollback()
callFromCursor, err := dbtx.Cursor(kv.CallFromIndex)
if err != nil {
return nil, err
}
defer callFromCursor.Close()
callToCursor, err := dbtx.Cursor(kv.CallToIndex)
if err != nil {
return nil, err
}
defer callToCursor.Close()
chainConfig, err := api.chainConfig(dbtx)
if err != nil {
return nil, err
}
isFirstPage := false
if blockNum == 0 {
isFirstPage = true
} else {
// Internal search code considers blockNum [including], so adjust the value
blockNum--
}
// Initialize search cursors at the first shard >= desired block number
callFromProvider := NewCallCursorBackwardBlockProvider(callFromCursor, addr, blockNum)
callToProvider := NewCallCursorBackwardBlockProvider(callToCursor, addr, blockNum)
callFromToProvider := newCallFromToBlockProvider(false, callFromProvider, callToProvider)
txs := make([]*RPCTransaction, 0, pageSize)
receipts := make([]map[string]interface{}, 0, pageSize)
resultCount := uint16(0)
hasMore := true
for {
if resultCount >= pageSize || !hasMore {
break
}
var results []*TransactionsWithReceipts
results, hasMore, err = api.traceBlocks(ctx, addr, chainConfig, pageSize, resultCount, callFromToProvider)
if err != nil {
return nil, err
}
for _, r := range results {
if r == nil {
return nil, errors.New("internal error during search tracing")
}
for i := len(r.Txs) - 1; i >= 0; i-- {
txs = append(txs, r.Txs[i])
}
for i := len(r.Receipts) - 1; i >= 0; i-- {
receipts = append(receipts, r.Receipts[i])
}
resultCount += uint16(len(r.Txs))
if resultCount >= pageSize {
break
}
}
}
return &TransactionsWithReceipts{txs, receipts, isFirstPage, !hasMore}, nil
}
// Search transactions that touch a certain address.
//
// It searches forward a certain block (excluding); the results are sorted descending.
//
// The pageSize indicates how many txs may be returned. If there are less txs than pageSize,
// they are just returned. But it may return a little more than pageSize if there are more txs
// than the necessary to fill pageSize in the last found block, i.e., let's say you want pageSize == 25,
// you already found 24 txs, the next block contains 4 matches, then this function will return 28 txs.
func (api *OtterscanAPIImpl) SearchTransactionsAfter(ctx context.Context, addr common.Address, blockNum uint64, pageSize uint16) (*TransactionsWithReceipts, error) {
dbtx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer dbtx.Rollback()
callFromCursor, err := dbtx.Cursor(kv.CallFromIndex)
if err != nil {
return nil, err
}
defer callFromCursor.Close()
callToCursor, err := dbtx.Cursor(kv.CallToIndex)
if err != nil {
return nil, err
}
defer callToCursor.Close()
chainConfig, err := api.chainConfig(dbtx)
if err != nil {
return nil, err
}
isLastPage := false
if blockNum == 0 {
isLastPage = true
} else {
// Internal search code considers blockNum [including], so adjust the value
blockNum++
}
// Initialize search cursors at the first shard >= desired block number
callFromProvider := NewCallCursorForwardBlockProvider(callFromCursor, addr, blockNum)
callToProvider := NewCallCursorForwardBlockProvider(callToCursor, addr, blockNum)
callFromToProvider := newCallFromToBlockProvider(true, callFromProvider, callToProvider)
txs := make([]*RPCTransaction, 0, pageSize)
receipts := make([]map[string]interface{}, 0, pageSize)
resultCount := uint16(0)
hasMore := true
for {
if resultCount >= pageSize || !hasMore {
break
}
var results []*TransactionsWithReceipts
results, hasMore, err = api.traceBlocks(ctx, addr, chainConfig, pageSize, resultCount, callFromToProvider)
if err != nil {
return nil, err
}
for _, r := range results {
if r == nil {
return nil, errors.New("internal error during search tracing")
}
txs = append(txs, r.Txs...)
receipts = append(receipts, r.Receipts...)
resultCount += uint16(len(r.Txs))
if resultCount >= pageSize {
break
}
}
}
// Reverse results
lentxs := len(txs)
for i := 0; i < lentxs/2; i++ {
txs[i], txs[lentxs-1-i] = txs[lentxs-1-i], txs[i]
receipts[i], receipts[lentxs-1-i] = receipts[lentxs-1-i], receipts[i]
}
return &TransactionsWithReceipts{txs, receipts, !hasMore, isLastPage}, nil
}
func (api *OtterscanAPIImpl) traceBlocks(ctx context.Context, addr common.Address, chainConfig *params.ChainConfig, pageSize, resultCount uint16, callFromToProvider BlockProvider) ([]*TransactionsWithReceipts, bool, error) {
var wg sync.WaitGroup
// Estimate the common case of user address having at most 1 interaction/block and
// trace N := remaining page matches as number of blocks to trace concurrently.
// TODO: this is not optimimal for big contract addresses; implement some better heuristics.
estBlocksToTrace := pageSize - resultCount
results := make([]*TransactionsWithReceipts, estBlocksToTrace)
totalBlocksTraced := 0
hasMore := true
for i := 0; i < int(estBlocksToTrace); i++ {
var nextBlock uint64
var err error
nextBlock, hasMore, err = callFromToProvider()
if err != nil {
return nil, false, err
}
// TODO: nextBlock == 0 seems redundant with hasMore == false
if !hasMore && nextBlock == 0 {
break
}
wg.Add(1)
totalBlocksTraced++
go api.searchTraceBlock(ctx, &wg, addr, chainConfig, i, nextBlock, results)
}
wg.Wait()
return results[:totalBlocksTraced], hasMore, nil
}
func (api *OtterscanAPIImpl) delegateGetBlockByNumber(tx kv.Tx, b *types.Block, number rpc.BlockNumber, inclTx bool) (map[string]interface{}, error) {
td, err := rawdb.ReadTd(tx, b.Hash(), b.NumberU64())
if err != nil {
return nil, err
}
additionalFields := make(map[string]interface{})
response, err := ethapi.RPCMarshalBlock(b, inclTx, inclTx, additionalFields)
if !inclTx {
delete(response, "transactions") // workaround for https://github.com/ledgerwatch/erigon/issues/4989#issuecomment-1218415666
}
response["totalDifficulty"] = (*hexutil.Big)(td)
response["transactionCount"] = b.Transactions().Len()
if err == nil && number == rpc.PendingBlockNumber {
// Pending blocks need to nil out a few fields
for _, field := range []string{"hash", "nonce", "miner"} {
response[field] = nil
}
}
// Explicitly drop unwanted fields
response["logsBloom"] = nil
return response, err
}
// TODO: temporary workaround due to API breakage from watch_the_burn
type internalIssuance struct {
BlockReward string `json:"blockReward,omitempty"`
UncleReward string `json:"uncleReward,omitempty"`
Issuance string `json:"issuance,omitempty"`
}
func (api *OtterscanAPIImpl) delegateIssuance(tx kv.Tx, block *types.Block, chainConfig *params.ChainConfig) (internalIssuance, error) {
if chainConfig.Ethash == nil {
// Clique for example has no issuance
return internalIssuance{}, nil
}
minerReward, uncleRewards := ethash.AccumulateRewards(chainConfig, block.Header(), block.Uncles())
issuance := minerReward
for _, r := range uncleRewards {
p := r // avoids warning?
issuance.Add(&issuance, &p)
}
var ret internalIssuance
ret.BlockReward = hexutil.EncodeBig(minerReward.ToBig())
ret.Issuance = hexutil.EncodeBig(issuance.ToBig())
issuance.Sub(&issuance, &minerReward)
ret.UncleReward = hexutil.EncodeBig(issuance.ToBig())
return ret, nil
}
func (api *OtterscanAPIImpl) delegateBlockFees(ctx context.Context, tx kv.Tx, block *types.Block, senders []common.Address, chainConfig *params.ChainConfig) (uint64, error) {
receipts, err := api.getReceipts(ctx, tx, chainConfig, block, senders)
if err != nil {
return 0, fmt.Errorf("getReceipts error: %v", err)
}
fees := uint64(0)
for _, receipt := range receipts {
txn := block.Transactions()[receipt.TransactionIndex]
effectiveGasPrice := uint64(0)
if !chainConfig.IsLondon(block.NumberU64()) {
effectiveGasPrice = txn.GetPrice().Uint64()
} else {
baseFee, _ := uint256.FromBig(block.BaseFee())
gasPrice := new(big.Int).Add(block.BaseFee(), txn.GetEffectiveGasTip(baseFee).ToBig())
effectiveGasPrice = gasPrice.Uint64()
}
fees += effectiveGasPrice * receipt.GasUsed
}
return fees, nil
}
func (api *OtterscanAPIImpl) getBlockWithSenders(ctx context.Context, number rpc.BlockNumber, tx kv.Tx) (*types.Block, []common.Address, error) {
if number == rpc.PendingBlockNumber {
return api.pendingBlock(), nil, nil
}
n, hash, _, err := rpchelper.GetBlockNumber(rpc.BlockNumberOrHashWithNumber(number), tx, api.filters)
if err != nil {
return nil, nil, err
}
block, senders, err := api._blockReader.BlockWithSenders(ctx, tx, hash, n)
return block, senders, err
}
func (api *OtterscanAPIImpl) GetBlockTransactions(ctx context.Context, number rpc.BlockNumber, pageNumber uint8, pageSize uint8) (map[string]interface{}, error) {
tx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
b, senders, err := api.getBlockWithSenders(ctx, number, tx)
if err != nil {
return nil, err
}
if b == nil {
return nil, nil
}
chainConfig, err := api.chainConfig(tx)
if err != nil {
return nil, err
}
getBlockRes, err := api.delegateGetBlockByNumber(tx, b, number, true)
if err != nil {
return nil, err
}
// Receipts
receipts, err := api.getReceipts(ctx, tx, chainConfig, b, senders)
if err != nil {
return nil, fmt.Errorf("getReceipts error: %v", err)
}
result := make([]map[string]interface{}, 0, len(receipts))
for _, receipt := range receipts {
txn := b.Transactions()[receipt.TransactionIndex]
marshalledRcpt := marshalReceipt(receipt, txn, chainConfig, b, txn.Hash(), true)
marshalledRcpt["logs"] = nil
marshalledRcpt["logsBloom"] = nil
result = append(result, marshalledRcpt)
}
// Pruned block attrs
prunedBlock := map[string]interface{}{}
for _, k := range []string{"timestamp", "miner", "baseFeePerGas"} {
prunedBlock[k] = getBlockRes[k]
}
// Crop tx input to 4bytes
var txs = getBlockRes["transactions"].([]interface{})
for _, rawTx := range txs {
rpcTx := rawTx.(*ethapi.RPCTransaction)
if len(rpcTx.Input) >= 4 {
rpcTx.Input = rpcTx.Input[:4]
}
}
// Crop page
pageEnd := b.Transactions().Len() - int(pageNumber)*int(pageSize)
pageStart := pageEnd - int(pageSize)
if pageEnd < 0 {
pageEnd = 0
}
if pageStart < 0 {
pageStart = 0
}
response := map[string]interface{}{}
getBlockRes["transactions"] = getBlockRes["transactions"].([]interface{})[pageStart:pageEnd]
response["fullblock"] = getBlockRes
response["receipts"] = result[pageStart:pageEnd]
return response, nil
}