Skip to content

Commit

Permalink
New analyzer to fetch runtime bytecode of EVM contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
mitjat committed Jun 15, 2023
1 parent 614d99e commit f933cc8
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 8 deletions.
198 changes: 198 additions & 0 deletions analyzer/evmcontractcode/evm_contract_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package evmcontractcode

import (
"context"
"fmt"
"time"

"golang.org/x/sync/errgroup"

"github.com/oasisprotocol/oasis-indexer/analyzer"
"github.com/oasisprotocol/oasis-indexer/analyzer/queries"
"github.com/oasisprotocol/oasis-indexer/analyzer/util"
"github.com/oasisprotocol/oasis-indexer/common"
"github.com/oasisprotocol/oasis-indexer/log"
"github.com/oasisprotocol/oasis-indexer/storage"
"github.com/oasisprotocol/oasis-indexer/storage/oasis/nodeapi"
)

// This analyzer checks the list of addresses with an unknown is_contract status,
// and determines it by calling `getCode()` on the address.
// If the code is returned, it is also stored in the DB.
// Every address that is the recipient of a call is a potential contract address.
// Candidate addresses are inserted into the DB by the block analyzer.
// Each candidate address only needs to be checked once.

const (
EvmContractCodeAnalyzerPrefix = "evm_contract_code_"
MaxDownloadBatch = 20
DownloadTimeout = 61 * time.Second
)

type EthAddress []byte

Check failure on line 32 in analyzer/evmcontractcode/evm_contract_code.go

View workflow job for this annotation

GitHub Actions / lint-go

File is not `gofumpt`-ed (gofumpt)
type OasisAddress string

type Main struct {
runtime common.Runtime
source nodeapi.RuntimeApiLite
target storage.TargetStorage
logger *log.Logger
}

var _ analyzer.Analyzer = (*Main)(nil)

func NewMain(
runtime common.Runtime,
sourceClient nodeapi.RuntimeApiLite,
target storage.TargetStorage,
logger *log.Logger,
) (*Main, error) {
return &Main{
runtime: runtime,
source: sourceClient,
target: target,
logger: logger.With("analyzer", EvmContractCodeAnalyzerPrefix+runtime),
}, nil
}

type ContractCandidate struct {
Addr OasisAddress
EthAddr EthAddress
DownloadRound uint64
}

func (m Main) getContractCandidates(ctx context.Context, limit int) ([]ContractCandidate, error) {
var candidates []ContractCandidate
rows, err := m.target.Query(ctx, queries.RuntimeEVMContractCodeAnalysisStale, m.runtime, limit)
if err != nil {
return nil, fmt.Errorf("querying contract candidates: %w", err)
}
defer rows.Close()
for rows.Next() {
var cc ContractCandidate
if err = rows.Scan(
&cc.Addr,
&cc.EthAddr,
&cc.DownloadRound,
); err != nil {
return nil, fmt.Errorf("scanning contract candidate: %w", err)
}
candidates = append(candidates, cc)
}
return candidates, nil
}

func (m Main) processContractCandidate(ctx context.Context, batch *storage.QueryBatch, candidate ContractCandidate) error {
m.logger.Info("downloading code", "addr", candidate.Addr, "eth_addr", fmt.Sprintf("%x", candidate.EthAddr))
code, err := m.source.EVMGetCode(ctx, candidate.DownloadRound, candidate.EthAddr)
if err != nil {
// Write nothing into the DB; we'll try again later.
return fmt.Errorf("downloading code for %x: %w", candidate.EthAddr, err)
}
if code == nil {
batch.Queue(
queries.RuntimeEVMContractCodeAnalysisSetIsContract,
m.runtime,
candidate.Addr,
false, // is_contract
)
} else {
batch.Queue(
queries.RuntimeEVMContractCodeAnalysisSetIsContract,
m.runtime,
candidate.Addr,
true, // is_contract
)
batch.Queue(
queries.RuntimeEVMContractSetRuntimeBytecode,
m.runtime,
candidate.Addr,
code,
)
}
return nil
}

