otterscan/cmd/otter/commands/otterscan_api.go

527 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/cmd/rpcdaemon/commands"
"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"
"github.com/ledgerwatch/log/v3"
)
// API_LEVEL Must be incremented every time new additions are made
const API_LEVEL = 8
type TransactionsWithReceipts struct {
Txs []*commands.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 {
*BaseAPIUtils
db kv.RoDB
}
func NewOtterscanAPI(base *BaseAPIUtils, db kv.RoDB) *OtterscanAPIImpl {
return &OtterscanAPIImpl{
BaseAPIUtils: 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, blockHash, _, 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
}
getHeader := func(hash common.Hash, number uint64) *types.Header {
return rawdb.ReadHeader(tx, hash, number)
}
msg, blockCtx, txCtx, ibs, _, err := transactions.ComputeTxEnv(ctx, block, chainConfig, getHeader, ethash.NewFaker(), tx, blockHash, txIndex)
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()
log.Info("got cursor")
callFromCursor, err := dbtx.Cursor(kv.CallFromIndex)
if err != nil {
return nil, err
}
defer callFromCursor.Close()
log.Info("call from cur")
callToCursor, err := dbtx.Cursor(kv.CallToIndex)
if err != nil {
return nil, err
}
defer callToCursor.Close()
log.Info("cur to call")
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([]*commands.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([]*commands.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
}
response, err := ethapi.RPCMarshalBlockDeprecated(b, inclTx, inclTx)
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
}