From 0a311a6fb1e441ebd17c7cf3bfdcbef622d4118a Mon Sep 17 00:00:00 2001 From: Koen Date: Mon, 20 Apr 2026 16:52:43 +0530 Subject: [PATCH] feat: resolve rollup from registry --- .github/workflows/build.yaml | 1 - .github/workflows/deploy-indexer.yaml | 3 +- .../workflows/deploy-staking-dashboard.yaml | 1 - README.md | 5 +- atp-indexer/.env.example | 6 +- atp-indexer/bootstrap.sh | 21 ++++-- atp-indexer/ponder.config.ts | 41 ++++++++++-- atp-indexer/ponder.schema.ts | 20 ++++++ atp-indexer/src/abis/index.ts | 7 ++ atp-indexer/src/abis/registry.abi.ts | 40 ++++++++++++ atp-indexer/src/api/handlers/atp/details.ts | 5 +- .../src/api/handlers/provider/details.ts | 5 +- atp-indexer/src/api/handlers/provider/list.ts | 5 +- atp-indexer/src/api/handlers/rollup/list.ts | 46 +++++++++++++ .../src/api/handlers/staking/summary.ts | 4 +- atp-indexer/src/api/index.ts | 2 + atp-indexer/src/api/routes/rollup.routes.ts | 12 ++++ atp-indexer/src/api/types/index.ts | 1 + atp-indexer/src/api/types/rollup.types.ts | 15 +++++ atp-indexer/src/api/utils/canonical-rollup.ts | 31 +++++++++ atp-indexer/src/config/index.ts | 3 +- .../registry/canonical-rollup-updated.ts | 27 ++++++++ atp-indexer/src/utils/rollup.ts | 31 ++++++++- atp-indexer/terraform/app.tf | 6 +- atp-indexer/terraform/variables.tf | 9 ++- staking-dashboard/.env.example | 1 - staking-dashboard/bootstrap.sh | 10 +-- staking-dashboard/src/contracts/index.ts | 65 ++++++++++++++++++- staking-dashboard/src/main.tsx | 7 ++ staking-dashboard/vite.config.ts | 1 - 30 files changed, 389 insertions(+), 42 deletions(-) create mode 100644 atp-indexer/src/abis/registry.abi.ts create mode 100644 atp-indexer/src/api/handlers/rollup/list.ts create mode 100644 atp-indexer/src/api/routes/rollup.routes.ts create mode 100644 atp-indexer/src/api/types/rollup.types.ts create mode 100644 atp-indexer/src/api/utils/canonical-rollup.ts create mode 100644 atp-indexer/src/events/registry/canonical-rollup-updated.ts diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b564b09b7..cd7e80b24 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -52,7 +52,6 @@ jobs: VITE_ATP_REGISTRY_ADDRESS: "0x0000000000000000000000000000000000000003" VITE_ATP_REGISTRY_AUCTION_ADDRESS: "0x0000000000000000000000000000000000000004" VITE_STAKING_REGISTRY_ADDRESS: "0x0000000000000000000000000000000000000005" - VITE_ROLLUP_ADDRESS: "0x0000000000000000000000000000000000000006" VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS: "0x0000000000000000000000000000000000000007" VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS: "0x0000000000000000000000000000000000000007" VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS: "0x0000000000000000000000000000000000000007" diff --git a/.github/workflows/deploy-indexer.yaml b/.github/workflows/deploy-indexer.yaml index 8ffca00fc..3b553f47c 100644 --- a/.github/workflows/deploy-indexer.yaml +++ b/.github/workflows/deploy-indexer.yaml @@ -61,10 +61,11 @@ jobs: ATP_REGISTRY_ADDRESS: ${{ vars.ATP_REGISTRY_ADDRESS }} ATP_REGISTRY_AUCTION_ADDRESS: ${{ vars.ATP_REGISTRY_AUCTION_ADDRESS }} STAKING_REGISTRY_ADDRESS: ${{ vars.STAKING_REGISTRY_ADDRESS }} - ROLLUP_ADDRESS: ${{ vars.ROLLUP_ADDRESS }} + REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS }} START_BLOCK: ${{ vars.ATP_FACTORY_DEPLOYMENT_BLOCK }} MATP_FACTORY_START_BLOCK: ${{ vars.ATP_FACTORY_MATP_DEPLOYMENT_BLOCK }} LATP_FACTORY_START_BLOCK: ${{ vars.ATP_FACTORY_LATP_DEPLOYMENT_BLOCK }} + REGISTRY_START_BLOCK: ${{ vars.REGISTRY_DEPLOYMENT_BLOCK }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/deploy-staking-dashboard.yaml b/.github/workflows/deploy-staking-dashboard.yaml index 538827576..b98b5a7b8 100644 --- a/.github/workflows/deploy-staking-dashboard.yaml +++ b/.github/workflows/deploy-staking-dashboard.yaml @@ -62,7 +62,6 @@ jobs: VITE_ATP_REGISTRY_ADDRESS: ${{ vars.ATP_REGISTRY_ADDRESS }} VITE_ATP_REGISTRY_AUCTION_ADDRESS: ${{ vars.ATP_REGISTRY_AUCTION_ADDRESS }} VITE_STAKING_REGISTRY_ADDRESS: ${{ vars.STAKING_REGISTRY_ADDRESS }} - VITE_ROLLUP_ADDRESS: ${{ vars.ROLLUP_ADDRESS }} VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS: ${{ vars.ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS }} VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS: ${{ vars.ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS }} VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS: ${{ vars.ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS }} diff --git a/README.md b/README.md index 88385e515..a2a94dcb6 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,8 @@ Example `contract_addresses.json`: "atpRegistry": "0x...", "atpRegistryAuction": "0x...", "stakingRegistry": "0x...", - "rollupAddress": "0x...", + "registryAddress": "0x...", + "registryDeploymentBlock": "12345678", "atpWithdrawableAndClaimableStaker": "0x...", "genesisSequencerSale": "0x...", "governanceAddress": "0x...", @@ -103,6 +104,8 @@ Example `contract_addresses.json`: } ``` +The canonical rollup is no longer a separate configuration value, the indexer and frontend both resolve it dynamically from `Registry.getCanonicalRollup()`. Rollup upgrades (new `addRollup()` calls on the Registry) are picked up automatically: the indexer continues indexing every historical rollup via Ponder's factory pattern on the `CanonicalRollupUpdated` event, and the frontend re-resolves on every page load. + For production contract addresses, see the [Aztec documentation](https://docs.aztec.network/) or contact the Aztec team. ## Project Structure diff --git a/atp-indexer/.env.example b/atp-indexer/.env.example index 54b65d1d7..337d5f52c 100644 --- a/atp-indexer/.env.example +++ b/atp-indexer/.env.example @@ -13,13 +13,17 @@ ATP_FACTORY_AUCTION_ADDRESS=0x... ATP_FACTORY_MATP_ADDRESS=0x... ATP_FACTORY_LATP_ADDRESS=0x... STAKING_REGISTRY_ADDRESS=0x... -ROLLUP_ADDRESS=0x... +# Aztec Registry, used as factory source so the current AND all historical +# rollups are indexed automatically. On mainnet: 0x35b22e09Ee0390539439E24f06Da43D83f90e298 +REGISTRY_ADDRESS=0x... # Indexer Settings START_BLOCK=0 # Per-factory start blocks (optional, for efficiency - set to deployment block of each factory) MATP_FACTORY_START_BLOCK=0 LATP_FACTORY_START_BLOCK=0 +# Registry deployment block, factory backfill starts here (mainnet: 23786827) +REGISTRY_START_BLOCK=0 # Application NODE_ENV=development diff --git a/atp-indexer/bootstrap.sh b/atp-indexer/bootstrap.sh index 5a49f957a..06ee47ad3 100755 --- a/atp-indexer/bootstrap.sh +++ b/atp-indexer/bootstrap.sh @@ -50,10 +50,11 @@ get_contract_addresses() { ATP_REGISTRY_AUCTION_ADDRESS="${ATP_REGISTRY_AUCTION_ADDRESS:-}" ATP_FACTORY_AUCTION_ADDRESS="${ATP_FACTORY_AUCTION_ADDRESS:-}" STAKING_REGISTRY_ADDRESS="${STAKING_REGISTRY_ADDRESS:-}" - ROLLUP_ADDRESS="${ROLLUP_ADDRESS:-}" + REGISTRY_ADDRESS="${REGISTRY_ADDRESS:-}" START_BLOCK="${START_BLOCK:-0}" MATP_FACTORY_START_BLOCK="${MATP_FACTORY_START_BLOCK:-0}" LATP_FACTORY_START_BLOCK="${LATP_FACTORY_START_BLOCK:-0}" + REGISTRY_START_BLOCK="${REGISTRY_START_BLOCK:-0}" return 0 fi @@ -82,14 +83,16 @@ get_contract_addresses() { # other STAKING_REGISTRY_ADDRESS=$(cat $contract_addresses_file | jq -r '.stakingRegistry') - ROLLUP_ADDRESS=$(cat $contract_addresses_file | jq -r '.rollupAddress') + REGISTRY_ADDRESS=$(cat $contract_addresses_file | jq -r '.registryAddress') # For dev environment, use 0 to catch all events # For other environments, use atpFactoryDeploymentBlock as the starting point if [ "$environment" = "dev" ]; then START_BLOCK=0 + REGISTRY_START_BLOCK=0 else START_BLOCK=$(cat $contract_addresses_file | jq -r '.atpFactoryDeploymentBlock // 0') + REGISTRY_START_BLOCK=$(cat $contract_addresses_file | jq -r '.registryDeploymentBlock // 0') fi return 0 fi @@ -159,10 +162,11 @@ ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS} ATP_FACTORY_MATP_ADDRESS=${ATP_FACTORY_MATP_ADDRESS} ATP_FACTORY_LATP_ADDRESS=${ATP_FACTORY_LATP_ADDRESS} STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS} -ROLLUP_ADDRESS=${ROLLUP_ADDRESS} +REGISTRY_ADDRESS=${REGISTRY_ADDRESS} # Ponder settings START_BLOCK=${START_BLOCK} +REGISTRY_START_BLOCK=${REGISTRY_START_BLOCK} # API PORT=${PORT} @@ -198,7 +202,8 @@ ATP_FACTORY_AUCTION_ADDRESS=${ATP_FACTORY_AUCTION_ADDRESS} ATP_FACTORY_MATP_ADDRESS=${ATP_FACTORY_MATP_ADDRESS} ATP_FACTORY_LATP_ADDRESS=${ATP_FACTORY_LATP_ADDRESS} STAKING_REGISTRY_ADDRESS=${STAKING_REGISTRY_ADDRESS} -ROLLUP_ADDRESS=${ROLLUP_ADDRESS} +REGISTRY_ADDRESS=${REGISTRY_ADDRESS} +REGISTRY_START_BLOCK=${REGISTRY_START_BLOCK} # Indexer settings START_BLOCK=${DEFAULT_START_BLOCK} @@ -355,6 +360,7 @@ function deploy() { START_BLOCK="${START_BLOCK:-0}" MATP_FACTORY_START_BLOCK="${MATP_FACTORY_START_BLOCK:-0}" LATP_FACTORY_START_BLOCK="${LATP_FACTORY_START_BLOCK:-0}" + REGISTRY_START_BLOCK="${REGISTRY_START_BLOCK:-0}" # Initialize Terraform with the S3 backend (cd terraform && terraform init \ @@ -376,7 +382,8 @@ function deploy() { -var=atp_factory_matp_address=$ATP_FACTORY_MATP_ADDRESS \ -var=atp_factory_latp_address=$ATP_FACTORY_LATP_ADDRESS \ -var=staking_registry_address=$STAKING_REGISTRY_ADDRESS \ - -var=rollup_address=$ROLLUP_ADDRESS \ + -var=registry_address=$REGISTRY_ADDRESS \ + -var=registry_start_block=$REGISTRY_START_BLOCK \ -var=start_block=$START_BLOCK \ -var=matp_factory_start_block=$MATP_FACTORY_START_BLOCK \ -var=latp_factory_start_block=$LATP_FACTORY_START_BLOCK \ @@ -487,8 +494,8 @@ case $ACTION in echo " ATP_FACTORY_ADDRESS, ATP_FACTORY_AUCTION_ADDRESS" echo " ATP_FACTORY_MATP_ADDRESS, ATP_FACTORY_LATP_ADDRESS" echo " ATP_REGISTRY_ADDRESS, ATP_REGISTRY_AUCTION_ADDRESS" - echo " STAKING_REGISTRY_ADDRESS, ROLLUP_ADDRESS" - echo " START_BLOCK (optional, defaults to 0)" + echo " STAKING_REGISTRY_ADDRESS, REGISTRY_ADDRESS" + echo " START_BLOCK, REGISTRY_START_BLOCK (optional, defaults to 0)" echo "" echo " For production contract addresses, contact the Aztec team." ;; diff --git a/atp-indexer/ponder.config.ts b/atp-indexer/ponder.config.ts index 83326702b..93e8880f8 100644 --- a/atp-indexer/ponder.config.ts +++ b/atp-indexer/ponder.config.ts @@ -6,6 +6,7 @@ import { STAKING_REGISTRY_ABI, ROLLUP_ABI, STAKER_ABI, + REGISTRY_ABI, } from "./src/abis"; // ATPCreated event signature for factory pattern @@ -13,6 +14,13 @@ const ATPCreatedEvent = parseAbiItem( "event ATPCreated(address indexed beneficiary, address indexed atp, uint256 allocation)" ); +// Aztec Registry emits this on every rollup upgrade. Used as a factory source +// so every canonical rollup, historical and future, is indexed without a +// config change. +const CanonicalRollupUpdatedEvent = parseAbiItem( + "event CanonicalRollupUpdated(address indexed instance, uint256 indexed version)" +); + // Per-factory start blocks for efficient indexing const FACTORY_START_BLOCKS = { genesis: config.START_BLOCK || 0, @@ -111,14 +119,39 @@ export default createConfig({ }, /** - * Rollup Contract - * Handles validator deposits and tracks validator queue + * Aztec Registry + * Tracked as a regular contract so we can index CanonicalRollupUpdated + * events into a rollup_version table, served from /api/rollups so the + * frontend doesn't need its own Registry RPC calls. The same event is + * also used as a factory source for the Rollup contract below. + */ + Registry: { + chain: config.networkName, + abi: REGISTRY_ABI, + address: config.REGISTRY_ADDRESS as `0x${string}`, + startBlock: config.REGISTRY_START_BLOCK, + }, + + /** + * Rollup Contract (dynamic, sourced from the Aztec Registry) + * Handles validator deposits and tracks validator queue. + * + * Uses Ponder's factory pattern on the Registry's `CanonicalRollupUpdated` + * event so every rollup address the Registry has ever announced (historical + * versions and any future upgrade) is indexed automatically. Handlers in + * src/events/rollup/ fire for events from any of these addresses, so + * rewards/withdrawals/slashing on older rollups keep being tracked even + * after an upgrade. */ Rollup: { chain: config.networkName, abi: ROLLUP_ABI, - address: config.ROLLUP_ADDRESS as `0x${string}`, - startBlock: config.START_BLOCK, + address: factory({ + address: config.REGISTRY_ADDRESS as `0x${string}`, + event: CanonicalRollupUpdatedEvent, + parameter: "instance", + }), + startBlock: config.REGISTRY_START_BLOCK, }, /** diff --git a/atp-indexer/ponder.schema.ts b/atp-indexer/ponder.schema.ts index d9200aee5..f36f0bee9 100644 --- a/atp-indexer/ponder.schema.ts +++ b/atp-indexer/ponder.schema.ts @@ -425,3 +425,23 @@ export const tokensWithdrawnToBeneficiary = onchainTable("tokens_withdrawn_to_be atpAddressIdx: index().on(table.atpAddress), stakerAddressIdx: index().on(table.stakerAddress), })); + +/** + * RollupVersion + * Every rollup the Aztec Registry has made canonical via addRollup(). + * Populated from CanonicalRollupUpdated events; the latest row (by blockNumber) + * is the current canonical rollup. Used by /api/rollups so the frontend doesn't + * have to make its own Registry RPC calls at boot, and so historical rollup + * addresses are available for future cross-rollup claim flows. + */ +export const rollupVersion = onchainTable("rollup_version", (t) => ({ + version: t.bigint().primaryKey(), + address: t.hex().notNull(), + blockNumber: t.bigint().notNull(), + txHash: t.hex().notNull(), + logIndex: t.integer().notNull(), + timestamp: t.bigint().notNull(), +}), (table) => ({ + addressIdx: index().on(table.address), + blockNumberIdx: index().on(table.blockNumber), +})); diff --git a/atp-indexer/src/abis/index.ts b/atp-indexer/src/abis/index.ts index c0e0cf741..d2de6b721 100644 --- a/atp-indexer/src/abis/index.ts +++ b/atp-indexer/src/abis/index.ts @@ -40,3 +40,10 @@ export { TokensWithdrawnToBeneficiaryEventAbi, STAKER_ABI, } from './staker.abi'; + +// Registry ABIs (Aztec governance Registry: source of canonical rollup upgrades) +export { + CanonicalRollupUpdatedEventAbi, + REGISTRY_FUNCTIONS, + REGISTRY_ABI, +} from './registry.abi'; diff --git a/atp-indexer/src/abis/registry.abi.ts b/atp-indexer/src/abis/registry.abi.ts new file mode 100644 index 000000000..fbd1e3e81 --- /dev/null +++ b/atp-indexer/src/abis/registry.abi.ts @@ -0,0 +1,40 @@ +/** + * Aztec Registry Contract ABI + * + * The Registry tracks the canonical rollup instance over time. When a new + * rollup is deployed (e.g. on upgrade), `addRollup()` is called which emits + * `CanonicalRollupUpdated`. The indexer uses this event as a factory source + * so every rollup (past, present, and future) is indexed automatically. + */ + +export const CanonicalRollupUpdatedEventAbi = { + type: 'event', + name: 'CanonicalRollupUpdated', + inputs: [ + { name: 'instance', type: 'address', indexed: true }, + { name: 'version', type: 'uint256', indexed: true }, + ], + anonymous: false, +} as const; + +export const REGISTRY_FUNCTIONS = [ + { + type: 'function', + name: 'getCanonicalRollup', + inputs: [], + outputs: [{ name: '', type: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'numberOfVersions', + inputs: [], + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + }, +] as const; + +export const REGISTRY_ABI = [ + CanonicalRollupUpdatedEventAbi, + ...REGISTRY_FUNCTIONS, +] as const; diff --git a/atp-indexer/src/api/handlers/atp/details.ts b/atp-indexer/src/api/handlers/atp/details.ts index 2835ebe10..3100df41f 100644 --- a/atp-indexer/src/api/handlers/atp/details.ts +++ b/atp-indexer/src/api/handlers/atp/details.ts @@ -3,7 +3,7 @@ import { db } from 'ponder:api'; import { eq, desc, sql, or } from 'drizzle-orm'; import { normalizeAddress, checksumAddress } from '../../../utils/address'; import { getActivationThreshold } from '../../../utils/rollup'; -import { config } from '../../../config'; +import { getCanonicalRollupAddress } from '../../utils/canonical-rollup'; import { getPublicClient } from '../../../utils/viem-client'; import type { ATPDetailsResponse } from '../../types/atp.types'; import { fetchFailedDeposits, markStakesWithFailedDeposits } from '../../../utils/failed-deposits'; @@ -148,7 +148,8 @@ export async function handleATPDetails(c: Context): Promise { const validStakingOpsCount = markedDelegations.filter(isActiveStake).length; // Calculate total staked - const activationThreshold = await getActivationThreshold(config.ROLLUP_ADDRESS, client); + const rollupAddress = await getCanonicalRollupAddress(client); + const activationThreshold = await getActivationThreshold(rollupAddress, client); const totalStaked = BigInt(activationThreshold) * (BigInt(validDirectStakesCount) + BigInt(validStakingOpsCount)); // Query slashed table to get total slashed per attester address diff --git a/atp-indexer/src/api/handlers/provider/details.ts b/atp-indexer/src/api/handlers/provider/details.ts index 7bf48e63d..dd2812dc4 100644 --- a/atp-indexer/src/api/handlers/provider/details.ts +++ b/atp-indexer/src/api/handlers/provider/details.ts @@ -6,7 +6,7 @@ import { getProviderMetadata } from '../../../utils/provider-metadata'; import type { ProviderDetailsResponse } from '../../types/provider.types'; import { fetchFailedDeposits, filterValidStakes } from '../../../utils/failed-deposits'; import { getActivationThreshold } from '../../../utils/rollup'; -import { config } from '../../../config'; +import { getCanonicalRollupAddress } from '../../utils/canonical-rollup'; import { getPublicClient } from '../../../utils/viem-client'; import { provider, @@ -26,8 +26,9 @@ export async function handleProviderDetails(c: Context): Promise { const id = c.req.param('id'); const client = getPublicClient(); + const rollupAddress = await getCanonicalRollupAddress(client); const [activationThreshold, providerData, allAtpDelegationsCount, allErc20DelegationsCount, allDirectStakesCount, allFailedDepositCount] = await Promise.all([ - getActivationThreshold(config.ROLLUP_ADDRESS, client), + getActivationThreshold(rollupAddress, client), db.select().from(provider).where(eq(provider.providerIdentifier, id)).limit(1), db.select({ count: count() }).from(stakedWithProvider), db.select({ count: count() }).from(erc20StakedWithProvider), diff --git a/atp-indexer/src/api/handlers/provider/list.ts b/atp-indexer/src/api/handlers/provider/list.ts index 4c8a1a8fd..750325a11 100644 --- a/atp-indexer/src/api/handlers/provider/list.ts +++ b/atp-indexer/src/api/handlers/provider/list.ts @@ -6,7 +6,7 @@ import { getAllProviderMetadata } from '../../../utils/provider-metadata'; import type { ProviderListResponse } from '../../types/provider.types'; import { fetchFailedDeposits, markStakesWithFailedDeposits } from '../../../utils/failed-deposits'; import { getActivationThreshold } from '../../../utils/rollup'; -import { config } from '../../../config'; +import { getCanonicalRollupAddress } from '../../utils/canonical-rollup'; import { getPublicClient } from '../../../utils/viem-client'; import { provider, @@ -27,8 +27,9 @@ export async function handleProviderList(c: Context): Promise { // Get provider list of IDs from JSON const providerIds = Array.from(metadata.keys()); + const rollupAddress = await getCanonicalRollupAddress(client); const [activationThreshold, dbProviders, atpDelegations, erc20Delegations, allDirectStakes] = await Promise.all([ - getActivationThreshold(config.ROLLUP_ADDRESS, client), + getActivationThreshold(rollupAddress, client), db.select().from(provider).where(inArray(provider.providerIdentifier, providerIds)), db.select({ providerIdentifier: stakedWithProvider.providerIdentifier, diff --git a/atp-indexer/src/api/handlers/rollup/list.ts b/atp-indexer/src/api/handlers/rollup/list.ts new file mode 100644 index 000000000..c1eb10956 --- /dev/null +++ b/atp-indexer/src/api/handlers/rollup/list.ts @@ -0,0 +1,46 @@ +import type { Context } from 'hono'; +import { db } from 'ponder:api'; +import { asc } from 'drizzle-orm'; +import { rollupVersion } from 'ponder:schema'; +import { checksumAddress } from '../../../utils/address'; +import type { RollupListResponse } from '../../types/rollup.types'; + +/** + * Handle GET /api/rollups + * Returns the current canonical rollup plus every historical rollup the + * Registry has ever made canonical. Lets the frontend (and other clients) + * avoid making their own Registry RPC calls at boot, and gives the UI what + * it needs for future cross-rollup flows (e.g. claiming unclaimed rewards + * from a previous rollup). + * + * Source of truth: the rollup_version table, populated from + * Registry.CanonicalRollupUpdated events. + */ +export async function handleRollupList(c: Context): Promise { + try { + const rows = await db + .select() + .from(rollupVersion) + .orderBy(asc(rollupVersion.blockNumber)); + + const versions = rows.map(r => ({ + version: r.version.toString(), + address: checksumAddress(r.address), + blockNumber: Number(r.blockNumber), + timestamp: Number(r.timestamp), + })); + + const canonical = versions.length > 0 + ? versions[versions.length - 1].address + : null; + + const response: RollupListResponse = { canonical, versions }; + return c.json(response); + } catch (error) { + console.error('Error fetching rollup versions:', error); + return c.json({ + error: 'Failed to fetch rollup versions', + message: error instanceof Error ? error.message : String(error), + }, 500); + } +} diff --git a/atp-indexer/src/api/handlers/staking/summary.ts b/atp-indexer/src/api/handlers/staking/summary.ts index 5adb35c84..80221c53f 100644 --- a/atp-indexer/src/api/handlers/staking/summary.ts +++ b/atp-indexer/src/api/handlers/staking/summary.ts @@ -2,7 +2,7 @@ import type { Context } from 'hono'; import { db } from 'ponder:api'; import { count } from 'drizzle-orm'; import { getActivationThreshold, calculateAPR } from '../../../utils/rollup'; -import { config } from '../../../config'; +import { getCanonicalRollupAddress } from '../../utils/canonical-rollup'; import { getPublicClient } from '../../../utils/viem-client'; import type { StakingSummaryResponse } from '../../types/staking.types'; import { @@ -23,7 +23,7 @@ import { export async function handleStakingSummary(c: Context): Promise { try { const client = getPublicClient(); - const rollupAddress = config.ROLLUP_ADDRESS; + const rollupAddress = await getCanonicalRollupAddress(client); // Data model explanation: // - `deposit` table: ALL Rollup:Deposit events (validator registrations on-chain) diff --git a/atp-indexer/src/api/index.ts b/atp-indexer/src/api/index.ts index 40a61972f..7d669275a 100644 --- a/atp-indexer/src/api/index.ts +++ b/atp-indexer/src/api/index.ts @@ -6,6 +6,7 @@ import { healthRoutes } from './routes/health.routes'; import { providerRoutes } from './routes/provider.routes'; import { stakingRoutes } from './routes/staking.routes'; import { atpRoutes } from './routes/atp.routes'; +import { rollupRoutes } from './routes/rollup.routes'; import { config } from '../config'; /** @@ -32,6 +33,7 @@ app.route('/api/health', healthRoutes); app.route('/api/providers', providerRoutes); app.route('/api/staking', stakingRoutes); app.route('/api/atp', atpRoutes); +app.route('/api/rollups', rollupRoutes); app.notFound((c) => { return c.json({ error: 'Not found' }, 404); diff --git a/atp-indexer/src/api/routes/rollup.routes.ts b/atp-indexer/src/api/routes/rollup.routes.ts new file mode 100644 index 000000000..ca0c9bf6d --- /dev/null +++ b/atp-indexer/src/api/routes/rollup.routes.ts @@ -0,0 +1,12 @@ +import { Hono } from 'hono'; +import { handleRollupList } from '../handlers/rollup/list'; +import { pollingLimiter } from '../middleware/rate-limit'; + +export const rollupRoutes = new Hono(); + +/** + * GET /api/rollups + * List the current canonical rollup + every historical rollup the Registry + * has ever made canonical. + */ +rollupRoutes.get('/', pollingLimiter, handleRollupList); diff --git a/atp-indexer/src/api/types/index.ts b/atp-indexer/src/api/types/index.ts index 549ab31e9..e9d45a7d4 100644 --- a/atp-indexer/src/api/types/index.ts +++ b/atp-indexer/src/api/types/index.ts @@ -5,4 +5,5 @@ export * from './provider.types'; export * from './atp.types'; export * from './staking.types'; +export * from './rollup.types'; export * from './common.types'; diff --git a/atp-indexer/src/api/types/rollup.types.ts b/atp-indexer/src/api/types/rollup.types.ts new file mode 100644 index 000000000..08ff9475b --- /dev/null +++ b/atp-indexer/src/api/types/rollup.types.ts @@ -0,0 +1,15 @@ +/** + * Rollup API Response Types + */ + +export interface RollupVersionRow { + version: string; // Registry version id (uint256, stringified) + address: string; // Checksummed rollup address + blockNumber: number; // Block at which this rollup became canonical + timestamp: number; // Block timestamp (unix seconds) +} + +export interface RollupListResponse { + canonical: string | null; // Latest canonical rollup address (null before first event is indexed) + versions: RollupVersionRow[]; // Every rollup ever made canonical, oldest first +} diff --git a/atp-indexer/src/api/utils/canonical-rollup.ts b/atp-indexer/src/api/utils/canonical-rollup.ts new file mode 100644 index 000000000..307b2931b --- /dev/null +++ b/atp-indexer/src/api/utils/canonical-rollup.ts @@ -0,0 +1,31 @@ +import { db } from "ponder:api"; +import { rollupVersion } from "ponder:schema"; +import { desc } from "drizzle-orm"; +import type { Address, PublicClient } from "viem"; +import { getCanonicalRollupFromRegistry } from "../../utils/rollup"; + +/** + * Resolve the current canonical rollup address for API handlers. + * + * Preferred path: read the most recent row from the rollup_version table, + * which is populated by the Registry:CanonicalRollupUpdated handler. That's + * the indexer's own source of truth for canonical rollup upgrades, so there's + * no reason to re-query the chain on every API request. + * + * Fallback path: live Registry RPC call (cached 60s). Only reached during the + * brief window after a fresh sync when the indexer hasn't yet processed the + * first CanonicalRollupUpdated event. + */ +export async function getCanonicalRollupAddress(client: PublicClient): Promise
{ + const latest = await db + .select() + .from(rollupVersion) + .orderBy(desc(rollupVersion.blockNumber)) + .limit(1); + + if (latest.length > 0) { + return latest[0].address as Address; + } + + return getCanonicalRollupFromRegistry(client); +} diff --git a/atp-indexer/src/config/index.ts b/atp-indexer/src/config/index.ts index 97111f554..5a28ee5ab 100644 --- a/atp-indexer/src/config/index.ts +++ b/atp-indexer/src/config/index.ts @@ -51,12 +51,13 @@ const configSchema = z.object({ ATP_FACTORY_MATP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), ATP_FACTORY_LATP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), STAKING_REGISTRY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), - ROLLUP_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), + REGISTRY_ADDRESS: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address format'), // Indexer settings START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'START_BLOCK must be non-negative').default('0'), MATP_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'MATP_FACTORY_START_BLOCK must be non-negative').optional(), LATP_FACTORY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'LATP_FACTORY_START_BLOCK must be non-negative').optional(), + REGISTRY_START_BLOCK: z.string().transform(Number).refine(n => n >= 0, 'REGISTRY_START_BLOCK must be non-negative').default('0'), // Application NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), diff --git a/atp-indexer/src/events/registry/canonical-rollup-updated.ts b/atp-indexer/src/events/registry/canonical-rollup-updated.ts new file mode 100644 index 000000000..c57ad4ff1 --- /dev/null +++ b/atp-indexer/src/events/registry/canonical-rollup-updated.ts @@ -0,0 +1,27 @@ +import { ponder } from "ponder:registry"; +import { rollupVersion } from "ponder:schema"; +import { normalizeAddress } from "../../utils/address"; + +/** + * Handle CanonicalRollupUpdated event from the Aztec Registry. + * Every canonical rollup upgrade goes through Registry.addRollup(), which + * emits this event. We record it so /api/rollups can expose current + history + * without the frontend or API handlers making their own Registry RPC calls. + */ +ponder.on("Registry:CanonicalRollupUpdated", async ({ event, context }) => { + const { instance, version } = event.args; + const { db } = context; + + await db.insert(rollupVersion).values({ + version, + address: normalizeAddress(instance) as `0x${string}`, + blockNumber: event.block.number, + txHash: event.transaction.hash, + logIndex: event.log.logIndex, + timestamp: event.block.timestamp, + }); + + console.log( + `Canonical rollup updated: version ${version}, instance ${instance}` + ); +}); diff --git a/atp-indexer/src/utils/rollup.ts b/atp-indexer/src/utils/rollup.ts index 5f5864bb0..e891abd5f 100644 --- a/atp-indexer/src/utils/rollup.ts +++ b/atp-indexer/src/utils/rollup.ts @@ -1,4 +1,4 @@ -import { ROLLUP_ABI, ROLLUP_FUNCTIONS } from "../abis"; +import { ROLLUP_ABI, ROLLUP_FUNCTIONS, REGISTRY_ABI } from "../abis"; import { config } from "../config"; import type { PublicClient, Address } from 'viem'; @@ -7,6 +7,35 @@ let cachedAPR: number | null = null; let cacheTimestamp = 0; const CACHE_TTL = 0.5 * 60 * 1000; // 30s +// Canonical rollup address cache. Upgrades are rare; a short TTL means we +// pick up a new canonical rollup on the next request after an upgrade +// without an indexer restart. +let cachedCanonicalRollup: Address | null = null; +let cachedCanonicalRollupAt = 0; +const CANONICAL_ROLLUP_TTL = 60 * 1000; // 60s + +/** + * Get the current canonical rollup address directly from the Registry via RPC. + * This is the fallback path. API consumers should prefer the indexed + * rollup_version table (see src/api/utils/canonical-rollup.ts). Used when the + * indexer hasn't yet processed the first CanonicalRollupUpdated event. + */ +export async function getCanonicalRollupFromRegistry(client: PublicClient): Promise
{ + if (cachedCanonicalRollup && Date.now() - cachedCanonicalRollupAt < CANONICAL_ROLLUP_TTL) { + return cachedCanonicalRollup; + } + + const rollup = await client.readContract({ + address: config.REGISTRY_ADDRESS as Address, + abi: REGISTRY_ABI, + functionName: 'getCanonicalRollup', + }) as Address; + + cachedCanonicalRollup = rollup; + cachedCanonicalRollupAt = Date.now(); + return rollup; +} + /** * Get activation threshold from Rollup contract */ diff --git a/atp-indexer/terraform/app.tf b/atp-indexer/terraform/app.tf index 9966e6893..a0bcc1f16 100644 --- a/atp-indexer/terraform/app.tf +++ b/atp-indexer/terraform/app.tf @@ -259,7 +259,8 @@ locals { { name = "ATP_FACTORY_AUCTION_ADDRESS", value = var.atp_factory_auction_address }, { name = "ATP_FACTORY_MATP_ADDRESS", value = var.atp_factory_matp_address }, { name = "ATP_FACTORY_LATP_ADDRESS", value = var.atp_factory_latp_address }, - { name = "ROLLUP_ADDRESS", value = var.rollup_address }, + { name = "REGISTRY_ADDRESS", value = var.registry_address }, + { name = "REGISTRY_START_BLOCK", value = var.registry_start_block }, { name = "POLLING_INTERVAL", value = var.polling_interval }, { name = "MAX_RETRIES", value = var.max_retries }, { name = "PARALLEL_BATCHES", value = var.parallel_batches }, @@ -288,7 +289,8 @@ locals { { name = "ATP_FACTORY_AUCTION_ADDRESS", value = var.atp_factory_auction_address }, { name = "ATP_FACTORY_MATP_ADDRESS", value = var.atp_factory_matp_address }, { name = "ATP_FACTORY_LATP_ADDRESS", value = var.atp_factory_latp_address }, - { name = "ROLLUP_ADDRESS", value = var.rollup_address }, + { name = "REGISTRY_ADDRESS", value = var.registry_address }, + { name = "REGISTRY_START_BLOCK", value = var.registry_start_block }, { name = "POLLING_INTERVAL", value = var.polling_interval }, { name = "MAX_RETRIES", value = var.max_retries }, { name = "PARALLEL_BATCHES", value = var.parallel_batches }, diff --git a/atp-indexer/terraform/variables.tf b/atp-indexer/terraform/variables.tf index b176b13ca..97efe9533 100644 --- a/atp-indexer/terraform/variables.tf +++ b/atp-indexer/terraform/variables.tf @@ -144,8 +144,13 @@ variable "staking_registry_address" { type = string } -variable "rollup_address" { - description = "Rollup contract address" +variable "registry_address" { + description = "Aztec Registry contract address. The indexer uses it as a factory source to pick up all rollup upgrades automatically." + type = string +} + +variable "registry_start_block" { + description = "Block number the Registry was deployed at (factory backfill starts here)." type = string } diff --git a/staking-dashboard/.env.example b/staking-dashboard/.env.example index 54e1dabf0..87d4e0c68 100644 --- a/staking-dashboard/.env.example +++ b/staking-dashboard/.env.example @@ -7,7 +7,6 @@ VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS=0x0000000000000000000000000000000000000 VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS=0x0000000000000000000000000000000000000000 VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS=0x0000000000000000000000000000000000000000 VITE_STAKING_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000000 -VITE_ROLLUP_ADDRESS=0x0000000000000000000000000000000000000000 VITE_GENESIS_SEQUENCER_SALE_ADDRESS=0x0000000000000000000000000000000000000000 VITE_GOVERNANCE_ADDRESS=0x0000000000000000000000000000000000000000 VITE_GSE_ADDRESS=0x0000000000000000000000000000000000000000 diff --git a/staking-dashboard/bootstrap.sh b/staking-dashboard/bootstrap.sh index bf300d340..2244070fd 100755 --- a/staking-dashboard/bootstrap.sh +++ b/staking-dashboard/bootstrap.sh @@ -56,7 +56,6 @@ load_contract_addresses() { VITE_ATP_REGISTRY_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpRegistry') VITE_ATP_REGISTRY_AUCTION_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpRegistryAuction') VITE_STAKING_REGISTRY_ADDRESS=$(cat $contract_addresses_file | jq -r '.stakingRegistry') - VITE_ROLLUP_ADDRESS=$(cat $contract_addresses_file | jq -r '.rollupAddress') VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpWithdrawableAndClaimableStaker') VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpWithdrawableAndClaimableStaker') VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS=$(cat $contract_addresses_file | jq -r '.atpWithdrawableAndClaimableStaker') @@ -154,9 +153,6 @@ function update_env_file() { log_step "Updating VITE_STAKING_REGISTRY_ADDRESS: $VITE_STAKING_REGISTRY_ADDRESS" update_env_var $env_file "VITE_STAKING_REGISTRY_ADDRESS" "$VITE_STAKING_REGISTRY_ADDRESS" - log_step "Updating VITE_ROLLUP_ADDRESS: $VITE_ROLLUP_ADDRESS" - update_env_var $env_file "VITE_ROLLUP_ADDRESS" "$VITE_ROLLUP_ADDRESS" - log_step "Updating VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS: $VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS" update_env_var $env_file "VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS" "$VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS" @@ -261,7 +257,6 @@ VITE_ATP_FACTORY_AUCTION_ADDRESS=$VITE_ATP_FACTORY_AUCTION_ADDRESS VITE_ATP_REGISTRY_ADDRESS=$VITE_ATP_REGISTRY_ADDRESS VITE_ATP_REGISTRY_AUCTION_ADDRESS=$VITE_ATP_REGISTRY_AUCTION_ADDRESS VITE_STAKING_REGISTRY_ADDRESS=$VITE_STAKING_REGISTRY_ADDRESS -VITE_ROLLUP_ADDRESS=$VITE_ROLLUP_ADDRESS VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS=$VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS=$VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS VITE_GENESIS_SEQUENCER_SALE_ADDRESS=$VITE_GENESIS_SEQUENCER_SALE_ADDRESS @@ -316,8 +311,6 @@ function update_env_file_deploy() { update_env_var "$WEBSITE_ROOT/.env.$environment" "VITE_ATP_REGISTRY_AUCTION_ADDRESS" $VITE_ATP_REGISTRY_AUCTION_ADDRESS log_step "Updating VITE_STAKING_REGISTRY_ADDRESS" update_env_var "$WEBSITE_ROOT/.env.$environment" "VITE_STAKING_REGISTRY_ADDRESS" $VITE_STAKING_REGISTRY_ADDRESS - log_step "Updating VITE_ROLLUP_ADDRESS" - update_env_var "$WEBSITE_ROOT/.env.$environment" "VITE_ROLLUP_ADDRESS" $VITE_ROLLUP_ADDRESS log_step "Updating VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS" update_env_var "$WEBSITE_ROOT/.env.$environment" "VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS" $VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS log_step "Updating VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS" @@ -544,7 +537,8 @@ case $ACTION in echo " \"atpRegistry\": \"0x...\"," echo " \"atpRegistryAuction\": \"0x...\"," echo " \"stakingRegistry\": \"0x...\"," - echo " \"rollupAddress\": \"0x...\"," + echo " \"registryAddress\": \"0x...\"," + echo " \"registryDeploymentBlock\": \"12345678\"," echo " \"atpWithdrawableAndClaimableStaker\": \"0x...\"," echo " \"genesisSequencerSale\": \"0x...\"," echo " \"governanceAddress\": \"0x...\"," diff --git a/staking-dashboard/src/contracts/index.ts b/staking-dashboard/src/contracts/index.ts index 5dac2bd64..dd363e800 100644 --- a/staking-dashboard/src/contracts/index.ts +++ b/staking-dashboard/src/contracts/index.ts @@ -24,7 +24,6 @@ const contractEnvSchema = z.object({ VITE_ATP_REGISTRY_ADDRESS: addressSchema, VITE_ATP_REGISTRY_AUCTION_ADDRESS: addressSchema, VITE_STAKING_REGISTRY_ADDRESS: addressSchema, - VITE_ROLLUP_ADDRESS: addressSchema, VITE_GENESIS_SEQUENCER_SALE_ADDRESS: addressSchema.optional(), VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS: addressSchema, VITE_GOVERNANCE_ADDRESS: addressSchema, @@ -34,6 +33,61 @@ const contractEnvSchema = z.object({ // Validate eagerly at startup const env = contractEnvSchema.parse(import.meta.env); +/** + * A rollup version record as returned by GET /api/rollups. + * Ordered oldest first in the API response. + */ +export interface RollupVersion { + version: string; + address: Address; + blockNumber: number; + timestamp: number; +} + +// The canonical rollup is resolved at boot from the indexer's /api/rollups +// endpoint (backed by the Registry:CanonicalRollupUpdated event). We also +// cache the full version history for future cross-rollup flows (e.g. +// claiming rewards from an old rollup). Callers still read +// `contracts.rollup.address` synchronously; `initRollupVersions()` is +// awaited in main.tsx before React renders so the getter always returns a +// real address. +let _canonicalRollupAddress: Address | null = null; +let _rollupVersions: RollupVersion[] = []; + +export async function initRollupVersions(): Promise
{ + const apiHost = import.meta.env.VITE_API_HOST; + if (!apiHost) { + throw new Error("VITE_API_HOST must be set to resolve the canonical rollup"); + } + + const res = await fetch(`${apiHost}/api/rollups`); + if (!res.ok) { + throw new Error(`/api/rollups returned ${res.status}`); + } + const body = (await res.json()) as { + canonical: string | null; + versions: RollupVersion[]; + }; + + if (!body.canonical || body.versions.length === 0) { + throw new Error( + "Indexer has not yet recorded a canonical rollup; the Registry:CanonicalRollupUpdated event has not been processed. Wait for the indexer to catch up past the Registry deployment block." + ); + } + + _rollupVersions = body.versions; + _canonicalRollupAddress = body.canonical as Address; + return _canonicalRollupAddress; +} + +/** + * All rollup versions the Registry has ever made canonical, oldest first. + * Empty until `initRollupVersions()` resolves. + */ +export function getRollupVersions(): readonly RollupVersion[] { + return _rollupVersions; +} + const contracts = { atpFactory: { address: env.VITE_ATP_FACTORY_ADDRESS, @@ -56,7 +110,14 @@ const contracts = { abi: StakingRegistryAbi, }, rollup: { - address: env.VITE_ROLLUP_ADDRESS, + get address(): Address { + if (!_canonicalRollupAddress) { + throw new Error( + "Canonical rollup address not initialized: initRollupVersions() must be awaited before app render" + ); + } + return _canonicalRollupAddress; + }, abi: RollupAbi, }, genesisSequencerSale: { diff --git a/staking-dashboard/src/main.tsx b/staking-dashboard/src/main.tsx index 9de63495c..1977347e6 100644 --- a/staking-dashboard/src/main.tsx +++ b/staking-dashboard/src/main.tsx @@ -16,9 +16,16 @@ import { AlertProvider } from "./contexts/AlertContext.tsx"; import { TermsModalProvider } from "./contexts/TermsModalContext.tsx"; import { CustomAvatar } from "./components/CustomAvatar/CustomAvatar.tsx"; import { Alert } from "./components/Alert"; +import { initRollupVersions } from "./contracts"; const queryClient = new QueryClient(); +// Resolve the canonical rollup (and historical rollup versions) from the +// indexer's /api/rollups endpoint before rendering, so every downstream hook +// can read contracts.rollup.address synchronously. Rollup upgrades no longer +// require a code change: just a page reload. +await initRollupVersions(); + createRoot(document.getElementById("root")!).render( diff --git a/staking-dashboard/vite.config.ts b/staking-dashboard/vite.config.ts index 105bac0d6..cd1d2d37d 100644 --- a/staking-dashboard/vite.config.ts +++ b/staking-dashboard/vite.config.ts @@ -17,7 +17,6 @@ export default defineConfig(({ mode, command }) => { 'VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS', 'VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS', 'VITE_STAKING_REGISTRY_ADDRESS', - 'VITE_ROLLUP_ADDRESS', 'VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS', 'VITE_SAFE_API_KEY', 'VITE_RPC_URL',