EVM event identity, migration and recovery
This page describes the application version with migration 022_evm_event_identity.sql. It does not assert that a deployment has upgraded. Historical impact is unknown: a separate read-only assessment must compare canonical receipts, indexed rows and destination execution before claiming either live loss or no impact.
Identity and compatibility
An EVM source event is (evmChainId, lowercase sourceTxHash, sourceEventIndex). sourceEventIndex is the actual receipt log index (ethers.Log.index, JSON-RPC logIndex), which is block-wide. Locks, wrapped burns and mixed siblings have separate rows and statuses. sourceContract and sourceBlockHash retain emitter and inclusion evidence. The API also returns the readable sourceEventIdentity string evm:{chainId}:{txHash}:{index}.
sourceTxIndex retains the transaction's position in its block for Casper eth_tx_index. The existing sourceLogIndex field remains a deprecated transaction-position alias for EVM rows, preserving its historical API meaning. Never use it to identify a log. Legacy rows start with null sourceEventIndex; the migration does not guess missing log positions or change IDs, amounts, nonces, statuses, destination evidence, signature counts or timestamps. Casper source representation is unchanged.
New uniqueness is chain/hash/log index. Legacy rows retain a separate scoped uniqueness constraint until mapped. A hash-only API lookup still returns 409 on multiple rows, as introduced by #113. To track an EVM event, supply both sourceChainId and sourceEventIndex on /api/v1/transactions/hash/{hash}. An unresolved legacy row for that receipt returns legacy_event_identity_required (409). A lone hash-matched row is not proof that every historical event was indexed.
Completion and retry
TokensMinted emits chain/block/transaction position but omits the source nonce. Before selecting a lock, the indexer verifies successful factory-attributed execution and validates the direct factory transaction hash/SDK structure. mint_with_signatures supplies the nonce directly; mint_with_attestation supplies it in the validated attestation bytes. It then selects by source chain, block, transaction position, lock type and nonce, including terminal candidates when checking ambiguity. The attestation's positional log field is not substituted for the receipt log index.
An absent source remains pending even if a sibling already exists. Missing direct-call context, proxy calls and multiple indistinguishable mint events remain unresolved; outer proxy arguments cannot identify the inner mint. Legacy pending records are preserved and can recover context from their destination deploy. New pending mint keys include destination hash and the emitted correlation fields, so different mint payloads in one deploy do not overwrite each other. Identical payloads share an unresolved bucket; a multi-mint deploy cannot retire it through the single-direct-mint path.
Native unlock retains authenticated chain/nonce correlation and checks every matching wrapped burn, including completed rows. Completion writes are conditional: they preserve failed/confirming rows, reject conflicting destination hashes, and preserve an already completed row's destination metadata on replay. One event completing never retires another nonce's retry.
EVM retries use chain/locker scope plus hash/actual log index, and re-read the canonical block hash before invoking the source handler. Head observations remain confirming. A re-included transaction can change both transaction position and block-wide log index: ingestion checks chain/hash/type/nonce before inserting another obligation, validates the stored payload/emitter, and re-anchors the existing pre-finality row when the new index is free. Its row ID is preserved. If another sibling occupies the new index, ingestion refuses the move and retains the conflict for reconciliation; it does not overwrite or delete that sibling. Orphan head observations are pruned. Finalized/terminal inclusion conflicts are retained for reconciliation, not silently replaced. EVM destination completion is invoked directly by finalized scanning: persistence or ambiguity errors retain that scan cursor, not a source-dispatcher retry. Issue #117 owns broader recovery lifecycle, null-chain operator resolution and redeployment ambiguity; single-writer deployment constraints remain.
Consumer compatibility
The API/OpenAPI, executable transaction tracker and documentation smoke consumer in this repository use the explicit source-event identity. Once resolved, retain the numeric row ID for polling; a pre-finality re-anchor can change sourceEventIndex. Never infer a sibling's completion from a shared transaction hash.
The initial read-only web audit on 2026-09-11 found hash-only optimistic storage/reconciliation and overlay filtering, allowing one indexed sibling to hide another local placeholder. Companion astralbeam-web PR #26 implements event-specific persistent storage and optimistic overlay reconciliation, plus receipt-bound EVM completion that follows the resolved row ID. Its verification at c7c3a0dbe4f0211d23c4d9f76a9c52822e18969f includes 344 passing tests, independently rerun by the coordinating author, plus a production build and browser approval/completion regressions for both lock and wrapped burn. Receipt expectations and pending metadata are captured from the submitted write; wallet disconnection or wallet/form/chain changes cannot replace them. Tests cover sibling isolation, missing/ambiguous receipts, post-submission context changes and fresh retry/reopen snapshots. Hosted CI and independent current-head approval remain readiness gates. That companion is not merged or deployed; the core change alone does not update the web consumer. All coordinated #108 PRs must be ready, approved and CI-green. Merge and deployment remain separately authorized actions.
Missing or ambiguous EVM receipt identity stays unresolved, without hash-only completion fallback. Hash-only entries may describe a submitted transaction, but cannot prove completion of an individual bridge event. Casper retains its existing unambiguous source-hash representation; generalized multi-event Casper discovery and historical reconstruction are outside this companion. Neither repository's local tests establish deployed behavior or historical completeness.
Upgrade
- Stop indexer/API writers, keep a verified database snapshot and record schema migration state, cursors, pending queues and row counts. Do not run old and new writers together.
- Apply the normal migrations with the correct
BRIDGE_NETWORK. Migration 022 works on fresh and populated databases and can be replayed after interruption. It creates the new unique keys before dropping the old unscoped key. MySQL DDL commits independently, so keep writers stopped until the migration finishes. Do not infer atomic DDL rollback. - Start the matching application version. Existing legacy receipts fail closed during new event ingestion until mapped; successful current receipts continue normally. Check recovery age and API 409 responses.
- Use the read-only assessment to choose explicit historical hashes. A new binary does not rewind existing scan cursors or reconstruct previously discarded siblings automatically.
Bounded source identity backfill
Set EVM_IDENTITY_CHAIN to a configured chain slug, EVM_IDENTITY_RPC_URL to that chain's trusted RPC, and EVM_IDENTITY_LOCKER to the assessed locker. Database variables must target the intended environment. Preview is the default and performs only database/chain reads:
npm run backfill:evm-identity --prefix api -- --dry-run 0xSOURCE_TRANSACTION_HASH
Supply 1–100 exact hashes. The tool validates RPC chain ID, successful receipt, configured confirmation depth, protocol finality and canonical block hash. It decodes all recognized source logs in each receipt, including other emitters when testing ambiguity. A legacy row must map one-to-one by chain, source block and transaction position, type, nonce, raw amount, sender, recipient and a known chain-specific token mapping. Unknown/null-chain rows, unknown tokens, missing evidence, multiple matches and conflicts with canonical rows abort the batch. No operator mapping is guessed.
After reviewing the preview and obtaining execution authorization, set EVM_IDENTITY_BACKUP to a new snapshot filename and repeat the same hashes with --execute --confirm. The tool writes the snapshot before mutation, locks/revalidates all selected rows and canonical blocks, then atomically fills identity fields and enqueues every source event from the selected locker. It changes no lifecycle status or cursor. Normal finalized recovery creates missing siblings and preserves completed legacy rows. A changed snapshot or ambiguity rolls back the whole DML batch. Repeat with a new snapshot filename after an interrupted run; duplicate queue entries are safe.
For missing Casper destination correlation, use the existing backfill:source tool on the exact Casper destination deploy to replay authenticated execution. The old destination-only backfill script refuses EVM-source hash/block-position hints and manual hash-only completion because they cannot distinguish siblings. Do not force-complete a row based on another event in the receipt.
Rollback
Prefer fixing forward. Stop writers and preserve the migrated database plus all post-upgrade receipts/queues. Run the read-only compatibility check:
npm run db:check-identity-rollback --prefix api
It reports collisions under the old (source_tx_hash, source_log_index) key and old pending-mint deploy-hash key, and exits nonzero if either cannot be recreated. Passing is only a necessary schema check; it does not authorize old writers or establish historical completeness. Never delete siblings or merge statuses to make an old key fit.
There is deliberately no destructive down migration. If rollback is necessary, restore a reviewed pre-migration snapshot into a replacement database while writers remain stopped, retain the migrated database, and reconcile/replay all post-snapshot work before resuming. Restoring a snapshot without replay loses new records. Identity-backfill snapshots contain full pre-state for review; reverse only reviewed identity assignments under stopped writers and exact state checks, never statuses or destination evidence. Null-chain/redeployment resolutions require the separate operator workflow in #117.
Historical assessment
A read-only capture of both testnet APIs returned 32 rows, including 20 EVM-source rows. Fourteen retrieved receipts each confirmed that the stored legacy position was the transaction index rather than the actual Locked log index; six receipts remained unresolved. No retrieved receipt contained sibling bridge events, but this bounded inventory cannot establish absence of historical loss, wholly omitted transactions, or relayer execution outcomes. See the assessment and per-row evidence. No live backfill or database mutation was performed.
Local verification
api/src/indexers/__tests__/event-identity.mysql.test.ts runs real handlers and repositories against an explicitly supplied disposable IDENTITY_MYSQL_PORT, creating and dropping only its random test databases. It covers lock/burn/mixed siblings, chain separation, duplicates, independent statuses, reorgs (including changed log indices and terminal/sibling conflicts), completion/retry identity, populated migration replay, legacy compatibility and the actual backfill apply path. Ordinary tests must unset MySQL opt-in variables and exclude dist/** after building. Use distinct disposable loopback ports for ORIGIN_REPAIR_MYSQL_PORT, TEST_MYSQL_PORT, RECOVERY_MYSQL_PORT and IDENTITY_MYSQL_PORT; never supply a shared database endpoint. The API Tests workflow runs the complete origin-upgrade suite with its dedicated service and production migration entrypoint. Its snapshots compare every old field, including statuses and timestamps, and explicitly assert migration 022's six additive transaction columns.