mirror of
https://gitlab.com/pulsechaincom/erigon-pulse.git
synced 2024-12-24 12:37:16 +00:00
961a0070cc
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).
520 lines
16 KiB
Go
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
|
|
}
|