func (m Main) processBatch(ctx context.Context) (int, error) {
contractCandidates, err := m.getContractCandidates(ctx, MaxDownloadBatch)
if err != nil {
return 0, fmt.Errorf("getting contract candidates: %w", err)
}
m.logger.Info("processing", "num_contract_candidates", len(contractCandidates))
if len(contractCandidates) == 0 {
return 0, nil
}

ctxWithTimeout, cancel := context.WithTimeout(ctx, DownloadTimeout)
defer cancel()
group, groupCtx := errgroup.WithContext(ctxWithTimeout)

batches := make([]*storage.QueryBatch, 0, len(contractCandidates))

for _, cc := range contractCandidates {
// Redeclare `st` for unclobbered use within goroutine.
cc := cc
batch := &storage.QueryBatch{}
batches = append(batches, batch)
group.Go(func() error {
return m.processContractCandidate(groupCtx, batch, cc)
})
}

if err := group.Wait(); err != nil {
return 0, err
}

batch := &storage.QueryBatch{}
for _, b := range batches {
batch.Extend(b)
}
if err := m.target.SendBatch(ctx, batch); err != nil {
return 0, fmt.Errorf("sending batch: %w", err)
}
return len(contractCandidates), nil
}

func (m Main) Start(ctx context.Context) {
backoff, err := util.NewBackoff(
100*time.Millisecond,
// Cap the timeout at the expected round time. All runtimes currently have the same round time.
6*time.Second,
)
if err != nil {
m.logger.Error("error configuring indexer backoff policy",
"err", err,
)
return
}

for {
select {
case <-time.After(backoff.Timeout()):
// Process next block.
case <-ctx.Done():
m.logger.Warn("shutting down evm_contract_code analyzer", "reason", ctx.Err())
return
}

numProcessed, err := m.processBatch(ctx)
if err != nil {
m.logger.Error("error processing batch", "err", err)
backoff.Failure()
continue
}

if numProcessed == 0 {
// Count this as a failure to reduce the polling when we are
// running faster than the block analyzer can find new contract candidates.
backoff.Failure()
continue
}

backoff.Success()
}
}

