From 761497dd20242619e3c3d4d2dea79bda0575a5b2 Mon Sep 17 00:00:00 2001 From: Nico Chamo Date: Thu, 16 Jul 2026 20:15:52 -0300 Subject: [PATCH 1/2] refactor(txe): replace AztecNodeService with TXE-local node --- yarn-project/txe/esbuild.config.mjs | 48 +- .../stubs/aztec_node_node_metrics_stub.ts | 20 - .../stubs/aztec_node_sentinel_factory_stub.ts | 5 - .../esbuild/stubs/aztec_node_sentinel_stub.ts | 7 - .../txe/esbuild/stubs/bb_prover_test_stub.ts | 2 - yarn-project/txe/package.json | 2 - .../txe/src/state_machine/archiver.ts | 11 +- .../txe/src/state_machine/dummy_p2p_client.ts | 260 --------- .../state_machine/global_variable_builder.ts | 39 -- yarn-project/txe/src/state_machine/index.ts | 41 +- .../txe/src/state_machine/mock_epoch_cache.ts | 107 ---- yarn-project/txe/src/state_machine/node.ts | 517 ++++++++++++++++++ .../src/state_machine/unimplemented_node.ts | 345 ++++++++++++ yarn-project/txe/tsconfig.json | 6 - yarn-project/yarn.lock | 2 - 15 files changed, 882 insertions(+), 530 deletions(-) delete mode 100644 yarn-project/txe/esbuild/stubs/aztec_node_node_metrics_stub.ts delete mode 100644 yarn-project/txe/esbuild/stubs/aztec_node_sentinel_factory_stub.ts delete mode 100644 yarn-project/txe/esbuild/stubs/aztec_node_sentinel_stub.ts delete mode 100644 yarn-project/txe/esbuild/stubs/bb_prover_test_stub.ts delete mode 100644 yarn-project/txe/src/state_machine/dummy_p2p_client.ts delete mode 100644 yarn-project/txe/src/state_machine/global_variable_builder.ts delete mode 100644 yarn-project/txe/src/state_machine/mock_epoch_cache.ts create mode 100644 yarn-project/txe/src/state_machine/node.ts create mode 100644 yarn-project/txe/src/state_machine/unimplemented_node.ts diff --git a/yarn-project/txe/esbuild.config.mjs b/yarn-project/txe/esbuild.config.mjs index 46be5f765882..3caa92d52ee1 100644 --- a/yarn-project/txe/esbuild.config.mjs +++ b/yarn-project/txe/esbuild.config.mjs @@ -17,27 +17,16 @@ import { redirectsPlugin } from './esbuild/plugins/redirects.mjs'; import { enforceSizeLimits } from './esbuild/plugins/size_guard.mjs'; import { stripArtifactDebugPlugin } from './esbuild/plugins/strip_artifact_debug.mjs'; -// `@aztec/*` packages that AztecNodeService imports transitively but the TXE worker never -// actually constructs or executes — sequencer/validator/prover/etc. are passed in as `undefined`, -// so the imported symbols are only used inside AztecNodeService methods TXE never calls. The -// Proxy-based `empty_stub.cjs` throws on access, surfacing any false assumption loudly. +// `@aztec/*` packages that `@aztec/archiver` imports transitively but the TXE worker never +// actually constructs or executes — TXEArchiver only uses the archiver's data-store layer, so the +// L1-facing symbols are never reached. The Proxy-based `empty_stub.cjs` throws on access, +// surfacing any false assumption loudly. // // NOTE: `@aztec/archiver` is intentionally NOT stubbed — TXEArchiver extends ArchiverDataSourceBase // and uses createArchiverDataStores at runtime. // NOTE: `@aztec/blob-lib` is NOT stubbed — stdlib's tx_effect calls getNumTxBlobFields at runtime // when serializing transactions. -const fullyStubbedAztecPackages = [ - '@aztec/p2p', - '@aztec/sequencer-client', - '@aztec/validator-client', - '@aztec/prover-client', - '@aztec/prover-node', - '@aztec/slasher', - '@aztec/epoch-cache', - '@aztec/blob-client', - '@aztec/node-keystore', - '@aztec/node-lib', -]; +const fullyStubbedAztecPackages = ['@aztec/epoch-cache', '@aztec/blob-client']; // Each row redirects a specifier match to a stub file under `esbuild/stubs/`. The local stub // re-exports only the surface the bundled code reaches at runtime, or throws on call for code @@ -60,7 +49,6 @@ const redirects = [ { filter: /^@aztec\/protocol-contracts\/providers\/bundle$/, stub: 'protocol_contracts_bundle_stub.ts' }, { filter: /^@aztec\/archiver$/, stub: 'archiver_stub.ts' }, { filter: /^@aztec\/bb-prover$/, stub: 'bb_prover_stub.ts' }, - { filter: /^@aztec\/bb-prover\/test$/, stub: 'bb_prover_test_stub.ts' }, { filter: /^@aztec\/world-state$/, stub: 'world_state_stub.ts' }, { filter: /^@noble\/curves\/secp256k1$/, stub: 'noble_secp256k1_stub.ts' }, { filter: /^@noble\/curves\/bls12-381$/, stub: 'noble_bls12_stub.ts' }, @@ -69,28 +57,10 @@ const redirects = [ // otherwise pull in heavy unused code, but we can't wholesale stub the package (it has live // siblings TXE still needs). // - // - aztec-node/.../node_metrics.js: constructed by AztecNodeService but never observed - // because TXE's meter is the Noop client. - // - aztec-node/.../sentinel/{factory,sentinel}.js: only reached via `AztecNodeService.start()`, - // which TXE never invokes. // - telemetry-client/.../start.js: contains `await import('./otel.js')` which forces emission // of a large OpenTelemetry SDK chunk even though TXE never starts telemetry. // - zod/.../locales/index.js: barrel re-exports every i18n bundle; TXE only needs `en`. - { - filter: /^\.\/node_metrics\.(js|ts)$/, - importerContains: '/aztec-node/', - stub: 'aztec_node_node_metrics_stub.ts', - }, - { - filter: /^\.\.\/sentinel\/factory\.(js|ts)$/, - importerContains: '/aztec-node/', - stub: 'aztec_node_sentinel_factory_stub.ts', - }, - { - filter: /^\.\.\/sentinel\/sentinel\.(js|ts)$/, - importerContains: '/aztec-node/', - stub: 'aztec_node_sentinel_stub.ts', - }, + // // start.js arrives as a resolved absolute path from outside the package… { filter: /telemetry-client\/(dest|src)\/start\.(js|ts)$/, stub: 'telemetry_start_stub.ts' }, // …and as a relative `./start.js` from siblings inside the package. @@ -174,9 +144,9 @@ const result = await build({ // bundled), viem, abitype, and the rest of the L1 client surface. '@aztec/ethereum', '@aztec/l1-artifacts', - // Same reasoning: stdlib/p2p/signature_utils, archiver/l1, and aztec-node/config all - // statically import `viem` for L1 RPC + signing primitives. None of those code paths execute - // in TXE, so externalize the whole tree. + // Same reasoning: stdlib/p2p/signature_utils and archiver/l1 statically import `viem` for + // L1 RPC + signing primitives. Neither code path executes in TXE, so externalize the whole + // tree. 'viem', 'abitype', 'ox', diff --git a/yarn-project/txe/esbuild/stubs/aztec_node_node_metrics_stub.ts b/yarn-project/txe/esbuild/stubs/aztec_node_node_metrics_stub.ts deleted file mode 100644 index c43cbdea56ca..000000000000 --- a/yarn-project/txe/esbuild/stubs/aztec_node_node_metrics_stub.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { throwStub } from './stub_helpers.js'; - -export class NodeMetrics { - // The constructor runs whenever an AztecNodeService is built, so it must stay a no-op. The - // recording methods below are only reached via sendTx / startSnapshotUpload, neither of which the - // TXE node exercises, so they throw if ever called. - constructor(_client: unknown, _name?: string) {} - - receivedTx(_durationMs: number, _isAccepted: boolean): void { - throwStub('NodeMetrics.receivedTx'); - } - - recordSnapshot(_durationMs: number): void { - throwStub('NodeMetrics.recordSnapshot'); - } - - recordSnapshotError(): void { - throwStub('NodeMetrics.recordSnapshotError'); - } -} diff --git a/yarn-project/txe/esbuild/stubs/aztec_node_sentinel_factory_stub.ts b/yarn-project/txe/esbuild/stubs/aztec_node_sentinel_factory_stub.ts deleted file mode 100644 index 564e8a7fefe9..000000000000 --- a/yarn-project/txe/esbuild/stubs/aztec_node_sentinel_factory_stub.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { throwStub } from './stub_helpers.js'; - -export function createSentinel(..._args: unknown[]): never { - throwStub('createSentinel'); -} diff --git a/yarn-project/txe/esbuild/stubs/aztec_node_sentinel_stub.ts b/yarn-project/txe/esbuild/stubs/aztec_node_sentinel_stub.ts deleted file mode 100644 index 35b62d412303..000000000000 --- a/yarn-project/txe/esbuild/stubs/aztec_node_sentinel_stub.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { throwStub } from './stub_helpers.js'; - -export class Sentinel { - constructor(..._args: unknown[]) { - throwStub('Sentinel'); - } -} diff --git a/yarn-project/txe/esbuild/stubs/bb_prover_test_stub.ts b/yarn-project/txe/esbuild/stubs/bb_prover_test_stub.ts deleted file mode 100644 index c284ff0b52ea..000000000000 --- a/yarn-project/txe/esbuild/stubs/bb_prover_test_stub.ts +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable no-restricted-imports, import-x/no-relative-packages */ -export { TestCircuitVerifier } from '../../../bb-prover/dest/test/test_verifier.js'; diff --git a/yarn-project/txe/package.json b/yarn-project/txe/package.json index a72490699fe7..022559bfd807 100644 --- a/yarn-project/txe/package.json +++ b/yarn-project/txe/package.json @@ -71,9 +71,7 @@ "dependencies": { "@aztec/accounts": "workspace:^", "@aztec/archiver": "workspace:^", - "@aztec/aztec-node": "workspace:^", "@aztec/aztec.js": "workspace:^", - "@aztec/bb-prover": "workspace:^", "@aztec/bb.js": "workspace:^", "@aztec/constants": "workspace:^", "@aztec/foundation": "workspace:^", diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index c91789f7d3bf..37d0024b1b87 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -36,6 +36,13 @@ export class TXEArchiver extends ArchiverDataSourceBase { public async addCheckpoints(checkpoints: PublishedCheckpoint[], result?: ValidateCheckpointResult): Promise { await this.updater.addCheckpoints(checkpoints, result); + // The TXE has no L1 and no proving, so every checkpoint is proven and finalized the moment it is added. Advancing + // the store markers keeps store-backed tag resolution ('proven'/'finalized') consistent with getL2Tips below. + const last = checkpoints.at(-1); + if (last) { + await this.updater.setProvenCheckpointNumber(last.checkpoint.number); + await this.updater.setFinalizedCheckpointNumber(last.checkpoint.number); + } } public getRollupAddress(): Promise { @@ -47,8 +54,8 @@ export class TXEArchiver extends ArchiverDataSourceBase { } public getL1Constants(): Promise { - // The TXE has no L1, so it serves the empty constants (epochDuration=1) used throughout the TXE state machine - // (see mock_epoch_cache). The node calls this when assembling a mined tx receipt to derive the epoch number. + // The TXE has no L1, so it serves the empty constants (epochDuration=1). The node calls this when assembling + // a mined tx receipt to derive the epoch number. return Promise.resolve(EmptyL1RollupConstants); } diff --git a/yarn-project/txe/src/state_machine/dummy_p2p_client.ts b/yarn-project/txe/src/state_machine/dummy_p2p_client.ts deleted file mode 100644 index c2130a756aad..000000000000 --- a/yarn-project/txe/src/state_machine/dummy_p2p_client.ts +++ /dev/null @@ -1,260 +0,0 @@ -import type { CheckpointProposalHash, SlotNumber } from '@aztec/foundation/branded-types'; -import type { - AuthRequest, - ENR, - P2P, - P2PBlockReceivedCallback, - P2PCheckpointAttestationCallback, - P2PCheckpointReceivedCallback, - P2PConfig, - P2PDuplicateAttestationCallback, - P2PDuplicateProposalCallback, - P2POversizedProposalCallback, - P2PSyncState, - PeerId, - ReqRespSubProtocol, - ReqRespSubProtocolHandler, - StatusMessage, -} from '@aztec/p2p'; -import type { EthAddress, L2BlockStreamEvent, L2Tips } from '@aztec/stdlib/block'; -import type { ITxProvider, PeerInfo } from '@aztec/stdlib/interfaces/server'; -import type { - BlockProposal, - CheckpointAttestation, - CheckpointProposal, - CheckpointProposalCore, - TopicType, -} from '@aztec/stdlib/p2p'; -import type { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx'; - -export class DummyP2P implements P2P { - public validateTxsReceivedInBlockProposal(_txs: Tx[]): Promise { - return Promise.resolve(); - } - - public clear(): Promise { - throw new Error('DummyP2P does not implement "clear".'); - } - - public getPendingTxs(): Promise { - throw new Error('DummyP2P does not implement "getPendingTxs"'); - } - - public getEncodedEnr(): Promise { - throw new Error('DummyP2P does not implement "getEncodedEnr"'); - } - - public getPeers(_includePending?: boolean): Promise { - throw new Error('DummyP2P does not implement "getPeers"'); - } - - public getGossipMeshPeerCount(_topicType: TopicType): Promise { - return Promise.resolve(0); - } - - public broadcastProposal(_proposal: BlockProposal): Promise { - throw new Error('DummyP2P does not implement "broadcastProposal"'); - } - - public broadcastCheckpointProposal(_proposal: CheckpointProposal): Promise { - throw new Error('DummyP2P does not implement "broadcastCheckpointProposal"'); - } - - public broadcastCheckpointAttestations(_attestations: CheckpointAttestation[]): Promise { - throw new Error('DummyP2P does not implement "broadcastCheckpointAttestations"'); - } - - public registerBlockProposalHandler(_handler: P2PBlockReceivedCallback): void { - throw new Error('DummyP2P does not implement "registerBlockProposalHandler"'); - } - - public registerValidatorCheckpointProposalHandler(_handler: P2PCheckpointReceivedCallback): void { - throw new Error('DummyP2P does not implement "registerValidatorCheckpointProposalHandler"'); - } - - public registerAllNodesCheckpointProposalHandler(_handler: P2PCheckpointReceivedCallback): void { - throw new Error('DummyP2P does not implement "registerAllNodesCheckpointProposalHandler"'); - } - - public requestTxs(_txHashes: TxHash[]): Promise<(Tx | undefined)[]> { - throw new Error('DummyP2P does not implement "requestTxs"'); - } - - public requestTxByHash(_txHash: TxHash): Promise { - throw new Error('DummyP2P does not implement "requestTxByHash"'); - } - - public sendTx(_tx: Tx): Promise { - throw new Error('DummyP2P does not implement "sendTx"'); - } - - public handleFailedExecution(_txHashes: TxHash[]): Promise { - throw new Error('DummyP2P does not implement "handleFailedExecution"'); - } - - public getTxByHashFromPool(_txHash: TxHash): Promise { - throw new Error('DummyP2P does not implement "getTxByHashFromPool"'); - } - - public getTxByHash(_txHash: TxHash): Promise { - throw new Error('DummyP2P does not implement "getTxByHash"'); - } - - public getArchivedTxByHash(_txHash: TxHash): Promise { - throw new Error('DummyP2P does not implement "getArchivedTxByHash"'); - } - - public getTxStatus(_txHash: TxHash): Promise<'pending' | 'mined' | undefined> { - // In TXE there is no concept of transactions but we need to implement this because of tagging. We return 'mined' - // tx status for any tx hash. - return Promise.resolve('mined'); - } - - public iteratePendingTxs(): AsyncIterableIterator { - throw new Error('DummyP2P does not implement "iteratePendingTxs"'); - } - - public iterateEligiblePendingTxs(): AsyncIterableIterator { - throw new Error('DummyP2P does not implement "iterateEligiblePendingTxs"'); - } - - public getPendingTxCount(): Promise { - throw new Error('DummyP2P does not implement "getPendingTxCount"'); - } - - public hasEligiblePendingTxs(_minCount: number): Promise { - throw new Error('DummyP2P does not implement "hasEligiblePendingTxs"'); - } - - public start(): Promise { - throw new Error('DummyP2P does not implement "start"'); - } - - public stop(): Promise { - throw new Error('DummyP2P does not implement "stop"'); - } - - public isReady(): boolean { - throw new Error('DummyP2P does not implement "isReady"'); - } - - public getStatus(): Promise { - throw new Error('DummyP2P does not implement "getStatus"'); - } - - public getEnr(): ENR | undefined { - throw new Error('DummyP2P does not implement "getEnr"'); - } - - public isP2PClient(): true { - throw new Error('DummyP2P does not implement "isP2PClient"'); - } - - public getTxProvider(): ITxProvider { - throw new Error('DummyP2P does not implement "getTxProvider"'); - } - - public getTxsByHash(_txHashes: TxHash[]): Promise { - throw new Error('DummyP2P does not implement "getTxsByHash"'); - } - - public getCheckpointAttestationsForSlot( - _slot: SlotNumber, - _proposalPayloadHash?: CheckpointProposalHash, - ): Promise { - throw new Error('DummyP2P does not implement "getCheckpointAttestationsForSlot"'); - } - - public addOwnCheckpointAttestations(_attestations: CheckpointAttestation[]): Promise { - throw new Error('DummyP2P does not implement "addOwnCheckpointAttestations"'); - } - - public getProposalsForSlot(_slot: SlotNumber): Promise<{ - blockProposals: BlockProposal[]; - checkpointProposals: CheckpointProposalCore[]; - }> { - return Promise.resolve({ blockProposals: [], checkpointProposals: [] }); - } - - public hasCheckpointProposalForSlot(_slot: SlotNumber): Promise { - return Promise.resolve(false); - } - - public getL2BlockHash(_number: number): Promise { - throw new Error('DummyP2P does not implement "getL2BlockHash"'); - } - - public updateP2PConfig(_config: Partial): Promise { - throw new Error('DummyP2P does not implement "updateP2PConfig"'); - } - - public getL2Tips(): Promise { - throw new Error('DummyP2P does not implement "getL2Tips"'); - } - - public handleBlockStreamEvent(_event: L2BlockStreamEvent): Promise { - throw new Error('DummyP2P does not implement "handleBlockStreamEvent"'); - } - - public sync() { - throw new Error('DummyP2P does not implement "sync"'); - } - - public getTxsByHashFromPool(_txHashes: TxHash[]): Promise<(Tx | undefined)[]> { - throw new Error('DummyP2P does not implement "getTxsByHashFromPool"'); - } - - public hasTxsInPool(_txHashes: TxHash[]): Promise { - throw new Error('DummyP2P does not implement "hasTxsInPool"'); - } - - public getSyncedLatestBlockNum(): Promise { - throw new Error('DummyP2P does not implement "getSyncedLatestBlockNum"'); - } - - public getSyncedProvenBlockNum(): Promise { - throw new Error('DummyP2P does not implement "getSyncedProvenBlockNum"'); - } - - public getSyncedLatestSlot(): Promise { - throw new Error('DummyP2P does not implement "getSyncedLatestSlot"'); - } - - protectTxs(_txHashes: TxHash[], _blockHeader: BlockHeader): Promise { - throw new Error('DummyP2P does not implement "protectTxs".'); - } - - prepareForSlot(_slotNumber: SlotNumber): Promise { - return Promise.resolve(); - } - - addReqRespSubProtocol(_subProtocol: ReqRespSubProtocol, _handler: ReqRespSubProtocolHandler): Promise { - throw new Error('DummyP2P does not implement "addReqRespSubProtocol".'); - } - handleAuthRequestFromPeer(_authRequest: AuthRequest, _peerId: PeerId): Promise { - throw new Error('DummyP2P does not implement "handleAuthRequestFromPeer".'); - } - - //This is no-op - public registerThisValidatorAddresses(_address: EthAddress[]): void {} - - public registerDuplicateProposalCallback(_callback: P2PDuplicateProposalCallback): void { - throw new Error('DummyP2P does not implement "registerDuplicateProposalCallback"'); - } - - public registerOversizedProposalCallback(_callback: P2POversizedProposalCallback): void { - throw new Error('DummyP2P does not implement "registerOversizedProposalCallback"'); - } - - public registerDuplicateAttestationCallback(_callback: P2PDuplicateAttestationCallback): void { - throw new Error('DummyP2P does not implement "registerDuplicateAttestationCallback"'); - } - - public registerCheckpointAttestationCallback(_callback: P2PCheckpointAttestationCallback): void { - throw new Error('DummyP2P does not implement "registerCheckpointAttestationCallback"'); - } - - public hasBlockProposalsForSlot(_slot: SlotNumber): Promise { - throw new Error('DummyP2P does not implement "hasBlockProposalsForSlot"'); - } -} diff --git a/yarn-project/txe/src/state_machine/global_variable_builder.ts b/yarn-project/txe/src/state_machine/global_variable_builder.ts deleted file mode 100644 index c722168d5649..000000000000 --- a/yarn-project/txe/src/state_machine/global_variable_builder.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts'; -import type { SlotNumber } from '@aztec/foundation/branded-types'; -import { times } from '@aztec/foundation/collection'; -import type { EthAddress } from '@aztec/foundation/eth-address'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { FEE_ORACLE_LAG, GasFees } from '@aztec/stdlib/gas'; -import { makeGlobalVariables } from '@aztec/stdlib/testing'; -import type { CheckpointGlobalVariables, FeeProvider, GlobalVariableBuilder } from '@aztec/stdlib/tx'; - -/** Simple FeeProvider for TXE that returns zero fees. */ -export class TXEFeeProvider implements FeeProvider { - public getCurrentMinFees(): Promise { - return Promise.resolve(new GasFees(0, 0)); - } - - public getPredictedMinFees(): Promise { - return Promise.resolve(times(FEE_ORACLE_LAG, () => new GasFees(0, 0))); - } -} - -export class TXEGlobalVariablesBuilder implements GlobalVariableBuilder { - public buildCheckpointGlobalVariables( - _coinbase: EthAddress, - _feeRecipient: AztecAddress, - _slotNumber: SlotNumber, - _simulationOverridesPlan?: SimulationOverridesPlan, - ): Promise { - const vars = makeGlobalVariables(); - return Promise.resolve({ - chainId: vars.chainId, - version: vars.version, - slotNumber: vars.slotNumber, - timestamp: vars.timestamp, - coinbase: vars.coinbase, - feeRecipient: vars.feeRecipient, - gasFees: vars.gasFees, - }); - } -} diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index 4f724f3f8130..e1b4efba6200 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -1,5 +1,3 @@ -import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node'; -import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; @@ -17,17 +15,9 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { TXEArchiver } from './archiver.js'; -import { DummyP2P } from './dummy_p2p_client.js'; -import { TXEFeeProvider, TXEGlobalVariablesBuilder } from './global_variable_builder.js'; -import { MockEpochCache } from './mock_epoch_cache.js'; +import { TXENode } from './node.js'; import { TXESynchronizer } from './synchronizer.js'; -const VERSION = 1; -const CHAIN_ID = 1; -// Hardcoded so the bundled TXE doesn't try to read stdlib's package.json at a relative path -// that becomes invalid once bundled -const PACKAGE_VERSION = 'txe'; - export class TXEStateMachine { constructor( public node: AztecNode, @@ -46,34 +36,7 @@ export class TXEStateMachine { noteStore: NoteStore, ) { const synchronizer = await TXESynchronizer.create(); - const aztecNodeConfig = {} as AztecNodeConfig; - - const log = createLogger('txe_node'); - const node = new AztecNodeService({ - config: aztecNodeConfig, - p2pClient: new DummyP2P(), - blockSource: archiver, - logsSource: archiver, - contractDataSource: archiver, - l1ToL2MessageSource: archiver, - worldStateSynchronizer: synchronizer, - sequencer: undefined, - proverNode: undefined, - slasherClient: undefined, - validatorsSentinel: undefined, - stopStartedWatchers: async () => {}, - l1ChainId: CHAIN_ID, - version: VERSION, - globalVariableBuilder: new TXEGlobalVariablesBuilder(), - rollupContract: undefined, - feeProvider: new TXEFeeProvider(), - epochCache: new MockEpochCache(), - packageVersion: PACKAGE_VERSION, - peerProofVerifier: new TestCircuitVerifier(), - rpcProofVerifier: new TestCircuitVerifier(), - telemetry: undefined, - log, - }); + const node = new TXENode(archiver, synchronizer); const contractClassService = new ContractClassService(node, contractStore); const contractSyncService = new ContractSyncService( diff --git a/yarn-project/txe/src/state_machine/mock_epoch_cache.ts b/yarn-project/txe/src/state_machine/mock_epoch_cache.ts deleted file mode 100644 index 80d98f24f8ae..000000000000 --- a/yarn-project/txe/src/state_machine/mock_epoch_cache.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { EpochAndSlot, EpochCacheInterface, EpochCommitteeInfo, SlotTag } from '@aztec/epoch-cache'; -import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; -import { EthAddress } from '@aztec/foundation/eth-address'; -import { EmptyL1RollupConstants, type L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; - -/** - * Mock implementation of the EpochCacheInterface used to satisfy dependencies of AztecNodeService. - * Since in TXE we don't validate transactions, mock suffices here. - */ -export class MockEpochCache implements EpochCacheInterface { - getCommittee(_slot: SlotTag = 'now'): Promise { - return Promise.resolve({ - committee: undefined, - seed: 0n, - epoch: EpochNumber.ZERO, - isEscapeHatchOpen: false, - }); - } - - getSlotNow(): SlotNumber { - return SlotNumber(0); - } - - getTargetSlot(): SlotNumber { - return SlotNumber(0); - } - - getEpochNow(): EpochNumber { - return EpochNumber.ZERO; - } - - getTargetEpoch(): EpochNumber { - return EpochNumber.ZERO; - } - - getEpochAndSlotNow(): EpochAndSlot & { nowMs: bigint } { - return { - epoch: EpochNumber.ZERO, - slot: SlotNumber(0), - ts: 0n, - nowMs: 0n, - }; - } - - getEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } { - return { - epoch: EpochNumber.ZERO, - slot: SlotNumber(0), - ts: 0n, - nowSeconds: 0n, - }; - } - - getTargetEpochAndSlotInNextL1Slot(): EpochAndSlot & { nowSeconds: bigint } { - return this.getEpochAndSlotInNextL1Slot(); - } - - getProposerIndexEncoding(_epoch: EpochNumber, _slot: SlotNumber, _seed: bigint): `0x${string}` { - return '0x00'; - } - - computeProposerIndex(_slot: SlotNumber, _epoch: EpochNumber, _seed: bigint, _size: bigint): bigint { - return 0n; - } - - getCurrentAndNextSlot(): { currentSlot: SlotNumber; nextSlot: SlotNumber } { - return { - currentSlot: SlotNumber(0), - nextSlot: SlotNumber(0), - }; - } - - getTargetAndNextSlot(): { targetSlot: SlotNumber; nextSlot: SlotNumber } { - return { - targetSlot: SlotNumber(0), - nextSlot: SlotNumber(0), - }; - } - - getProposerAttesterAddressInSlot(_slot: SlotNumber): Promise { - return Promise.resolve(undefined); - } - - isInCommittee(_slot: SlotTag, _validator: EthAddress): Promise { - return Promise.resolve(false); - } - - getRegisteredValidators(): Promise { - return Promise.resolve([]); - } - - filterInCommittee(_slot: SlotTag, _validators: EthAddress[]): Promise { - return Promise.resolve([]); - } - - isEscapeHatchOpen(_epoch: EpochNumber): Promise { - return Promise.resolve(false); - } - - isEscapeHatchOpenAtSlot(_slot: SlotTag): Promise { - return Promise.resolve(false); - } - - getL1Constants(): L1RollupConstants { - return EmptyL1RollupConstants; - } -} diff --git a/yarn-project/txe/src/state_machine/node.ts b/yarn-project/txe/src/state_machine/node.ts new file mode 100644 index 000000000000..678b00e4280f --- /dev/null +++ b/yarn-project/txe/src/state_machine/node.ts @@ -0,0 +1,517 @@ +import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; +import { BlockNumber, type CheckpointNumber } from '@aztec/foundation/branded-types'; +import { Fr } from '@aztec/foundation/curves/bn254'; +import { BadRequestError } from '@aztec/foundation/json-rpc'; +import { MembershipWitness, type SiblingPath } from '@aztec/foundation/trees'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { + type BlockData, + BlockHash, + type BlockParameter, + BlockTag, + type DataInBlock, + type L2Block, + type L2Tips, + type NormalizedBlockParameter, + inspectBlockParameter, +} from '@aztec/stdlib/block'; +import type { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers'; +import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash'; +import { + type AztecNode, + type BlockIncludeOptions, + type BlockResponse, + type BlocksIncludeOptions, + l1PublishInfoFromL1PublishedData, +} from '@aztec/stdlib/interfaces/client'; +import type { MerkleTreeReadOperations } from '@aztec/stdlib/interfaces/server'; +import type { LogResult, PrivateLogsQuery, PublicLogsQuery } from '@aztec/stdlib/logs'; +import { InboxLeaf, type L2ToL1MembershipWitness } from '@aztec/stdlib/messaging'; +import { + MerkleTreeId, + type NullifierLeafPreimage, + NullifierMembershipWitness, + type PublicDataTreeLeafPreimage, + PublicDataWitness, +} from '@aztec/stdlib/trees'; +import { + type GetTxReceiptOptions, + type IndexedTxEffect, + MinedTxReceipt, + type MinedTxStatus, + PendingTxReceipt, + type TxHash, + type TxReceipt, + TxStatus, +} from '@aztec/stdlib/tx'; + +import type { TXEArchiver } from './archiver.js'; +import type { TXESynchronizer } from './synchronizer.js'; +import { UnimplementedAztecNode } from './unimplemented_node.js'; + +const VERSION = 1; +const CHAIN_ID = 1; + +/** + * Normalizes a {@link BlockParameter} (which may be a bare value) into a {@link NormalizedBlockParameter} + * object form. Performs no chain-tip resolution. + */ +function normalizeBlockParameter(param: BlockParameter): NormalizedBlockParameter { + if (BlockHash.isBlockHash(param)) { + return { hash: param }; + } + if (typeof param === 'number') { + return { number: param }; + } + if (typeof param === 'string') { + if (BlockTag.includes(param)) { + return { tag: param === 'latest' ? 'proposed' : param }; + } + throw new BadRequestError(`Invalid BlockParameter tag: ${param}`); + } + if (typeof param === 'object' && param !== null) { + if ('number' in param) { + return { number: param.number }; + } + if ('hash' in param) { + return { hash: param.hash }; + } + if ('archive' in param) { + return { archive: param.archive }; + } + if ('tag' in param) { + if (BlockTag.includes(param.tag)) { + return { tag: param.tag }; + } + throw new BadRequestError(`Invalid BlockParameter tag: ${param.tag}`); + } + } + throw new BadRequestError(`Invalid BlockParameter: ${JSON.stringify(param)}`); +} + +/** + * Minimal {@link AztecNode} implementation serving the read-side queries that the PXE services instantiated by the + * TXE perform, directly against the TXE's own archiver and world state. Block production doesn't go through this + * node (see `TXEStateMachine.handleL2Block`), so everything server-side (p2p, sequencing, proving, validation) is + * inherited as a throwing stub from {@link UnimplementedAztecNode}. + */ +export class TXENode extends UnimplementedAztecNode implements AztecNode { + constructor( + private readonly archiver: TXEArchiver, + private readonly synchronizer: TXESynchronizer, + ) { + super(); + } + + public override async findLeavesIndexes( + referenceBlock: BlockParameter, + treeId: MerkleTreeId, + leafValues: Fr[], + ): Promise<(DataInBlock | undefined)[]> { + const committedDb = await this.#getWorldState(referenceBlock); + const maybeIndices = await committedDb.findLeafIndices( + treeId, + leafValues.map(x => x.toBuffer()), + ); + // Filter out undefined values to query block numbers only for found leaves + const definedIndices = maybeIndices.filter(x => x !== undefined); + + // Now we find the block numbers for the defined indices + const blockNumbers = await committedDb.getBlockNumbersForLeafIndices(treeId, definedIndices); + + // Build a map from leaf index to block number + const indexToBlockNumber = new Map(); + for (let i = 0; i < definedIndices.length; i++) { + const blockNumber = blockNumbers[i]; + if (blockNumber === undefined) { + throw new Error( + `Block number is undefined for leaf index ${definedIndices[i]} in tree ${MerkleTreeId[treeId]}`, + ); + } + indexToBlockNumber.set(definedIndices[i], blockNumber); + } + + // Get unique block numbers in order to optimize num calls to getLeafValue function. + const uniqueBlockNumbers = [...new Set(indexToBlockNumber.values())]; + + // Now we obtain the block hashes from the archive tree (block number = leaf index in archive tree). + const blockHashes = await Promise.all( + uniqueBlockNumbers.map(blockNumber => { + return committedDb.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber)); + }), + ); + + // Build a map from block number to block hash + const blockNumberToHash = new Map(); + for (let i = 0; i < uniqueBlockNumbers.length; i++) { + const blockHash = blockHashes[i]; + if (blockHash === undefined) { + throw new Error(`Block hash is undefined for block number ${uniqueBlockNumbers[i]}`); + } + blockNumberToHash.set(uniqueBlockNumbers[i], blockHash); + } + + // Create DataInBlock objects by combining indices, blockNumbers and blockHashes and return them. + return maybeIndices.map(index => { + if (index === undefined) { + return undefined; + } + const blockNumber = indexToBlockNumber.get(index); + if (blockNumber === undefined) { + throw new Error(`Block number not found for leaf index ${index} in tree ${MerkleTreeId[treeId]}`); + } + const l2BlockHash = blockNumberToHash.get(blockNumber); + if (l2BlockHash === undefined) { + throw new Error(`Block hash not found for block number ${blockNumber}`); + } + return { + l2BlockNumber: blockNumber, + l2BlockHash, + data: index, + }; + }); + } + + public override async getNullifierMembershipWitness( + referenceBlock: BlockParameter, + nullifier: Fr, + ): Promise { + const db = await this.#getWorldState(referenceBlock); + const [witness] = await db.findSiblingPaths(MerkleTreeId.NULLIFIER_TREE, [nullifier.toBuffer()]); + if (!witness) { + return undefined; + } + + const { index, path } = witness; + const leafPreimage = await db.getLeafPreimage(MerkleTreeId.NULLIFIER_TREE, index); + if (!leafPreimage) { + return undefined; + } + + return new NullifierMembershipWitness(index, leafPreimage as NullifierLeafPreimage, path); + } + + public override async getLowNullifierMembershipWitness( + referenceBlock: BlockParameter, + nullifier: Fr, + ): Promise { + const committedDb = await this.#getWorldState(referenceBlock); + const findResult = await committedDb.getPreviousValueIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBigInt()); + if (!findResult) { + return undefined; + } + const { index, alreadyPresent } = findResult; + if (alreadyPresent) { + throw new Error( + `Cannot prove nullifier non-inclusion: nullifier ${nullifier.toBigInt()} already exists in the tree`, + ); + } + const preimageData = (await committedDb.getLeafPreimage(MerkleTreeId.NULLIFIER_TREE, index))!; + + const siblingPath = await committedDb.getSiblingPath(MerkleTreeId.NULLIFIER_TREE, BigInt(index)); + return new NullifierMembershipWitness(BigInt(index), preimageData as NullifierLeafPreimage, siblingPath); + } + + public override async getPublicDataWitness( + referenceBlock: BlockParameter, + leafSlot: Fr, + ): Promise { + const committedDb = await this.#getWorldState(referenceBlock); + const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt()); + if (!lowLeafResult) { + return undefined; + } else { + const preimage = (await committedDb.getLeafPreimage( + MerkleTreeId.PUBLIC_DATA_TREE, + lowLeafResult.index, + )) as PublicDataTreeLeafPreimage; + const path = await committedDb.getSiblingPath(MerkleTreeId.PUBLIC_DATA_TREE, lowLeafResult.index); + return new PublicDataWitness(lowLeafResult.index, preimage, path); + } + } + + public override async getBlockHashMembershipWitness( + referenceBlock: BlockParameter, + blockHash: BlockHash, + ): Promise | undefined> { + // The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`, + // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1). + // So we need the world state at block N-1, not block N, to produce a sibling path matching that root. + const referenceBlockNumber = await this.#resolveBlockNumber(normalizeBlockParameter(referenceBlock)); + if (referenceBlockNumber === BlockNumber.ZERO) { + // Block 0 (the initial block) has an empty archive, so no membership witness can exist. + return undefined; + } + const committedDb = this.synchronizer.getSnapshot(BlockNumber(referenceBlockNumber - 1)); + const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); + return pathAndIndex === undefined + ? undefined + : MembershipWitness.fromSiblingPath(pathAndIndex.index, pathAndIndex.path); + } + + public override async getNoteHashMembershipWitness( + referenceBlock: BlockParameter, + noteHash: Fr, + ): Promise | undefined> { + const committedDb = await this.#getWorldState(referenceBlock); + const [pathAndIndex] = await committedDb.findSiblingPaths( + MerkleTreeId.NOTE_HASH_TREE, + [noteHash], + ); + return pathAndIndex === undefined + ? undefined + : MembershipWitness.fromSiblingPath(pathAndIndex.index, pathAndIndex.path); + } + + public override async getL1ToL2MessageMembershipWitness( + referenceBlock: BlockParameter, + l1ToL2Message: Fr, + ): Promise<[bigint, SiblingPath] | undefined> { + const db = await this.#getWorldState(referenceBlock); + const [witness] = await db.findSiblingPaths(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, [l1ToL2Message]); + if (!witness) { + return undefined; + } + + return [witness.index, witness.path]; + } + + public override async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise { + const messageIndex = await this.archiver.getL1ToL2MessageIndex(l1ToL2Message); + return messageIndex !== undefined ? InboxLeaf.checkpointNumberFromIndex(messageIndex) : undefined; + } + + public override getL2ToL1MembershipWitness(): Promise { + return this.archiver.getL2ToL1MembershipWitness(); + } + + public override getBlockNumber(): Promise { + // Every chain tip is the latest block in the TXE, so the tag can be ignored. + return this.archiver.getBlockNumber(); + } + + public override getChainTips(): Promise { + return this.archiver.getL2Tips(); + } + + public override getL1Constants(): Promise { + return this.archiver.getL1Constants(); + } + + public override async getBlock( + param: BlockParameter, + options: Opts = {} as Opts, + ): Promise | undefined> { + const query = normalizeBlockParameter(param); + if (options.includeTransactions) { + const block = await this.archiver.getBlock(query); + return block && ((await this.#blockResponseFromL2Block(block, options)) as BlockResponse); + } + const data = await this.archiver.getBlockData(query); + return data && ((await this.#blockResponseFromBlockData(data, options)) as BlockResponse); + } + + public override getBlockData(param: BlockParameter): Promise { + return this.archiver.getBlockData(normalizeBlockParameter(param)); + } + + public override async getBlocks( + from: BlockNumber, + limit: number, + options: Opts = {} as Opts, + ): Promise[]> { + if (options.includeTransactions) { + const blocks = await this.archiver.getBlocks({ from, limit, onlyCheckpointed: !!options.onlyCheckpointed }); + return (await Promise.all( + blocks.map(block => this.#blockResponseFromL2Block(block, options)), + )) as BlockResponse[]; + } + const dataItems = await this.archiver.getBlocksData({ from, limit, onlyCheckpointed: !!options.onlyCheckpointed }); + return (await Promise.all( + dataItems.map(data => this.#blockResponseFromBlockData(data, options)), + )) as BlockResponse[]; + } + + public override getVersion(): Promise { + return Promise.resolve(VERSION); + } + + public override getChainId(): Promise { + return Promise.resolve(CHAIN_ID); + } + + public override getPrivateLogsByTags(query: PrivateLogsQuery): Promise { + return this.archiver.getPrivateLogsByTags(query); + } + + public override getPublicLogsByTags(query: PublicLogsQuery): Promise { + return this.archiver.getPublicLogsByTags(query); + } + + public override async getTxReceipt( + txHash: TxHash, + options?: TGetTxReceiptOptions, + ): Promise> { + const indexed = await this.archiver.getTxEffect(txHash); + let receipt: TxReceipt; + if (indexed) { + receipt = await this.#assembleMinedReceipt(indexed, options); + } else { + if (options?.includePendingTx) { + // The pending tx itself cannot be served (there is no tx pool), so fail loudly rather than return a + // receipt whose type promises a tx that is silently undefined. + throw new Error('TXE node does not support "includePendingTx"'); + } + // The TXE has no tx pool, but tagging sync probes arbitrary tx hashes and expects unknown ones to resolve to a + // pending receipt rather than a dropped one (the previous node-backed setup reported every tx as known to p2p). + receipt = new PendingTxReceipt(txHash, undefined); + } + return receipt; + } + + public override getTxEffect(txHash: TxHash): Promise { + return this.archiver.getTxEffect(txHash); + } + + public override async getPublicStorageAt( + referenceBlock: BlockParameter, + contract: AztecAddress, + slot: Fr, + ): Promise { + const committedDb = await this.#getWorldState(referenceBlock); + const leafSlot = await computePublicDataTreeLeafSlot(contract, slot); + + const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt()); + if (!lowLeafResult || !lowLeafResult.alreadyPresent) { + return Fr.ZERO; + } + const preimage = (await committedDb.getLeafPreimage( + MerkleTreeId.PUBLIC_DATA_TREE, + lowLeafResult.index, + )) as PublicDataTreeLeafPreimage; + return preimage.leaf.value; + } + + public override getContractClass(id: Fr): Promise { + return this.archiver.getContractClass(id); + } + + public override async getContract( + address: AztecAddress, + referenceBlock: BlockParameter = 'latest', + ): Promise { + const blockData = await this.getBlockData(referenceBlock); + if (!blockData) { + throw new Error( + `Reference block ${inspectBlockParameter(referenceBlock)} not found when querying contract ${address}.`, + ); + } + return this.archiver.getContract(address, blockData.header.globalVariables.timestamp); + } + + /** + * Returns committed world state at the requested block. The TXE is single-writer with no reorgs and world state + * is updated synchronously with the archiver (see `TXEStateMachine.handleL2Block`), so no syncing or fork + * verification is needed: tags resolve to the committed db and concrete blocks to a snapshot. + */ + async #getWorldState(referenceBlock: BlockParameter): Promise { + const query = normalizeBlockParameter(referenceBlock); + if ('tag' in query) { + // Every tag ('proposed', 'checkpointed', 'proven', 'finalized') is the latest block in the TXE. + return this.synchronizer.getCommitted(); + } + const blockNumber = await this.#resolveBlockNumber(query); + return this.synchronizer.getSnapshot(blockNumber); + } + + /** + * Resolves any {@link NormalizedBlockParameter} variant to a concrete block number via the archiver. Numeric + * queries go through the archiver too, so a reference to a block that doesn't exist fails here instead of + * reaching the native world state. + */ + async #resolveBlockNumber(query: NormalizedBlockParameter): Promise { + const blockNumber = await this.archiver.getBlockNumber(query); + if (blockNumber === undefined) { + throw new Error(`Block not found for ${inspectBlockParameter(query)} when resolving query.`); + } + return blockNumber; + } + + #blockResponseFromBlockData(data: BlockData, options: BlockIncludeOptions): Promise { + const response: BlockResponse = { + header: data.header, + archive: data.archive, + hash: data.blockHash, + checkpointNumber: data.checkpointNumber, + indexWithinCheckpoint: data.indexWithinCheckpoint, + number: data.header.getBlockNumber(), + }; + return this.#withCheckpointContext(response, options); + } + + async #blockResponseFromL2Block(block: L2Block, options: BlockIncludeOptions): Promise { + const response: BlockResponse = { + header: block.header, + archive: block.archive, + hash: await block.hash(), + checkpointNumber: block.checkpointNumber, + indexWithinCheckpoint: block.indexWithinCheckpoint, + number: block.number, + }; + response.body = block.body; + return this.#withCheckpointContext(response, options); + } + + /** Attaches L1 publish info and attestations from the checkpoint store when the options request them. */ + async #withCheckpointContext(response: BlockResponse, options: BlockIncludeOptions): Promise { + if (options.includeL1PublishInfo || options.includeAttestations) { + const checkpoint = await this.archiver.getCheckpointData({ number: response.checkpointNumber }); + if (options.includeL1PublishInfo) { + response.l1 = l1PublishInfoFromL1PublishedData(checkpoint?.l1); + } + if (options.includeAttestations) { + response.attestations = checkpoint?.attestations ?? []; + } + } + return response; + } + + /** + * Assembles a {@link MinedTxReceipt} from a raw {@link IndexedTxEffect}, deriving the finalization status from the + * L2 tips and the epoch from the block's slot number. + */ + async #assembleMinedReceipt(indexed: IndexedTxEffect, options?: GetTxReceiptOptions): Promise { + const blockNumber = indexed.l2BlockNumber; + const [tips, l1Constants] = await Promise.all([this.archiver.getL2Tips(), this.archiver.getL1Constants()]); + + const status = this.#deriveMinedStatus(blockNumber, tips); + const epochNumber = getEpochAtSlot(indexed.slotNumber, l1Constants); + + return new MinedTxReceipt( + indexed.data.txHash, + status, + MinedTxReceipt.executionResultFromRevertCode(indexed.data.revertCode), + indexed.data.transactionFee.toBigInt(), + indexed.l2BlockHash, + blockNumber, + indexed.slotNumber, + indexed.txIndexInBlock, + epochNumber, + options?.includeTxEffect ? indexed.data : undefined, + /*debugLogs=*/ undefined, + ); + } + + #deriveMinedStatus(blockNumber: BlockNumber, tips: L2Tips): MinedTxStatus { + if (blockNumber <= tips.finalized.block.number) { + return TxStatus.FINALIZED; + } else if (blockNumber <= tips.proven.block.number) { + return TxStatus.PROVEN; + } else if (blockNumber <= tips.checkpointed.block.number) { + return TxStatus.CHECKPOINTED; + } else { + return TxStatus.PROPOSED; + } + } +} diff --git a/yarn-project/txe/src/state_machine/unimplemented_node.ts b/yarn-project/txe/src/state_machine/unimplemented_node.ts new file mode 100644 index 000000000000..64b5ed6f8890 --- /dev/null +++ b/yarn-project/txe/src/state_machine/unimplemented_node.ts @@ -0,0 +1,345 @@ +import type { ARCHIVE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; +import type { + BlockNumber, + CheckpointNumber, + CheckpointProposalHash, + EpochNumber, + SlotNumber, +} from '@aztec/foundation/branded-types'; +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { EthAddress } from '@aztec/foundation/eth-address'; +import type { MembershipWitness, SiblingPath } from '@aztec/foundation/trees'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { + BlockData, + BlockHash, + BlockParameter, + CheckpointsQuery, + DataInBlock, + L2BlockTag, + L2Tips, +} from '@aztec/stdlib/block'; +import type { CheckpointData } from '@aztec/stdlib/checkpoint'; +import type { + ContractClassPublic, + ContractInstanceWithAddress, + NodeInfo, + ProtocolContractAddresses, +} from '@aztec/stdlib/contract'; +import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; +import type { GasFees, ManaUsageEstimate } from '@aztec/stdlib/gas'; +import type { + AztecNode, + BlockIncludeOptions, + BlockResponse, + BlocksIncludeOptions, + CheckpointIncludeOptions, + CheckpointParameter, + CheckpointResponse, + CheckpointTag, + GetTxByHashOptions, + PeerInfo, + ProposalsForSlot, +} from '@aztec/stdlib/interfaces/client'; +import type { AllowedElement, WorldStateSyncStatus } from '@aztec/stdlib/interfaces/server'; +import type { LogResult, PrivateLogsQuery, PublicLogsQuery } from '@aztec/stdlib/logs'; +import type { L2ToL1MembershipWitness } from '@aztec/stdlib/messaging'; +import type { CheckpointAttestation } from '@aztec/stdlib/p2p'; +import type { MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; +import type { + GetTxReceiptOptions, + IndexedTxEffect, + PublicSimulationOutput, + SimulationOverrides, + Tx, + TxHash, + TxReceipt, + TxValidationResult, +} from '@aztec/stdlib/tx'; +import type { SingleValidatorStats, ValidatorsStats } from '@aztec/stdlib/validators'; + +/** + * {@link AztecNode} implementation in which every method throws. TXENode extends it, overriding the read-side + * queries the TXE actually serves, so any surface the TXE doesn't cover fails loudly instead of silently + * misbehaving. Method order mirrors the interface declaration. + */ +export class UnimplementedAztecNode implements AztecNode { + public getWorldStateSyncStatus(): Promise { + throw new Error('TXE node does not implement "getWorldStateSyncStatus"'); + } + + public findLeavesIndexes( + _referenceBlock: BlockParameter, + _treeId: MerkleTreeId, + _leafValues: Fr[], + ): Promise<(DataInBlock | undefined)[]> { + throw new Error('TXE node does not implement "findLeavesIndexes"'); + } + + public getNullifierMembershipWitness( + _referenceBlock: BlockParameter, + _nullifier: Fr, + ): Promise { + throw new Error('TXE node does not implement "getNullifierMembershipWitness"'); + } + + public getLowNullifierMembershipWitness( + _referenceBlock: BlockParameter, + _nullifier: Fr, + ): Promise { + throw new Error('TXE node does not implement "getLowNullifierMembershipWitness"'); + } + + public getPublicDataWitness(_referenceBlock: BlockParameter, _leafSlot: Fr): Promise { + throw new Error('TXE node does not implement "getPublicDataWitness"'); + } + + public getBlockHashMembershipWitness( + _referenceBlock: BlockParameter, + _blockHash: BlockHash, + ): Promise | undefined> { + throw new Error('TXE node does not implement "getBlockHashMembershipWitness"'); + } + + public getNoteHashMembershipWitness( + _referenceBlock: BlockParameter, + _noteHash: Fr, + ): Promise | undefined> { + throw new Error('TXE node does not implement "getNoteHashMembershipWitness"'); + } + + public getL1ToL2MessageMembershipWitness( + _referenceBlock: BlockParameter, + _l1ToL2Message: Fr, + ): Promise<[bigint, SiblingPath] | undefined> { + throw new Error('TXE node does not implement "getL1ToL2MessageMembershipWitness"'); + } + + public getL1ToL2MessageCheckpoint(_l1ToL2Message: Fr): Promise { + throw new Error('TXE node does not implement "getL1ToL2MessageCheckpoint"'); + } + + public getL2ToL1Messages(_epoch: EpochNumber): Promise { + throw new Error('TXE node does not implement "getL2ToL1Messages"'); + } + + public getL2ToL1MembershipWitness( + _txHash: TxHash, + _message: Fr, + _messageIndexInTx?: number, + ): Promise { + throw new Error('TXE node does not implement "getL2ToL1MembershipWitness"'); + } + + public getBlockNumber(_tip?: L2BlockTag): Promise { + throw new Error('TXE node does not implement "getBlockNumber"'); + } + + public getCheckpointNumber(_tip?: CheckpointTag): Promise { + throw new Error('TXE node does not implement "getCheckpointNumber"'); + } + + public getChainTips(): Promise { + throw new Error('TXE node does not implement "getChainTips"'); + } + + public getL1Constants(): Promise { + throw new Error('TXE node does not implement "getL1Constants"'); + } + + public getSyncedL2SlotNumber(): Promise { + throw new Error('TXE node does not implement "getSyncedL2SlotNumber"'); + } + + public getSyncedL2EpochNumber(): Promise { + throw new Error('TXE node does not implement "getSyncedL2EpochNumber"'); + } + + public getSyncedL1Timestamp(): Promise { + throw new Error('TXE node does not implement "getSyncedL1Timestamp"'); + } + + public getCheckpointsData(_query: CheckpointsQuery): Promise { + throw new Error('TXE node does not implement "getCheckpointsData"'); + } + + public getBlock( + _param: BlockParameter, + _options?: Opts, + ): Promise | undefined> { + throw new Error('TXE node does not implement "getBlock"'); + } + + public getBlockData(_param: BlockParameter): Promise { + throw new Error('TXE node does not implement "getBlockData"'); + } + + public getBlocks( + _from: BlockNumber, + _limit: number, + _options?: Opts, + ): Promise[]> { + throw new Error('TXE node does not implement "getBlocks"'); + } + + public getCheckpoint( + _param: CheckpointParameter, + _options?: Opts, + ): Promise | undefined> { + throw new Error('TXE node does not implement "getCheckpoint"'); + } + + public getCheckpoints( + _from: CheckpointNumber, + _limit: number, + _options?: Opts, + ): Promise[]> { + throw new Error('TXE node does not implement "getCheckpoints"'); + } + + public isReady(): Promise { + throw new Error('TXE node does not implement "isReady"'); + } + + public getNodeInfo(): Promise { + throw new Error('TXE node does not implement "getNodeInfo"'); + } + + public getCurrentMinFees(): Promise { + throw new Error('TXE node does not implement "getCurrentMinFees"'); + } + + public getPredictedMinFees(_manaUsage?: ManaUsageEstimate): Promise { + throw new Error('TXE node does not implement "getPredictedMinFees"'); + } + + public getMaxPriorityFees(): Promise { + throw new Error('TXE node does not implement "getMaxPriorityFees"'); + } + + public getNodeVersion(): Promise { + throw new Error('TXE node does not implement "getNodeVersion"'); + } + + public getVersion(): Promise { + throw new Error('TXE node does not implement "getVersion"'); + } + + public getChainId(): Promise { + throw new Error('TXE node does not implement "getChainId"'); + } + + // Typed via the interface rather than the L1ContractAddresses type itself, which is only exported by + // @aztec/ethereum: the TXE has no L1 and doesn't depend on that package. + public getL1ContractAddresses(): ReturnType { + throw new Error('TXE node does not implement "getL1ContractAddresses"'); + } + + public getProtocolContractAddresses(): Promise { + throw new Error('TXE node does not implement "getProtocolContractAddresses"'); + } + + public getPrivateLogsByTags(_query: PrivateLogsQuery): Promise { + throw new Error('TXE node does not implement "getPrivateLogsByTags"'); + } + + public getPublicLogsByTags(_query: PublicLogsQuery): Promise { + throw new Error('TXE node does not implement "getPublicLogsByTags"'); + } + + public sendTx(_tx: Tx): Promise { + throw new Error('TXE node does not implement "sendTx"'); + } + + public getTxReceipt( + _txHash: TxHash, + _options?: TGetTxReceiptOptions, + ): Promise> { + throw new Error('TXE node does not implement "getTxReceipt"'); + } + + public getTxEffect(_txHash: TxHash): Promise { + throw new Error('TXE node does not implement "getTxEffect"'); + } + + public getPendingTxs(_limit?: number, _after?: TxHash, _options?: GetTxByHashOptions): Promise { + throw new Error('TXE node does not implement "getPendingTxs"'); + } + + public getPendingTxCount(): Promise { + throw new Error('TXE node does not implement "getPendingTxCount"'); + } + + public getTxByHash(_txHash: TxHash, _options?: GetTxByHashOptions): Promise { + throw new Error('TXE node does not implement "getTxByHash"'); + } + + public getTxsByHash(_txHashes: TxHash[], _options?: GetTxByHashOptions): Promise { + throw new Error('TXE node does not implement "getTxsByHash"'); + } + + public getPublicStorageAt(_referenceBlock: BlockParameter, _contract: AztecAddress, _slot: Fr): Promise { + throw new Error('TXE node does not implement "getPublicStorageAt"'); + } + + public getValidatorsStats(): Promise { + throw new Error('TXE node does not implement "getValidatorsStats"'); + } + + public getValidatorStats( + _validatorAddress: EthAddress, + _fromSlot?: SlotNumber, + _toSlot?: SlotNumber, + ): Promise { + throw new Error('TXE node does not implement "getValidatorStats"'); + } + + public simulatePublicCalls( + _tx: Tx, + _skipFeeEnforcement?: boolean, + _overrides?: SimulationOverrides, + ): Promise { + throw new Error('TXE node does not implement "simulatePublicCalls"'); + } + + public isValidTx( + _tx: Tx, + _options?: { isSimulation?: boolean; skipFeeEnforcement?: boolean }, + ): Promise { + throw new Error('TXE node does not implement "isValidTx"'); + } + + public getContractClass(_id: Fr): Promise { + throw new Error('TXE node does not implement "getContractClass"'); + } + + public getContract( + _address: AztecAddress, + _referenceBlock?: BlockParameter, + ): Promise { + throw new Error('TXE node does not implement "getContract"'); + } + + public getEncodedEnr(): Promise { + throw new Error('TXE node does not implement "getEncodedEnr"'); + } + + public getAllowedPublicSetup(): Promise { + throw new Error('TXE node does not implement "getAllowedPublicSetup"'); + } + + public getPeers(_includePending?: boolean): Promise { + throw new Error('TXE node does not implement "getPeers"'); + } + + public getCheckpointAttestationsForSlot( + _slot: SlotNumber, + _proposalPayloadHash?: CheckpointProposalHash, + ): Promise { + throw new Error('TXE node does not implement "getCheckpointAttestationsForSlot"'); + } + + public getProposalsForSlot(_slot: SlotNumber): Promise { + throw new Error('TXE node does not implement "getProposalsForSlot"'); + } +} diff --git a/yarn-project/txe/tsconfig.json b/yarn-project/txe/tsconfig.json index 49d16724eba3..e55a2511142e 100644 --- a/yarn-project/txe/tsconfig.json +++ b/yarn-project/txe/tsconfig.json @@ -12,15 +12,9 @@ { "path": "../archiver" }, - { - "path": "../aztec-node" - }, { "path": "../aztec.js" }, - { - "path": "../bb-prover" - }, { "path": "../constants" }, diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index ff07b196ee2f..79c530c1e018 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -2176,9 +2176,7 @@ __metadata: dependencies: "@aztec/accounts": "workspace:^" "@aztec/archiver": "workspace:^" - "@aztec/aztec-node": "workspace:^" "@aztec/aztec.js": "workspace:^" - "@aztec/bb-prover": "workspace:^" "@aztec/bb.js": "workspace:^" "@aztec/constants": "workspace:^" "@aztec/foundation": "workspace:^" From 6928e36f66cb7c7c12df4eb9e4a2b68e2c2ef23a Mon Sep 17 00:00:00 2001 From: Nico Chamo Date: Thu, 16 Jul 2026 21:24:29 -0300 Subject: [PATCH 2/2] refactor(txe): simplify mined tx status derivation --- yarn-project/txe/src/state_machine/node.ts | 94 ++++++++++------------ 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/yarn-project/txe/src/state_machine/node.ts b/yarn-project/txe/src/state_machine/node.ts index 678b00e4280f..50d133ba8c2f 100644 --- a/yarn-project/txe/src/state_machine/node.ts +++ b/yarn-project/txe/src/state_machine/node.ts @@ -53,43 +53,6 @@ import { UnimplementedAztecNode } from './unimplemented_node.js'; const VERSION = 1; const CHAIN_ID = 1; -/** - * Normalizes a {@link BlockParameter} (which may be a bare value) into a {@link NormalizedBlockParameter} - * object form. Performs no chain-tip resolution. - */ -function normalizeBlockParameter(param: BlockParameter): NormalizedBlockParameter { - if (BlockHash.isBlockHash(param)) { - return { hash: param }; - } - if (typeof param === 'number') { - return { number: param }; - } - if (typeof param === 'string') { - if (BlockTag.includes(param)) { - return { tag: param === 'latest' ? 'proposed' : param }; - } - throw new BadRequestError(`Invalid BlockParameter tag: ${param}`); - } - if (typeof param === 'object' && param !== null) { - if ('number' in param) { - return { number: param.number }; - } - if ('hash' in param) { - return { hash: param.hash }; - } - if ('archive' in param) { - return { archive: param.archive }; - } - if ('tag' in param) { - if (BlockTag.includes(param.tag)) { - return { tag: param.tag }; - } - throw new BadRequestError(`Invalid BlockParameter tag: ${param.tag}`); - } - } - throw new BadRequestError(`Invalid BlockParameter: ${JSON.stringify(param)}`); -} - /** * Minimal {@link AztecNode} implementation serving the read-side queries that the PXE services instantiated by the * TXE perform, directly against the TXE's own archiver and world state. Block production doesn't go through this @@ -478,14 +441,14 @@ export class TXENode extends UnimplementedAztecNode implements AztecNode { } /** - * Assembles a {@link MinedTxReceipt} from a raw {@link IndexedTxEffect}, deriving the finalization status from the - * L2 tips and the epoch from the block's slot number. + * Assembles a {@link MinedTxReceipt} from a raw {@link IndexedTxEffect}, deriving the epoch from the block's slot + * number. */ async #assembleMinedReceipt(indexed: IndexedTxEffect, options?: GetTxReceiptOptions): Promise { const blockNumber = indexed.l2BlockNumber; - const [tips, l1Constants] = await Promise.all([this.archiver.getL2Tips(), this.archiver.getL1Constants()]); + const l1Constants = await this.archiver.getL1Constants(); - const status = this.#deriveMinedStatus(blockNumber, tips); + const status = this.#deriveMinedStatus(); const epochNumber = getEpochAtSlot(indexed.slotNumber, l1Constants); return new MinedTxReceipt( @@ -503,15 +466,46 @@ export class TXENode extends UnimplementedAztecNode implements AztecNode { ); } - #deriveMinedStatus(blockNumber: BlockNumber, tips: L2Tips): MinedTxStatus { - if (blockNumber <= tips.finalized.block.number) { - return TxStatus.FINALIZED; - } else if (blockNumber <= tips.proven.block.number) { - return TxStatus.PROVEN; - } else if (blockNumber <= tips.checkpointed.block.number) { - return TxStatus.CHECKPOINTED; - } else { - return TxStatus.PROPOSED; + #deriveMinedStatus(): MinedTxStatus { + // The TXE marks every checkpoint proven and finalized the moment it is added (see TXEArchiver.addCheckpoints + // and getL2Tips), so a mined tx is always finalized. + return TxStatus.FINALIZED; + } +} + +/** + * Normalizes a {@link BlockParameter} (which may be a bare value) into a {@link NormalizedBlockParameter} + * object form. Performs no chain-tip resolution. + */ +function normalizeBlockParameter(param: BlockParameter): NormalizedBlockParameter { + if (BlockHash.isBlockHash(param)) { + return { hash: param }; + } + if (typeof param === 'number') { + return { number: param }; + } + if (typeof param === 'string') { + if (BlockTag.includes(param)) { + return { tag: param === 'latest' ? 'proposed' : param }; } + throw new BadRequestError(`Invalid BlockParameter tag: ${param}`); } + if (typeof param === 'object' && param !== null) { + if ('number' in param) { + return { number: param.number }; + } + if ('hash' in param) { + return { hash: param.hash }; + } + if ('archive' in param) { + return { archive: param.archive }; + } + if ('tag' in param) { + if (BlockTag.includes(param.tag)) { + return { tag: param.tag }; + } + throw new BadRequestError(`Invalid BlockParameter tag: ${param.tag}`); + } + } + throw new BadRequestError(`Invalid BlockParameter: ${JSON.stringify(param)}`); }