diff --git a/cmd/rpcdaemon/commands/daemon.go b/cmd/rpcdaemon/commands/daemon.go index 698b758def3..fe0f8045f9f 100644 --- a/cmd/rpcdaemon/commands/daemon.go +++ b/cmd/rpcdaemon/commands/daemon.go @@ -28,6 +28,7 @@ func APIList(db kv.RoDB, borDb kv.RoDB, eth rpchelper.ApiBackend, txPool txpool. adminImpl := NewAdminAPI(eth) parityImpl := NewParityAPIImpl(db) borImpl := NewBorAPI(base, db, borDb) // bor (consensus) specific + otsImpl := NewOtterscanAPI(base, db) for _, enabledAPI := range cfg.API { switch enabledAPI { @@ -108,6 +109,13 @@ func APIList(db kv.RoDB, borDb kv.RoDB, eth rpchelper.ApiBackend, txPool txpool. Service: ParityAPI(parityImpl), Version: "1.0", }) + case "ots": + list = append(list, rpc.API{ + Namespace: "ots", + Public: true, + Service: OtterscanAPI(otsImpl), + Version: "1.0", + }) } } diff --git a/cmd/rpcdaemon/commands/otterscan_api.go b/cmd/rpcdaemon/commands/otterscan_api.go new file mode 100644 index 00000000000..f9244cbf5ad --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_api.go @@ -0,0 +1,518 @@ +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 + } + + msg, blockCtx, txCtx, ibs, _, err := transactions.ComputeTxEnv(ctx, 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 +} diff --git a/cmd/rpcdaemon/commands/otterscan_block_details.go b/cmd/rpcdaemon/commands/otterscan_block_details.go new file mode 100644 index 00000000000..57750817969 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_block_details.go @@ -0,0 +1,97 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/common/hexutil" + "github.com/ledgerwatch/erigon/core/rawdb" + "github.com/ledgerwatch/erigon/rpc" +) + +func (api *OtterscanAPIImpl) GetBlockDetails(ctx context.Context, number rpc.BlockNumber) (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, false) + if err != nil { + return nil, err + } + getIssuanceRes, err := api.delegateIssuance(tx, b, chainConfig) + if err != nil { + return nil, err + } + feesRes, err := api.delegateBlockFees(ctx, tx, b, senders, chainConfig) + if err != nil { + return nil, err + } + + response := map[string]interface{}{} + response["block"] = getBlockRes + response["issuance"] = getIssuanceRes + response["totalFees"] = hexutil.Uint64(feesRes) + return response, nil +} + +// TODO: remove duplication with GetBlockDetails +func (api *OtterscanAPIImpl) GetBlockDetailsByHash(ctx context.Context, hash common.Hash) (map[string]interface{}, error) { + tx, err := api.db.BeginRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // b, senders, err := rawdb.ReadBlockByHashWithSenders(tx, hash) + blockNumber := rawdb.ReadHeaderNumber(tx, hash) + if blockNumber == nil { + return nil, fmt.Errorf("couldn't find block number for hash %v", hash.Bytes()) + } + b, senders, err := api._blockReader.BlockWithSenders(ctx, tx, hash, *blockNumber) + 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, rpc.BlockNumber(b.Number().Int64()), false) + if err != nil { + return nil, err + } + getIssuanceRes, err := api.delegateIssuance(tx, b, chainConfig) + if err != nil { + return nil, err + } + feesRes, err := api.delegateBlockFees(ctx, tx, b, senders, chainConfig) + if err != nil { + return nil, err + } + + response := map[string]interface{}{} + response["block"] = getBlockRes + response["issuance"] = getIssuanceRes + response["totalFees"] = hexutil.Uint64(feesRes) + return response, nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_contract_creator.go b/cmd/rpcdaemon/commands/otterscan_contract_creator.go new file mode 100644 index 00000000000..2bef2cc040d --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_contract_creator.go @@ -0,0 +1,169 @@ +package commands + +import ( + "bytes" + "context" + "fmt" + "sort" + + "github.com/RoaringBitmap/roaring/roaring64" + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/common/changeset" + "github.com/ledgerwatch/erigon/core/state" + "github.com/ledgerwatch/erigon/core/types/accounts" + "github.com/ledgerwatch/log/v3" +) + +type ContractCreatorData struct { + Tx common.Hash `json:"hash"` + Creator common.Address `json:"creator"` +} + +func (api *OtterscanAPIImpl) GetContractCreator(ctx context.Context, addr common.Address) (*ContractCreatorData, error) { + tx, err := api.db.BeginRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + reader := state.NewPlainStateReader(tx) + plainStateAcc, err := reader.ReadAccountData(addr) + if err != nil { + return nil, err + } + + // No state == non existent + if plainStateAcc == nil { + return nil, nil + } + + // EOA? + if plainStateAcc.IsEmptyCodeHash() { + return nil, nil + } + + // Contract; search for creation tx; navigate forward on AccountsHistory/ChangeSets + // + // We search shards in forward order on purpose because popular contracts may have + // dozens of states changes due to ETH deposits/withdraw after contract creation, + // so it is optimal to search from the beginning even if the contract has multiple + // incarnations. + accHistory, err := tx.Cursor(kv.AccountsHistory) + if err != nil { + return nil, err + } + defer accHistory.Close() + + accCS, err := tx.CursorDupSort(kv.AccountChangeSet) + if err != nil { + return nil, err + } + defer accCS.Close() + + // Locate shard that contains the block where incarnation changed + acs := changeset.Mapper[kv.AccountChangeSet] + k, v, err := accHistory.Seek(acs.IndexChunkKey(addr.Bytes(), 0)) + if err != nil { + return nil, err + } + if !bytes.HasPrefix(k, addr.Bytes()) { + log.Error("Couldn't find any shard for account history", "addr", addr) + return nil, fmt.Errorf("could't find any shard for account history addr=%v", addr) + } + + var acc accounts.Account + bm := roaring64.NewBitmap() + prevShardMaxBl := uint64(0) + for { + _, err := bm.ReadFrom(bytes.NewReader(v)) + if err != nil { + return nil, err + } + + // Shortcut precheck + st, err := acs.Find(accCS, bm.Maximum(), addr.Bytes()) + if err != nil { + return nil, err + } + if st == nil { + log.Error("Unexpected error, couldn't find changeset", "block", bm.Maximum(), "addr", addr) + return nil, fmt.Errorf("unexpected error, couldn't find changeset block=%v addr=%v", bm.Maximum(), addr) + } + + // Found the shard where the incarnation change happens; ignore all + // next shards + if err := acc.DecodeForStorage(st); err != nil { + return nil, err + } + if acc.Incarnation >= plainStateAcc.Incarnation { + break + } + prevShardMaxBl = bm.Maximum() + + k, v, err = accHistory.Next() + if err != nil { + return nil, err + } + + // No more shards; it means the max bl from previous shard + // contains the incarnation change + if !bytes.HasPrefix(k, addr.Bytes()) { + break + } + } + + // Binary search block number inside shard; get first block where desired + // incarnation appears + blocks := bm.ToArray() + var searchErr error + r := sort.Search(len(blocks), func(i int) bool { + bl := blocks[i] + st, err := acs.Find(accCS, bl, addr.Bytes()) + if err != nil { + searchErr = err + return false + } + if st == nil { + log.Error("Unexpected error, couldn't find changeset", "block", bl, "addr", addr) + return false + } + + if err := acc.DecodeForStorage(st); err != nil { + searchErr = err + return false + } + if acc.Incarnation < plainStateAcc.Incarnation { + return false + } + return true + }) + + if searchErr != nil { + return nil, searchErr + } + + // The sort.Search function finds the first block where the incarnation has + // changed to the desired one, so we get the previous block from the bitmap; + // however if the found block is already the first one from the bitmap, it means + // the block we want is the max block from the previous shard. + blockFound := prevShardMaxBl + if r > 0 { + blockFound = blocks[r-1] + } + + // Trace block, find tx and contract creator + chainConfig, err := api.chainConfig(tx) + if err != nil { + return nil, err + } + tracer := NewCreateTracer(ctx, addr) + if err := api.genericTracer(tx, ctx, blockFound, chainConfig, tracer); err != nil { + return nil, err + } + + return &ContractCreatorData{ + Tx: tracer.Tx.Hash(), + Creator: tracer.Creator, + }, nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_default_tracer.go b/cmd/rpcdaemon/commands/otterscan_default_tracer.go new file mode 100644 index 00000000000..fa023183558 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_default_tracer.go @@ -0,0 +1,38 @@ +package commands + +import ( + "math/big" + "time" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/core/vm" +) + +// Helper implementation of vm.Tracer; since the interface is big and most +// custom tracers implement just a few of the methods, this is a base struct +// to avoid lots of empty boilerplate code +type DefaultTracer struct { +} + +func (t *DefaultTracer) CaptureStart(env *vm.EVM, depth int, from common.Address, to common.Address, precompile bool, create bool, calltype vm.CallType, input []byte, gas uint64, value *big.Int, code []byte) { +} + +func (t *DefaultTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +} + +func (t *DefaultTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +} + +func (t *DefaultTracer) CaptureEnd(depth int, output []byte, startGas, endGas uint64, d time.Duration, err error) { +} + +func (t *DefaultTracer) CaptureSelfDestruct(from common.Address, to common.Address, value *big.Int) { +} + +func (t *DefaultTracer) CaptureAccountRead(account common.Address) error { + return nil +} + +func (t *DefaultTracer) CaptureAccountWrite(account common.Address) error { + return nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_generic_tracer.go b/cmd/rpcdaemon/commands/otterscan_generic_tracer.go new file mode 100644 index 00000000000..ea5a07ae56f --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_generic_tracer.go @@ -0,0 +1,74 @@ +package commands + +import ( + "context" + + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/consensus/ethash" + "github.com/ledgerwatch/erigon/core" + "github.com/ledgerwatch/erigon/core/state" + "github.com/ledgerwatch/erigon/core/types" + "github.com/ledgerwatch/erigon/core/vm" + "github.com/ledgerwatch/erigon/params" + "github.com/ledgerwatch/erigon/turbo/shards" + "github.com/ledgerwatch/log/v3" +) + +type GenericTracer interface { + vm.Tracer + SetTransaction(tx types.Transaction) + Found() bool +} + +func (api *OtterscanAPIImpl) genericTracer(dbtx kv.Tx, ctx context.Context, blockNum uint64, chainConfig *params.ChainConfig, tracer GenericTracer) error { + block, err := api.blockByNumberWithSenders(dbtx, blockNum) + if err != nil { + return err + } + if block == nil { + return nil + } + + reader := state.NewPlainState(dbtx, blockNum) + stateCache := shards.NewStateCache(32, 0 /* no limit */) + cachedReader := state.NewCachedReader(reader, stateCache) + noop := state.NewNoopWriter() + cachedWriter := state.NewCachedWriter(noop, stateCache) + + ibs := state.New(cachedReader) + signer := types.MakeSigner(chainConfig, blockNum) + + getHeader := func(hash common.Hash, number uint64) *types.Header { + h, e := api._blockReader.Header(ctx, dbtx, hash, number) + if e != nil { + log.Error("getHeader error", "number", number, "hash", hash, "err", e) + } + return h + } + engine := ethash.NewFaker() + + header := block.Header() + rules := chainConfig.Rules(block.NumberU64()) + for idx, tx := range block.Transactions() { + ibs.Prepare(tx.Hash(), block.Hash(), idx) + + msg, _ := tx.AsMessage(*signer, header.BaseFee, rules) + + BlockContext := core.NewEVMBlockContext(header, core.GetHashFn(header, getHeader), engine, nil) + TxContext := core.NewEVMTxContext(msg) + + vmenv := vm.NewEVM(BlockContext, TxContext, ibs, chainConfig, vm.Config{Debug: true, Tracer: tracer}) + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.GetGas()), true /* refunds */, false /* gasBailout */); err != nil { + return err + } + _ = ibs.FinalizeTx(vmenv.ChainConfig().Rules(block.NumberU64()), cachedWriter) + + if tracer.Found() { + tracer.SetTransaction(tx) + return nil + } + } + + return nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_has_code.go b/cmd/rpcdaemon/commands/otterscan_has_code.go new file mode 100644 index 00000000000..d5b267ca8fb --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_has_code.go @@ -0,0 +1,31 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/rpc" + "github.com/ledgerwatch/erigon/turbo/adapter" + "github.com/ledgerwatch/erigon/turbo/rpchelper" +) + +func (api *OtterscanAPIImpl) HasCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (bool, error) { + tx, err := api.db.BeginRo(ctx) + if err != nil { + return false, fmt.Errorf("hasCode cannot open tx: %w", err) + } + defer tx.Rollback() + + blockNumber, _, _, err := rpchelper.GetBlockNumber(blockNrOrHash, tx, api.filters) + if err != nil { + return false, err + } + + reader := adapter.NewStateReader(tx, blockNumber) + acc, err := reader.ReadAccountData(address) + if acc == nil || err != nil { + return false, err + } + return !acc.IsEmptyCodeHash(), nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_backward.go b/cmd/rpcdaemon/commands/otterscan_search_backward.go new file mode 100644 index 00000000000..4d8193e0119 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_backward.go @@ -0,0 +1,128 @@ +package commands + +import ( + "bytes" + + "github.com/RoaringBitmap/roaring/roaring64" + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/common" +) + +// Given a ChunkLocator, moves back over the chunks and inside each chunk, moves +// backwards over the block numbers. +func NewBackwardBlockProvider(chunkLocator ChunkLocator, maxBlock uint64) BlockProvider { + // block == 0 means no max + if maxBlock == 0 { + maxBlock = MaxBlockNum + } + var iter roaring64.IntIterable64 + var chunkProvider ChunkProvider + isFirst := true + finished := false + + return func() (uint64, bool, error) { + if finished { + return 0, false, nil + } + + if isFirst { + isFirst = false + + // Try to get first chunk + var ok bool + var err error + chunkProvider, ok, err = chunkLocator(maxBlock) + if err != nil { + finished = true + return 0, false, err + } + if !ok { + finished = true + return 0, false, nil + } + if chunkProvider == nil { + finished = true + return 0, false, nil + } + + // Has at least the first chunk; initialize the iterator + chunk, ok, err := chunkProvider() + if err != nil { + finished = true + return 0, false, err + } + if !ok { + finished = true + return 0, false, nil + } + + bm := roaring64.NewBitmap() + if _, err := bm.ReadFrom(bytes.NewReader(chunk)); err != nil { + finished = true + return 0, false, err + } + + // It can happen that on the first chunk we'll get a chunk that contains + // the last block <= maxBlock in the middle of the chunk/bitmap, so we + // remove all blocks after it (since there is no AdvanceIfNeeded() in + // IntIterable64) + if maxBlock != MaxBlockNum { + bm.RemoveRange(maxBlock+1, MaxBlockNum) + } + iter = bm.ReverseIterator() + + // This means it is the last chunk and the min block is > the last one + if !iter.HasNext() { + chunk, ok, err := chunkProvider() + if err != nil { + finished = true + return 0, false, err + } + if !ok { + finished = true + return 0, false, nil + } + + bm := roaring64.NewBitmap() + if _, err := bm.ReadFrom(bytes.NewReader(chunk)); err != nil { + finished = true + return 0, false, err + } + + iter = bm.ReverseIterator() + } + } + + nextBlock := iter.Next() + hasNext := iter.HasNext() + if !hasNext { + iter = nil + + // Check if there is another chunk to get blocks from + chunk, ok, err := chunkProvider() + if err != nil { + return 0, false, err + } + if !ok { + finished = true + return nextBlock, false, nil + } + + hasNext = true + + bm := roaring64.NewBitmap() + if _, err := bm.ReadFrom(bytes.NewReader(chunk)); err != nil { + finished = true + return 0, false, err + } + iter = bm.ReverseIterator() + } + + return nextBlock, hasNext, nil + } +} + +func NewCallCursorBackwardBlockProvider(cursor kv.Cursor, addr common.Address, maxBlock uint64) BlockProvider { + chunkLocator := newCallChunkLocator(cursor, addr, false) + return NewBackwardBlockProvider(chunkLocator, maxBlock) +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_backward_multi_test.go b/cmd/rpcdaemon/commands/otterscan_search_backward_multi_test.go new file mode 100644 index 00000000000..4aabcda0aa6 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_backward_multi_test.go @@ -0,0 +1,109 @@ +package commands + +import ( + "testing" +) + +func TestFromToBackwardBlockProviderWith1Chunk(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 0) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} + +func TestFromToBackwardBlockProviderWith1ChunkMiddleBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 1005) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} + +func TestFromToBackwardBlockProviderWith1ChunkNotExactBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 1003) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1000, false) +} + +func TestFromToBackwardBlockProviderWith1ChunkLastBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 1000) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1000, false) +} + +func TestFromToBackwardBlockProviderWith1ChunkBlockNotFound(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 900) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 0, false) +} + +func TestFromToBackwardBlockProviderWithNoChunks(t *testing.T) { + chunkLocator := newMockBackwardChunkLocator([][]byte{}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 0) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 0, false) +} + +func TestFromToBackwardBlockProviderWithMultipleChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1, chunk2}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 0) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1600, true) + checkNext(t, blockProvider, 1501, true) + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} + +func TestFromToBackwardBlockProviderWithMultipleChunksBlockBetweenChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1, chunk2}) + fromBlockProvider := NewBackwardBlockProvider(chunkLocator, 1500) + toBlockProvider := NewBackwardBlockProvider(newMockBackwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(true, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_backward_test.go b/cmd/rpcdaemon/commands/otterscan_search_backward_test.go new file mode 100644 index 00000000000..9f85b7e00f4 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_backward_test.go @@ -0,0 +1,143 @@ +package commands + +import ( + "bytes" + "testing" + + "github.com/RoaringBitmap/roaring/roaring64" +) + +func newMockBackwardChunkLocator(chunks [][]byte) ChunkLocator { + return func(block uint64) (ChunkProvider, bool, error) { + for i, v := range chunks { + bm := roaring64.NewBitmap() + if _, err := bm.ReadFrom(bytes.NewReader(v)); err != nil { + return nil, false, err + } + if block > bm.Maximum() { + continue + } + + return newMockBackwardChunkProvider(chunks[:i+1]), true, nil + } + + // Not found; return the last to simulate the behavior of returning + // everything up to the 0xffff... chunk + if len(chunks) > 0 { + return newMockBackwardChunkProvider(chunks), true, nil + } + + return nil, true, nil + } +} + +func newMockBackwardChunkProvider(chunks [][]byte) ChunkProvider { + i := len(chunks) - 1 + return func() ([]byte, bool, error) { + if i < 0 { + return nil, false, nil + } + + chunk := chunks[i] + i-- + return chunk, true, nil + } +} +func TestBackwardBlockProviderWith1Chunk(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 0) + + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} + +func TestBackwardBlockProviderWith1ChunkMiddleBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 1005) + + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} + +func TestBackwardBlockProviderWith1ChunkNotExactBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 1003) + + checkNext(t, blockProvider, 1000, false) +} + +func TestBackwardBlockProviderWith1ChunkLastBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 1000) + + checkNext(t, blockProvider, 1000, false) +} + +func TestBackwardBlockProviderWith1ChunkBlockNotFound(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 900) + + checkNext(t, blockProvider, 0, false) +} + +func TestBackwardBlockProviderWithNoChunks(t *testing.T) { + chunkLocator := newMockBackwardChunkLocator([][]byte{}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 0) + + checkNext(t, blockProvider, 0, false) +} + +func TestBackwardBlockProviderWithMultipleChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1, chunk2}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 0) + + checkNext(t, blockProvider, 1600, true) + checkNext(t, blockProvider, 1501, true) + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} + +func TestBackwardBlockProviderWithMultipleChunksBlockBetweenChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1, chunk2}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 1500) + + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1000, false) +} + +func TestBackwardBlockProviderWithMultipleChunksBlockNotFound(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockBackwardChunkLocator([][]byte{chunk1, chunk2}) + blockProvider := NewBackwardBlockProvider(chunkLocator, 900) + + checkNext(t, blockProvider, 0, false) +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_forward.go b/cmd/rpcdaemon/commands/otterscan_search_forward.go new file mode 100644 index 00000000000..f9774c6385c --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_forward.go @@ -0,0 +1,107 @@ +package commands + +import ( + "bytes" + + "github.com/RoaringBitmap/roaring/roaring64" + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/common" +) + +// Given a ChunkLocator, moves forward over the chunks and inside each chunk, moves +// forward over the block numbers. +func NewForwardBlockProvider(chunkLocator ChunkLocator, minBlock uint64) BlockProvider { + var iter roaring64.IntPeekable64 + var chunkProvider ChunkProvider + isFirst := true + finished := false + + return func() (uint64, bool, error) { + if finished { + return 0, false, nil + } + + if isFirst { + isFirst = false + + // Try to get first chunk + var ok bool + var err error + chunkProvider, ok, err = chunkLocator(minBlock) + if err != nil { + finished = true + return 0, false, err + } + if !ok { + finished = true + return 0, false, nil + } + if chunkProvider == nil { + finished = true + return 0, false, nil + } + + // Has at least the first chunk; initialize the iterator + chunk, ok, err := chunkProvider() + if err != nil { + finished = true + return 0, false, err + } + if !ok { + finished = true + return 0, false, nil + } + + bm := roaring64.NewBitmap() + if _, err := bm.ReadFrom(bytes.NewReader(chunk)); err != nil { + finished = true + return 0, false, err + } + iter = bm.Iterator() + + // It can happen that on the first chunk we'll get a chunk that contains + // the first block >= minBlock in the middle of the chunk/bitmap, so we + // skip all previous blocks before it. + iter.AdvanceIfNeeded(minBlock) + + // This means it is the last chunk and the min block is > the last one + if !iter.HasNext() { + finished = true + return 0, false, nil + } + } + + nextBlock := iter.Next() + hasNext := iter.HasNext() + if !hasNext { + iter = nil + + // Check if there is another chunk to get blocks from + chunk, ok, err := chunkProvider() + if err != nil { + finished = true + return 0, false, err + } + if !ok { + finished = true + return nextBlock, false, nil + } + + hasNext = true + + bm := roaring64.NewBitmap() + if _, err := bm.ReadFrom(bytes.NewReader(chunk)); err != nil { + finished = true + return 0, false, err + } + iter = bm.Iterator() + } + + return nextBlock, hasNext, nil + } +} + +func NewCallCursorForwardBlockProvider(cursor kv.Cursor, addr common.Address, minBlock uint64) BlockProvider { + chunkLocator := newCallChunkLocator(cursor, addr, true) + return NewForwardBlockProvider(chunkLocator, minBlock) +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_forward_multi_test.go b/cmd/rpcdaemon/commands/otterscan_search_forward_multi_test.go new file mode 100644 index 00000000000..cc1112ecec6 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_forward_multi_test.go @@ -0,0 +1,108 @@ +package commands + +import ( + "testing" +) + +func TestFromToForwardBlockProviderWith1Chunk(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 0) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1000, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1010, false) +} + +func TestFromToForwardBlockProviderWith1ChunkMiddleBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 1005) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1010, false) +} + +func TestFromToForwardBlockProviderWith1ChunkNotExactBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 1007) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1010, false) +} + +func TestFromToForwardBlockProviderWith1ChunkLastBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 1010) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1010, false) +} + +func TestFromToForwardBlockProviderWith1ChunkBlockNotFound(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 1100) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 0, false) +} + +func TestFromToForwardBlockProviderWithNoChunks(t *testing.T) { + chunkLocator := newMockForwardChunkLocator([][]byte{}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 0) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 0, false) +} + +func TestFromToForwardBlockProviderWithMultipleChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1, chunk2}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 0) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1000, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1501, true) + checkNext(t, blockProvider, 1600, false) +} + +func TestFromToForwardBlockProviderWithMultipleChunksBlockBetweenChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1, chunk2}) + fromBlockProvider := NewForwardBlockProvider(chunkLocator, 1300) + toBlockProvider := NewForwardBlockProvider(newMockForwardChunkLocator([][]byte{}), 0) + blockProvider := newCallFromToBlockProvider(false, fromBlockProvider, toBlockProvider) + + checkNext(t, blockProvider, 1501, true) + checkNext(t, blockProvider, 1600, false) +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_forward_test.go b/cmd/rpcdaemon/commands/otterscan_search_forward_test.go new file mode 100644 index 00000000000..8be27092b0b --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_forward_test.go @@ -0,0 +1,143 @@ +package commands + +import ( + "bytes" + "testing" + + "github.com/RoaringBitmap/roaring/roaring64" +) + +func newMockForwardChunkLocator(chunks [][]byte) ChunkLocator { + return func(block uint64) (ChunkProvider, bool, error) { + for i, v := range chunks { + bm := roaring64.NewBitmap() + if _, err := bm.ReadFrom(bytes.NewReader(v)); err != nil { + return nil, false, err + } + if block > bm.Maximum() { + continue + } + + return newMockForwardChunkProvider(chunks[i:]), true, nil + } + + // Not found; return the last to simulate the behavior of returning + // the 0xffff... chunk + if len(chunks) > 0 { + return newMockForwardChunkProvider(chunks[len(chunks)-1:]), true, nil + } + + return nil, true, nil + } +} + +func newMockForwardChunkProvider(chunks [][]byte) ChunkProvider { + i := 0 + return func() ([]byte, bool, error) { + if i >= len(chunks) { + return nil, false, nil + } + + chunk := chunks[i] + i++ + return chunk, true, nil + } +} + +func TestForwardBlockProviderWith1Chunk(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + blockProvider := NewForwardBlockProvider(chunkLocator, 0) + + checkNext(t, blockProvider, 1000, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1010, false) +} + +func TestForwardBlockProviderWith1ChunkMiddleBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + blockProvider := NewForwardBlockProvider(chunkLocator, 1005) + + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1010, false) +} + +func TestForwardBlockProviderWith1ChunkNotExactBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + blockProvider := NewForwardBlockProvider(chunkLocator, 1007) + + checkNext(t, blockProvider, 1010, false) +} + +func TestForwardBlockProviderWith1ChunkLastBlock(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + blockProvider := NewForwardBlockProvider(chunkLocator, 1010) + + checkNext(t, blockProvider, 1010, false) +} + +func TestForwardBlockProviderWith1ChunkBlockNotFound(t *testing.T) { + // Mocks 1 chunk + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1}) + blockProvider := NewForwardBlockProvider(chunkLocator, 1100) + + checkNext(t, blockProvider, 0, false) +} + +func TestForwardBlockProviderWithNoChunks(t *testing.T) { + chunkLocator := newMockForwardChunkLocator([][]byte{}) + blockProvider := NewForwardBlockProvider(chunkLocator, 0) + + checkNext(t, blockProvider, 0, false) +} + +func TestForwardBlockProviderWithMultipleChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1, chunk2}) + blockProvider := NewForwardBlockProvider(chunkLocator, 0) + + checkNext(t, blockProvider, 1000, true) + checkNext(t, blockProvider, 1005, true) + checkNext(t, blockProvider, 1010, true) + checkNext(t, blockProvider, 1501, true) + checkNext(t, blockProvider, 1600, false) +} + +func TestForwardBlockProviderWithMultipleChunksBlockBetweenChunks(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1, chunk2}) + blockProvider := NewForwardBlockProvider(chunkLocator, 1300) + + checkNext(t, blockProvider, 1501, true) + checkNext(t, blockProvider, 1600, false) +} + +func TestForwardBlockProviderWithMultipleChunksBlockNotFound(t *testing.T) { + // Mocks 2 chunks + chunk1 := createBitmap(t, []uint64{1000, 1005, 1010}) + chunk2 := createBitmap(t, []uint64{1501, 1600}) + + chunkLocator := newMockForwardChunkLocator([][]byte{chunk1, chunk2}) + blockProvider := NewForwardBlockProvider(chunkLocator, 1700) + + checkNext(t, blockProvider, 0, false) +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_multi.go b/cmd/rpcdaemon/commands/otterscan_search_multi.go new file mode 100644 index 00000000000..c5bdba0391f --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_multi.go @@ -0,0 +1,63 @@ +package commands + +func newCallFromToBlockProvider(isBackwards bool, callFromProvider, callToProvider BlockProvider) BlockProvider { + var nextFrom, nextTo uint64 + var hasMoreFrom, hasMoreTo bool + initialized := false + + return func() (uint64, bool, error) { + if !initialized { + initialized = true + + var err error + if nextFrom, hasMoreFrom, err = callFromProvider(); err != nil { + return 0, false, err + } + hasMoreFrom = hasMoreFrom || nextFrom != 0 + + if nextTo, hasMoreTo, err = callToProvider(); err != nil { + return 0, false, err + } + hasMoreTo = hasMoreTo || nextTo != 0 + } + + if !hasMoreFrom && !hasMoreTo { + return 0, false, nil + } + + var blockNum uint64 + if !hasMoreFrom { + blockNum = nextTo + } else if !hasMoreTo { + blockNum = nextFrom + } else { + blockNum = nextFrom + if isBackwards { + if nextTo < nextFrom { + blockNum = nextTo + } + } else { + if nextTo > nextFrom { + blockNum = nextTo + } + } + } + + // Pull next; it may be that from AND to contains the same blockNum + if hasMoreFrom && blockNum == nextFrom { + var err error + if nextFrom, hasMoreFrom, err = callFromProvider(); err != nil { + return 0, false, err + } + hasMoreFrom = hasMoreFrom || nextFrom != 0 + } + if hasMoreTo && blockNum == nextTo { + var err error + if nextTo, hasMoreTo, err = callToProvider(); err != nil { + return 0, false, err + } + hasMoreTo = hasMoreTo || nextTo != 0 + } + return blockNum, hasMoreFrom || hasMoreTo, nil + } +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_test.go b/cmd/rpcdaemon/commands/otterscan_search_test.go new file mode 100644 index 00000000000..e4a7c3b68d4 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_test.go @@ -0,0 +1,31 @@ +package commands + +import ( + "testing" + + "github.com/RoaringBitmap/roaring/roaring64" +) + +func createBitmap(t *testing.T, blocks []uint64) []byte { + bm := roaring64.NewBitmap() + bm.AddMany(blocks) + + chunk, err := bm.ToBytes() + if err != nil { + t.Fatal(err) + } + return chunk +} + +func checkNext(t *testing.T, blockProvider BlockProvider, expectedBlock uint64, expectedHasNext bool) { + bl, hasNext, err := blockProvider() + if err != nil { + t.Fatal(err) + } + if bl != expectedBlock { + t.Fatalf("Expected block %d, received %d", expectedBlock, bl) + } + if expectedHasNext != hasNext { + t.Fatalf("Expected hasNext=%t, received=%t; at block=%d", expectedHasNext, hasNext, expectedBlock) + } +} diff --git a/cmd/rpcdaemon/commands/otterscan_search_trace.go b/cmd/rpcdaemon/commands/otterscan_search_trace.go new file mode 100644 index 00000000000..a9a849dba54 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_search_trace.go @@ -0,0 +1,104 @@ +package commands + +import ( + "context" + "sync" + + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/consensus/ethash" + "github.com/ledgerwatch/erigon/core" + "github.com/ledgerwatch/erigon/core/rawdb" + "github.com/ledgerwatch/erigon/core/state" + "github.com/ledgerwatch/erigon/core/types" + "github.com/ledgerwatch/erigon/core/vm" + "github.com/ledgerwatch/erigon/params" + "github.com/ledgerwatch/erigon/turbo/shards" + "github.com/ledgerwatch/log/v3" +) + +func (api *OtterscanAPIImpl) searchTraceBlock(ctx context.Context, wg *sync.WaitGroup, addr common.Address, chainConfig *params.ChainConfig, idx int, bNum uint64, results []*TransactionsWithReceipts) { + defer wg.Done() + + // Trace block for Txs + newdbtx, err := api.db.BeginRo(ctx) + if err != nil { + log.Error("Search trace error", "err", err) + results[idx] = nil + return + } + defer newdbtx.Rollback() + + _, result, err := api.traceBlock(newdbtx, ctx, bNum, addr, chainConfig) + if err != nil { + log.Error("Search trace error", "err", err) + results[idx] = nil + return + } + results[idx] = result +} + +func (api *OtterscanAPIImpl) traceBlock(dbtx kv.Tx, ctx context.Context, blockNum uint64, searchAddr common.Address, chainConfig *params.ChainConfig) (bool, *TransactionsWithReceipts, error) { + rpcTxs := make([]*RPCTransaction, 0) + receipts := make([]map[string]interface{}, 0) + + // Retrieve the transaction and assemble its EVM context + blockHash, err := rawdb.ReadCanonicalHash(dbtx, blockNum) + if err != nil { + return false, nil, err + } + + block, senders, err := api._blockReader.BlockWithSenders(ctx, dbtx, blockHash, blockNum) + if err != nil { + return false, nil, err + } + + reader := state.NewPlainState(dbtx, blockNum) + stateCache := shards.NewStateCache(32, 0 /* no limit */) + cachedReader := state.NewCachedReader(reader, stateCache) + noop := state.NewNoopWriter() + cachedWriter := state.NewCachedWriter(noop, stateCache) + + ibs := state.New(cachedReader) + signer := types.MakeSigner(chainConfig, blockNum) + + getHeader := func(hash common.Hash, number uint64) *types.Header { + h, e := api._blockReader.Header(ctx, dbtx, hash, number) + if e != nil { + log.Error("getHeader error", "number", number, "hash", hash, "err", e) + } + return h + } + engine := ethash.NewFaker() + + blockReceipts := rawdb.ReadReceipts(dbtx, block, senders) + header := block.Header() + rules := chainConfig.Rules(block.NumberU64()) + found := false + for idx, tx := range block.Transactions() { + ibs.Prepare(tx.Hash(), block.Hash(), idx) + + msg, _ := tx.AsMessage(*signer, header.BaseFee, rules) + + tracer := NewTouchTracer(searchAddr) + BlockContext := core.NewEVMBlockContext(header, core.GetHashFn(header, getHeader), engine, nil) + TxContext := core.NewEVMTxContext(msg) + + vmenv := vm.NewEVM(BlockContext, TxContext, ibs, chainConfig, vm.Config{Debug: true, Tracer: tracer}) + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.GetGas()), true /* refunds */, false /* gasBailout */); err != nil { + return false, nil, err + } + _ = ibs.FinalizeTx(vmenv.ChainConfig().Rules(block.NumberU64()), cachedWriter) + + if tracer.Found { + rpcTx := newRPCTransaction(tx, block.Hash(), blockNum, uint64(idx), block.BaseFee()) + mReceipt := marshalReceipt(blockReceipts[idx], tx, chainConfig, block, tx.Hash(), true) + mReceipt["timestamp"] = block.Time() + rpcTxs = append(rpcTxs, rpcTx) + receipts = append(receipts, mReceipt) + found = true + } + } + + return found, &TransactionsWithReceipts{rpcTxs, receipts, false, false}, nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_trace_contract_creator.go b/cmd/rpcdaemon/commands/otterscan_trace_contract_creator.go new file mode 100644 index 00000000000..7875282934f --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_trace_contract_creator.go @@ -0,0 +1,50 @@ +package commands + +import ( + "context" + "math/big" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/core/types" + "github.com/ledgerwatch/erigon/core/vm" +) + +type CreateTracer struct { + DefaultTracer + ctx context.Context + target common.Address + found bool + Creator common.Address + Tx types.Transaction +} + +func NewCreateTracer(ctx context.Context, target common.Address) *CreateTracer { + return &CreateTracer{ + ctx: ctx, + target: target, + found: false, + } +} + +func (t *CreateTracer) SetTransaction(tx types.Transaction) { + t.Tx = tx +} + +func (t *CreateTracer) Found() bool { + return t.found +} + +func (t *CreateTracer) CaptureStart(env *vm.EVM, depth int, from common.Address, to common.Address, precompile bool, create bool, calltype vm.CallType, input []byte, gas uint64, value *big.Int, code []byte) { + if t.found { + return + } + if !create { + return + } + if to != t.target { + return + } + + t.found = true + t.Creator = from +} diff --git a/cmd/rpcdaemon/commands/otterscan_trace_operations.go b/cmd/rpcdaemon/commands/otterscan_trace_operations.go new file mode 100644 index 00000000000..59e8e05ec28 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_trace_operations.go @@ -0,0 +1,60 @@ +package commands + +import ( + "context" + "math/big" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/common/hexutil" + "github.com/ledgerwatch/erigon/core/vm" +) + +type OperationType int + +const ( + OP_TRANSFER OperationType = 0 + OP_SELF_DESTRUCT OperationType = 1 + OP_CREATE OperationType = 2 + OP_CREATE2 OperationType = 3 +) + +type InternalOperation struct { + Type OperationType `json:"type"` + From common.Address `json:"from"` + To common.Address `json:"to"` + Value *hexutil.Big `json:"value"` +} + +type OperationsTracer struct { + DefaultTracer + ctx context.Context + Results []*InternalOperation +} + +func NewOperationsTracer(ctx context.Context) *OperationsTracer { + return &OperationsTracer{ + ctx: ctx, + Results: make([]*InternalOperation, 0), + } +} + +func (t *OperationsTracer) CaptureStart(env *vm.EVM, depth int, from common.Address, to common.Address, precompile bool, create bool, calltype vm.CallType, input []byte, gas uint64, value *big.Int, code []byte) { + if depth == 0 { + return + } + + if calltype == vm.CALLT && value.Uint64() != 0 { + t.Results = append(t.Results, &InternalOperation{OP_TRANSFER, from, to, (*hexutil.Big)(value)}) + return + } + if calltype == vm.CREATET { + t.Results = append(t.Results, &InternalOperation{OP_CREATE, from, to, (*hexutil.Big)(value)}) + } + if calltype == vm.CREATE2T { + t.Results = append(t.Results, &InternalOperation{OP_CREATE2, from, to, (*hexutil.Big)(value)}) + } +} + +func (l *OperationsTracer) CaptureSelfDestruct(from common.Address, to common.Address, value *big.Int) { + l.Results = append(l.Results, &InternalOperation{OP_SELF_DESTRUCT, from, to, (*hexutil.Big)(value)}) +} diff --git a/cmd/rpcdaemon/commands/otterscan_trace_touch.go b/cmd/rpcdaemon/commands/otterscan_trace_touch.go new file mode 100644 index 00000000000..98db6a9bafc --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_trace_touch.go @@ -0,0 +1,27 @@ +package commands + +import ( + "bytes" + "math/big" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/core/vm" +) + +type TouchTracer struct { + DefaultTracer + searchAddr common.Address + Found bool +} + +func NewTouchTracer(searchAddr common.Address) *TouchTracer { + return &TouchTracer{ + searchAddr: searchAddr, + } +} + +func (t *TouchTracer) CaptureStart(env *vm.EVM, depth int, from common.Address, to common.Address, precompile bool, create bool, calltype vm.CallType, input []byte, gas uint64, value *big.Int, code []byte) { + if !t.Found && (bytes.Equal(t.searchAddr.Bytes(), from.Bytes()) || bytes.Equal(t.searchAddr.Bytes(), to.Bytes())) { + t.Found = true + } +} diff --git a/cmd/rpcdaemon/commands/otterscan_trace_transaction.go b/cmd/rpcdaemon/commands/otterscan_trace_transaction.go new file mode 100644 index 00000000000..3ba73df17e4 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_trace_transaction.go @@ -0,0 +1,87 @@ +package commands + +import ( + "context" + "math/big" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/common/hexutil" + "github.com/ledgerwatch/erigon/core/vm" +) + +func (api *OtterscanAPIImpl) TraceTransaction(ctx context.Context, hash common.Hash) ([]*TraceEntry, error) { + tx, err := api.db.BeginRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + tracer := NewTransactionTracer(ctx) + if _, err := api.runTracer(ctx, tx, hash, tracer); err != nil { + return nil, err + } + + return tracer.Results, nil +} + +type TraceEntry struct { + Type string `json:"type"` + Depth int `json:"depth"` + From common.Address `json:"from"` + To common.Address `json:"to"` + Value *hexutil.Big `json:"value"` + Input hexutil.Bytes `json:"input"` +} + +type TransactionTracer struct { + DefaultTracer + ctx context.Context + Results []*TraceEntry +} + +func NewTransactionTracer(ctx context.Context) *TransactionTracer { + return &TransactionTracer{ + ctx: ctx, + Results: make([]*TraceEntry, 0), + } +} + +func (t *TransactionTracer) CaptureStart(env *vm.EVM, depth int, from common.Address, to common.Address, precompile bool, create bool, callType vm.CallType, input []byte, gas uint64, value *big.Int, code []byte) { + if precompile { + return + } + + inputCopy := make([]byte, len(input)) + copy(inputCopy, input) + _value := new(big.Int) + _value.Set(value) + if callType == vm.CALLT { + t.Results = append(t.Results, &TraceEntry{"CALL", depth, from, to, (*hexutil.Big)(_value), inputCopy}) + return + } + if callType == vm.STATICCALLT { + t.Results = append(t.Results, &TraceEntry{"STATICCALL", depth, from, to, nil, inputCopy}) + return + } + if callType == vm.DELEGATECALLT { + t.Results = append(t.Results, &TraceEntry{"DELEGATECALL", depth, from, to, nil, inputCopy}) + return + } + if callType == vm.CALLCODET { + t.Results = append(t.Results, &TraceEntry{"CALLCODE", depth, from, to, (*hexutil.Big)(_value), inputCopy}) + return + } + if callType == vm.CREATET { + t.Results = append(t.Results, &TraceEntry{"CREATE", depth, from, to, (*hexutil.Big)(value), inputCopy}) + return + } + if callType == vm.CREATE2T { + t.Results = append(t.Results, &TraceEntry{"CREATE2", depth, from, to, (*hexutil.Big)(value), inputCopy}) + return + } +} + +func (l *TransactionTracer) CaptureSelfDestruct(from common.Address, to common.Address, value *big.Int) { + last := l.Results[len(l.Results)-1] + l.Results = append(l.Results, &TraceEntry{"SELFDESTRUCT", last.Depth + 1, from, to, (*hexutil.Big)(value), nil}) +} diff --git a/cmd/rpcdaemon/commands/otterscan_transaction_by_sender_and_nonce.go b/cmd/rpcdaemon/commands/otterscan_transaction_by_sender_and_nonce.go new file mode 100644 index 00000000000..b94ee6064f1 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_transaction_by_sender_and_nonce.go @@ -0,0 +1,163 @@ +package commands + +import ( + "bytes" + "context" + "sort" + + "github.com/RoaringBitmap/roaring/roaring64" + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/common/changeset" + "github.com/ledgerwatch/erigon/core/rawdb" + "github.com/ledgerwatch/erigon/core/types/accounts" +) + +func (api *OtterscanAPIImpl) GetTransactionBySenderAndNonce(ctx context.Context, addr common.Address, nonce uint64) (*common.Hash, error) { + tx, err := api.db.BeginRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + accHistoryC, err := tx.Cursor(kv.AccountsHistory) + if err != nil { + return nil, err + } + defer accHistoryC.Close() + + accChangesC, err := tx.CursorDupSort(kv.AccountChangeSet) + if err != nil { + return nil, err + } + defer accChangesC.Close() + + // Locate the chunk where the nonce happens + acs := changeset.Mapper[kv.AccountChangeSet] + k, v, err := accHistoryC.Seek(acs.IndexChunkKey(addr.Bytes(), 0)) + if err != nil { + return nil, err + } + + bitmap := roaring64.New() + maxBlPrevChunk := uint64(0) + var acc accounts.Account + + for { + if k == nil || !bytes.HasPrefix(k, addr.Bytes()) { + // Check plain state + data, err := tx.GetOne(kv.PlainState, addr.Bytes()) + if err != nil { + return nil, err + } + if err := acc.DecodeForStorage(data); err != nil { + return nil, err + } + + // Nonce changed in plain state, so it means the last block of last chunk + // contains the actual nonce change + if acc.Nonce > nonce { + break + } + + // Not found; asked for nonce still not used + return nil, nil + } + + // Inspect block changeset + if _, err := bitmap.ReadFrom(bytes.NewReader(v)); err != nil { + return nil, err + } + maxBl := bitmap.Maximum() + data, err := acs.Find(accChangesC, maxBl, addr.Bytes()) + if err != nil { + return nil, err + } + if err := acc.DecodeForStorage(data); err != nil { + return nil, err + } + + // Desired nonce was found in this chunk + if acc.Nonce > nonce { + break + } + + maxBlPrevChunk = maxBl + k, v, err = accHistoryC.Next() + if err != nil { + return nil, err + } + } + + // Locate the exact block inside chunk when the nonce changed + blocks := bitmap.ToArray() + var errSearch error = nil + idx := sort.Search(len(blocks), func(i int) bool { + if errSearch != nil { + return false + } + + // Locate the block changeset + data, err := acs.Find(accChangesC, blocks[i], addr.Bytes()) + if err != nil { + errSearch = err + return false + } + + if err := acc.DecodeForStorage(data); err != nil { + errSearch = err + return false + } + + // Since the state contains the nonce BEFORE the block changes, we look for + // the block when the nonce changed to be > the desired once, which means the + // previous history block contains the actual change; it may contain multiple + // nonce changes. + return acc.Nonce > nonce + }) + if errSearch != nil { + return nil, errSearch + } + + // Since the changeset contains the state BEFORE the change, we inspect + // the block before the one we found; if it is the first block inside the chunk, + // we use the last block from prev chunk + nonceBlock := maxBlPrevChunk + if idx > 0 { + nonceBlock = blocks[idx-1] + } + found, txHash, err := api.findNonce(ctx, tx, addr, nonce, nonceBlock) + if err != nil { + return nil, err + } + if !found { + return nil, nil + } + + return &txHash, nil +} + +func (api *OtterscanAPIImpl) findNonce(ctx context.Context, tx kv.Tx, addr common.Address, nonce uint64, blockNum uint64) (bool, common.Hash, error) { + hash, err := rawdb.ReadCanonicalHash(tx, blockNum) + if err != nil { + return false, common.Hash{}, err + } + block, senders, err := api._blockReader.BlockWithSenders(ctx, tx, hash, blockNum) + if err != nil { + return false, common.Hash{}, err + } + + txs := block.Transactions() + for i, s := range senders { + if s != addr { + continue + } + + t := txs[i] + if t.GetNonce() == nonce { + return true, t.Hash(), nil + } + } + + return false, common.Hash{}, nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_transaction_error.go b/cmd/rpcdaemon/commands/otterscan_transaction_error.go new file mode 100644 index 00000000000..2cccc802e9e --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_transaction_error.go @@ -0,0 +1,23 @@ +package commands + +import ( + "context" + + "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/common/hexutil" +) + +func (api *OtterscanAPIImpl) GetTransactionError(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) { + tx, err := api.db.BeginRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + result, err := api.runTracer(ctx, tx, hash, nil) + if err != nil { + return nil, err + } + + return result.Revert(), nil +} diff --git a/cmd/rpcdaemon/commands/otterscan_types.go b/cmd/rpcdaemon/commands/otterscan_types.go new file mode 100644 index 00000000000..ddd57f155e7 --- /dev/null +++ b/cmd/rpcdaemon/commands/otterscan_types.go @@ -0,0 +1,94 @@ +package commands + +import ( + "bytes" + "encoding/binary" + + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon/common" +) + +// Bootstrap a function able to locate a series of byte chunks containing +// related block numbers, starting from a specific block number (greater or equal than). +type ChunkLocator func(block uint64) (chunkProvider ChunkProvider, ok bool, err error) + +// Allows to iterate over a set of byte chunks. +// +// If err is not nil, it indicates an error and the other returned values should be +// ignored. +// +// If err is nil and ok is true, the returned chunk should contain the raw chunk data. +// +// If err is nil and ok is false, it indicates that there is no more data. Subsequent calls +// to the same function should return (nil, false, nil). +type ChunkProvider func() (chunk []byte, ok bool, err error) + +type BlockProvider func() (nextBlock uint64, hasMore bool, err error) + +// Standard key format for call from/to indexes [address + block] +func callIndexKey(addr common.Address, block uint64) []byte { + key := make([]byte, common.AddressLength+8) + copy(key[:common.AddressLength], addr.Bytes()) + binary.BigEndian.PutUint64(key[common.AddressLength:], block) + return key +} + +const MaxBlockNum = ^uint64(0) + +// This ChunkLocator searches over a cursor with a key format of [common.Address, block uint64], +// where block is the first block number contained in the chunk value. +// +// It positions the cursor on the chunk that contains the first block >= minBlock. +func newCallChunkLocator(cursor kv.Cursor, addr common.Address, navigateForward bool) ChunkLocator { + return func(minBlock uint64) (ChunkProvider, bool, error) { + searchKey := callIndexKey(addr, minBlock) + k, _, err := cursor.Seek(searchKey) + if k == nil { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + + return newCallChunkProvider(cursor, addr, navigateForward), true, nil + } +} + +// This ChunkProvider is built by NewForwardChunkLocator and advances the cursor forward until +// there is no more chunks for the desired addr. +func newCallChunkProvider(cursor kv.Cursor, addr common.Address, navigateForward bool) ChunkProvider { + first := true + var err error + // TODO: is this flag really used? + eof := false + return func() ([]byte, bool, error) { + if err != nil { + return nil, false, err + } + if eof { + return nil, false, nil + } + + var k, v []byte + if first { + first = false + k, v, err = cursor.Current() + } else { + if navigateForward { + k, v, err = cursor.Next() + } else { + k, v, err = cursor.Prev() + } + } + + if err != nil { + eof = true + return nil, false, err + } + if !bytes.HasPrefix(k, addr.Bytes()) { + eof = true + return nil, false, nil + } + return v, true, nil + } +}