Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changelog/1228.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
analyzer/evmverifier: Fix missing bytecode for verified contracts

Insert address preimages and queue verified contracts for bytecode analysis.
Reprocess contracts missing preimages to backfill historical data from
Sourcify.
2 changes: 1 addition & 1 deletion .github/workflows/ci-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ jobs:

test-e2e-regression:
env:
E2E_REGRESSION_ARTIFACTS_VERSION: 2025-10-21
E2E_REGRESSION_ARTIFACTS_VERSION: 2025-12-23
strategy:
matrix:
suite:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ E2E_REGRESSION_SUITES_NO_LINKS := eden_testnet_2025 eden_2025 eden damask
# make E2E_REGRESSION_SUITES='suite1 suite2' test-e2e-regression
E2E_REGRESSION_SUITES := $(E2E_REGRESSION_SUITES_NO_LINKS) edenfast

E2E_REGRESSION_ARTIFACTS_VERSION = 2025-10-21
E2E_REGRESSION_ARTIFACTS_VERSION = 2025-12-23

upload-e2e-regression-caches:
for suite in $(E2E_REGRESSION_SUITES); do \
Expand Down
66 changes: 64 additions & 2 deletions analyzer/evmverifier/evmverifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,54 @@ func (p *processor) getNexusVerifiedContracts(ctx context.Context) (map[oasisAdd
return nexusVerifiedContracts, nil
}

// getContractsMissingPreimage returns the set of verified contract addresses
// that are missing their address preimage (ETH address mapping).
func (p *processor) getContractsMissingPreimage(ctx context.Context) (map[oasisAddress]struct{}, error) {
rows, err := p.target.Query(ctx, queries.RuntimeEVMVerifiedContractsMissingPreimage, p.runtime)
if err != nil {
return nil, fmt.Errorf("querying contracts missing preimage: %w", err)
}
defer rows.Close()

missing := map[oasisAddress]struct{}{}
for rows.Next() {
var addr oasisAddress
if err = rows.Scan(&addr); err != nil {
return nil, fmt.Errorf("scanning contract missing preimage: %w", err)
}
missing[addr] = struct{}{}
}
return missing, nil
}

func (p *processor) GetItems(ctx context.Context, limit uint64) ([]contract, error) {
// Load all nexus-verified contracts from the DB.
nexusLevels, err := p.getNexusVerifiedContracts(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get nexus verified contracts: %w", err)
}

// Load contracts that are missing their address preimage.
// These need to be reprocessed to insert the preimage.
missingPreimage, err := p.getContractsMissingPreimage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get contracts missing preimage: %w", err)
}
if len(missingPreimage) > 0 {
p.logger.Info("found verified contracts missing preimage", "count", len(missingPreimage))
}

// Query Sourcify for list of all verified contracts.
sourcifyLevels, err := p.source.GetVerifiedContractAddresses(ctx, p.runtime)
if err != nil {
return nil, fmt.Errorf("failed to get verified contract addresses: %w", err)
}
p.logger.Debug("got verified contract addresses", "addresses", sourcifyLevels)

// Find contracts that are verified in Sourcify and not yet verified in Nexus.
// Find contracts that need processing:
// 1. Verified in Sourcify but not yet verified in Nexus
// 2. Upgrading from partial to full verification
// 3. Missing their address preimage (need to reprocess to insert it)
var items []contract
for ethAddr, sourcifyLevel := range sourcifyLevels {
oasisAddr, err := addresses.FromEthAddress(ethAddr.Bytes())
Expand All @@ -145,7 +178,13 @@ func (p *processor) GetItems(ctx context.Context, limit uint64) ([]contract, err
}

nexusLevel, isKnownToNexus := nexusLevels[oasisAddress(oasisAddr)]
if !isKnownToNexus || (nexusLevel == sourcify.VerificationLevelPartial && sourcifyLevel == sourcify.VerificationLevelFull) {
_, isMissingPreimage := missingPreimage[oasisAddress(oasisAddr)]

needsProcessing := !isKnownToNexus ||
(nexusLevel == sourcify.VerificationLevelPartial && sourcifyLevel == sourcify.VerificationLevelFull) ||
isMissingPreimage

if needsProcessing {
items = append(items, contract{
Addr: oasisAddress(oasisAddr),
EthAddr: ethAddr,
Expand Down Expand Up @@ -209,6 +248,29 @@ func (p *processor) ProcessItem(ctx context.Context, batch *storage.QueryBatch,
item.VerificationLevel,
)

// Insert the address preimage (ETH address -> Oasis address mapping).
// This is needed for the evm_contract_code analyzer to fetch bytecode.
// For contracts that were never seen by the block analyzer (e.g., created
// via encrypted transactions on Sapphire), this is the only way the
// preimage gets inserted.
batch.Queue(
queries.AddressPreimageInsert,
item.Addr,
"oasis-runtime-sdk/address: secp256k1eth",
0, // context_version
item.EthAddr.Bytes(),
)

// Queue the contract for bytecode analysis. This ensures contracts discovered
// via Sourcify verification get their bytecode fetched, even if the block analyzer
// never saw the contract (e.g., contracts only called internally by other contracts,
// or contracts created via encrypted transactions on Sapphire).
batch.Queue(
queries.RuntimeEVMContractCodeAnalysisInsert,
p.runtime,
item.Addr,
)

return nil
}

Expand Down
25 changes: 23 additions & 2 deletions analyzer/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -847,18 +847,25 @@ var (
WHERE runtime = $1 AND contract_candidate = $2`

RuntimeEVMContractCodeAnalysisStale = `
WITH download_round AS (
SELECT MAX(height) AS height
FROM analysis.processed_blocks
WHERE analyzer = $1::runtime::text AND processed_time IS NOT NULL
)
SELECT
code_analysis.contract_candidate,
pre.address_data AS eth_contract_candidate,
(SELECT MAX(height) FROM analysis.processed_blocks WHERE analyzer = $1::runtime::text AND processed_time IS NOT NULL) AS download_round
download_round.height 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
CROSS JOIN download_round
WHERE
code_analysis.runtime = $1::runtime AND
code_analysis.is_contract IS NULL
code_analysis.is_contract IS NULL AND
download_round.height IS NOT NULL
LIMIT $2`

RuntimeEVMContractCodeAnalysisStaleCount = `
Expand Down Expand Up @@ -1187,6 +1194,20 @@ var (
WHERE
runtime = $1 AND verification_level IS NOT NULL`

// RuntimeEVMVerifiedContractsMissingPreimage returns verified contracts that
// are missing their address preimage (ETH address mapping). These need to be
// reprocessed by the verifier to insert the preimage.
RuntimeEVMVerifiedContractsMissingPreimage = `
SELECT contracts.contract_address
FROM chain.evm_contracts AS contracts
LEFT JOIN chain.address_preimages AS preimages
ON contracts.contract_address = preimages.address
AND preimages.context_identifier = 'oasis-runtime-sdk/address: secp256k1eth'
WHERE
contracts.runtime = $1
AND contracts.verification_level IS NOT NULL
AND preimages.address IS NULL`

RuntimeEVMVerifyContractUpsert = `
INSERT INTO chain.evm_contracts (runtime, contract_address, verification_info_downloaded_at, abi, compilation_metadata, source_files, verification_level)
VALUES ($1, $2, CURRENT_TIMESTAMP, $3, $4, $5, $6)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- Backfill verified contracts into the bytecode analysis queue.
--
-- Some verified contracts were never queued for bytecode download because:
-- 1. They were only called internally by other contracts (not as direct tx recipients)
-- 2. They were created via encrypted transactions on Sapphire
-- 3. They were processed before commit 83c02272 which added detection of
-- contract candidates via log-emitting addresses
--
-- The Sourcify verifier now:
-- - Inserts address preimages for verified contracts
-- - Queues contracts for bytecode analysis
-- - Reprocesses contracts that are missing preimages
--
-- This migration queues any verified contracts (that have preimages) for
-- bytecode analysis if they're not already in the queue.
--
-- Reference: https://github.com/oasisprotocol/nexus/issues/1227

INSERT INTO analysis.evm_contract_code (runtime, contract_candidate)
SELECT
c.runtime,
c.contract_address
FROM chain.evm_contracts c
-- Only include contracts with preimages (required for bytecode fetching)
JOIN chain.address_preimages p
ON c.contract_address = p.address
AND p.context_identifier = 'oasis-runtime-sdk/address: secp256k1eth'
-- Exclude contracts already in the analysis queue
LEFT JOIN analysis.evm_contract_code a
ON a.runtime = c.runtime
AND a.contract_candidate = c.contract_address
WHERE
-- Only verified contracts
c.verification_level IS NOT NULL
-- Missing bytecode
AND c.runtime_bytecode IS NULL
-- Not already in queue
AND a.contract_candidate IS NULL
ON CONFLICT (runtime, contract_candidate) DO NOTHING;
2 changes: 1 addition & 1 deletion tests/e2e_regression/damask/e2e_config_1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ analysis:
nodes:
# Node will be accessed only when the filling up the cache.
# In CI, the cache is expected to have all the node responses as the node is not reachable.
damask: { default: { rpc: unix:/tmp/node.sock } }
damask: { default: { rpc: internal-mainnet-20220411-20231129:443 } } #{ rpc: unix:/tmp/node.sock } }
ipfs:
gateway: https://ipfs.io
fast_startup: true
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e_regression/damask/e2e_config_2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ analysis:
cache: { cache_dir: tests/e2e_regression/damask/rpc-cache }
chain_name: mainnet
nodes:
damask: { default: { rpc: unix:/tmp/node.sock } }
damask: { default: { rpc: internal-mainnet-20220411-20231129:443 } } #{ rpc: unix:/tmp/node.sock } }
ipfs:
gateway: https://ipfs.io
fast_startup: true
Expand All @@ -17,7 +17,7 @@ analysis:
evm_tokens_emerald: { stop_if_queue_empty_for: 1s }
evm_nfts_emerald: { stop_if_queue_empty_for: 1s }
evm_token_balances_emerald: { stop_if_queue_empty_for: 1s }
evm_contract_code_emerald: { stop_if_queue_empty_for: 1s }
# evm_contract_code_emerald: { stop_if_queue_empty_for: 1s } # Disabled: emerald block analyzer is disabled in phase 1
evm_abi_emerald: { stop_if_queue_empty_for: 10s } # Give evm_contract_verifier time to fetch ABIs first. The 10s has been enough in practice, but might need to be tuned in the future, especially if the caching proxy has an empty cache.
evm_contract_verifier_emerald: { stop_if_queue_empty_for: 1s, sourcify_server_url: http://localhost:9191 }
validator_staking_history: { from: 8_048_956, stop_if_queue_empty_for: 1s, max_backoff_time: 6s }
Expand Down
Loading
Loading