func (m Main) Name() string {
return EvmContractCodeAnalyzerPrefix + string(m.runtime)
}
25 changes: 25 additions & 0 deletions analyzer/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,36 @@ var (
(runtime, contract_address, creation_tx, creation_bytecode)
VALUES ($1, $2, $3, $4)`

RuntimeEVMContractSetRuntimeBytecode = `
UPDATE chain.evm_contracts
SET runtime_bytecode = $3
WHERE runtime = $1 AND contract_address = $2`

RuntimeEVMContractCodeAnalysisInsert = `
INSERT INTO analysis.evm_contract_code(runtime, contract_candidate)
VALUES ($1, $2)
ON CONFLICT (runtime, contract_candidate) DO NOTHING`

RuntimeEVMContractCodeAnalysisSetIsContract = `
UPDATE analysis.evm_contract_code
SET is_contract = $3
WHERE runtime = $1 AND contract_candidate = $2`

RuntimeEVMContractCodeAnalysisStale = `
SELECT
code_analysis.contract_candidate,
pre.address_data AS eth_contract_candidate,
(SELECT MAX(height) FROM chain.processed_blocks WHERE analyzer = $1::runtime::text AND processed_time IS NOT NULL) AS download_round
FROM analysis.evm_contract_code AS code_analysis
JOIN chain.address_preimages AS pre ON
pre.address = code_analysis.contract_candidate AND
pre.context_identifier = 'oasis-runtime-sdk/address: secp256k1eth' AND
pre.context_version = 0
WHERE
code_analysis.runtime = $1::runtime AND
code_analysis.is_contract IS NULL
LIMIT $2`

RuntimeEVMTokenBalanceUpdate = `
INSERT INTO chain.evm_token_balances (runtime, token_address, account_address, balance)
VALUES ($1, $2, $3, $4)
Expand Down
19 changes: 19 additions & 0 deletions cmd/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/oasisprotocol/oasis-indexer/analyzer"
"github.com/oasisprotocol/oasis-indexer/analyzer/consensus"
"github.com/oasisprotocol/oasis-indexer/analyzer/evmcontractcode"
"github.com/oasisprotocol/oasis-indexer/analyzer/evmtokenbalances"
"github.com/oasisprotocol/oasis-indexer/analyzer/evmtokens"
"github.com/oasisprotocol/oasis-indexer/analyzer/runtime"
Expand Down Expand Up @@ -319,6 +320,24 @@ func NewService(cfg *config.AnalysisConfig) (*Service, error) {
return evmtokenbalances.NewMain(common.RuntimeSapphire, runtimeMetadata, sourceClient, dbClient, logger)
})
}
if cfg.Analyzers.EmeraldContractCode != nil {
analyzers, err = addAnalyzer(analyzers, err, func() (A, error) {
sourceClient, err1 := sources.Runtime(ctx, common.RuntimeEmerald)
if err1 != nil {
return nil, err1
}
return evmcontractcode.NewMain(common.RuntimeEmerald, sourceClient, dbClient, logger)
})
}
if cfg.Analyzers.SapphireContractCode != nil {
analyzers, err = addAnalyzer(analyzers, err, func() (A, error) {
sourceClient, err1 := sources.Runtime(ctx, common.RuntimeSapphire)
if err1 != nil {
return nil, err1
}
return evmcontractcode.NewMain(common.RuntimeSapphire, sourceClient, dbClient, logger)
})
}
if cfg.Analyzers.MetadataRegistry != nil {
analyzers, err = addAnalyzer(analyzers, err, func() (A, error) {
return analyzer.NewMetadataRegistryAnalyzer(cfg.Analyzers.MetadataRegistry, dbClient, logger)
Expand Down
12 changes: 8 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,12 @@ type AnalyzersList struct {
Sapphire *BlockBasedAnalyzerConfig `koanf:"sapphire"`
Cipher *BlockBasedAnalyzerConfig `koanf:"cipher"`

EmeraldEvmTokens *EvmTokensAnalyzerConfig `koanf:"evm_tokens_emerald"`
SapphireEvmTokens *EvmTokensAnalyzerConfig `koanf:"evm_tokens_sapphire"`
EmeraldEvmTokenBalances *EvmTokensAnalyzerConfig `koanf:"evm_token_balances_emerald"`
SapphireEvmTokenBalances *EvmTokensAnalyzerConfig `koanf:"evm_token_balances_sapphire"`
EmeraldEvmTokens *EvmTokensAnalyzerConfig `koanf:"evm_tokens_emerald"`
SapphireEvmTokens *EvmTokensAnalyzerConfig `koanf:"evm_tokens_sapphire"`
EmeraldEvmTokenBalances *EvmTokensAnalyzerConfig `koanf:"evm_token_balances_emerald"`
SapphireEvmTokenBalances *EvmTokensAnalyzerConfig `koanf:"evm_token_balances_sapphire"`
EmeraldContractCode *EvmContractCodeAnalyzerConfig `koanf:"evm_contract_code_emerald"`
SapphireContractCode *EvmContractCodeAnalyzerConfig `koanf:"evm_contract_code_sapphire"`

MetadataRegistry *MetadataRegistryConfig `koanf:"metadata_registry"`
AggregateStats *AggregateStatsConfig `koanf:"aggregate_stats"`
Expand Down Expand Up @@ -268,6 +270,8 @@ type IntervalBasedAnalyzerConfig struct {

type EvmTokensAnalyzerConfig struct{}

type EvmContractCodeAnalyzerConfig struct{}

// Validate validates the range configuration.
func (cfg *BlockBasedAnalyzerConfig) Validate() error {
if cfg.To != 0 && cfg.From > cfg.To {
Expand Down
8 changes: 4 additions & 4 deletions storage/migrations/05_evm_runtime_bytecode.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ CREATE TABLE analysis.evm_contract_code (
runtime runtime NOT NULL,
contract_candidate oasis_addr NOT NULL,
PRIMARY KEY (runtime, contract_candidate),
-- Meaning of is_downloaded:
-- Meaning of is_contract:
-- TRUE: downloaded runtime bytecode
-- FALSE: download failed because `contract_candidate` is not a contract (= does not have code)
-- NULL: not yet attempted
is_downloaded bool
is_contract bool
);
-- Allow the analyzer to quickly retrieve addresses that have not been downloaded yet.
CREATE INDEX ix_evm_contract_code_todo ON analysis.evm_contract_code (runtime, contract_candidate) WHERE is_downloaded IS NULL;
CREATE INDEX ix_evm_contract_code_todo ON analysis.evm_contract_code (runtime, contract_candidate) WHERE is_contract IS NULL;

-- Bootstrap the table with the set of addresses we known are contracts because they are the result of an evm.Create tx.
INSERT INTO analysis.evm_contract_code (runtime, contract_candidate, is_downloaded)
INSERT INTO analysis.evm_contract_code (runtime, contract_candidate, is_contract)
SELECT runtime, contract_address, NULL
FROM chain.evm_contracts
ON CONFLICT (runtime, contract_candidate) DO NOTHING;
Expand Down

0 comments on commit f933cc8

Please sign in to comment.