From 5e4970b8840594d34eff88b1b70c3d21f2ba6ba9 Mon Sep 17 00:00:00 2001 From: spypsy Date: Mon, 16 Mar 2026 14:54:43 +0000 Subject: [PATCH 01/57] feat: detect announce IP changes automatically --- .../p2p/src/services/discv5/discV5_service.ts | 6 ++- .../services/discv5/discv5_service.test.ts | 19 +++++++-- .../p2p/src/services/libp2p/libp2p_service.ts | 42 ++++++++++++++++--- yarn-project/p2p/src/services/service.ts | 7 ++++ yarn-project/p2p/src/util.ts | 20 ++++----- 5 files changed, 71 insertions(+), 23 deletions(-) diff --git a/yarn-project/p2p/src/services/discv5/discV5_service.ts b/yarn-project/p2p/src/services/discv5/discV5_service.ts index de288a63092a..d242c39ed6e9 100644 --- a/yarn-project/p2p/src/services/discv5/discV5_service.ts +++ b/yarn-project/p2p/src/services/discv5/discV5_service.ts @@ -96,7 +96,7 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService lookupTimeout: 2000, requestTimeout: 2000, allowUnverifiedSessions: true, - enrUpdate: !p2pIp ? true : false, // If no p2p IP is set, enrUpdate can automatically resolve it + enrUpdate: config.queryForIp && !p2pIp, // Enable native ENR IP discovery when no static IP is configured ...configOverrides.config, }, metricsRegistry, @@ -129,9 +129,11 @@ export class DiscV5Service extends EventEmitter implements PeerDiscoveryService private onMultiaddrUpdated(m: Multiaddr) { // We want to update our tcp port to match the udp port // p2pBroadcastPort is optional on config, however it is set to default within the p2p client factory - const multiAddrTcp = multiaddr(convertToMultiaddr(m.nodeAddress().address, this.config.p2pBroadcastPort!, 'tcp')); + const address = m.nodeAddress().address; + const multiAddrTcp = multiaddr(convertToMultiaddr(address, this.config.p2pBroadcastPort!, 'tcp')); this.enr.setLocationMultiaddr(multiAddrTcp); this.logger.info('Multiaddr updated', { multiaddr: multiAddrTcp.toString() }); + this.emit('ip:changed', address); } public async start(): Promise { diff --git a/yarn-project/p2p/src/services/discv5/discv5_service.test.ts b/yarn-project/p2p/src/services/discv5/discv5_service.test.ts index fe4528c27ec2..31e71eadeacd 100644 --- a/yarn-project/p2p/src/services/discv5/discv5_service.test.ts +++ b/yarn-project/p2p/src/services/discv5/discv5_service.test.ts @@ -111,12 +111,16 @@ describe('Discv5Service', () => { await stopNodes(node1, node2); }); - it('should automatically resolve p2p ip if not set', async () => { + it('should automatically resolve p2p ip if not set and queryForIp is true', async () => { const extraNodes = 3; const nodes: DiscV5Service[] = []; - // Create a node with no p2pIp - const node = await createNode({ p2pIp: undefined, config: { addrVotesToUpdateEnr: 1, pingInterval: 200 } }); + // Create a node with no p2pIp and queryForIp=true -- enrUpdate should be enabled + const node = await createNode({ + p2pIp: undefined, + queryForIp: true, + config: { addrVotesToUpdateEnr: 1, pingInterval: 200 }, + }); await node.start(); nodes.push(node); @@ -134,12 +138,19 @@ describe('Discv5Service', () => { expect(node.getEnr().ip).toEqual(undefined); + // ip:changed should be emitted when the ENR IP is resolved + let discoveredIp: string | undefined; + node.on('ip:changed', (ip: string) => { + discoveredIp = ip; + }); + await runDiscoveryUntil(nodes, () => node.getEnr().ip !== undefined); - // Expect it's IP has been updated, and that the tcp and udp ports are the same + // Expect IP has been updated, tcp and udp ports match, and ip:changed event was emitted expect(node.getEnr().ip).not.toEqual(undefined); expect(node.getEnr().tcp).not.toEqual(undefined); expect(node.getEnr().tcp).toEqual(node.getEnr().udp); + expect(discoveredIp).toEqual(node.getEnr().ip?.toString()); await stopNodes(...nodes); }); diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index e59fbaa99b4c..6a466bb46449 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -52,9 +52,10 @@ import { yamux } from '@chainsafe/libp2p-yamux'; import { bootstrap } from '@libp2p/bootstrap'; import { identify } from '@libp2p/identify'; import { type Message, type MultiaddrConnection, type PeerId, TopicValidatorResult } from '@libp2p/interface'; -import type { ConnectionManager } from '@libp2p/interface-internal'; +import type { AddressManager, ConnectionManager } from '@libp2p/interface-internal'; import { mplex } from '@libp2p/mplex'; import { tcp } from '@libp2p/tcp'; +import { multiaddr } from '@multiformats/multiaddr'; import { ENR } from '@nethermindeth/enr'; import { createLibp2p } from 'libp2p'; @@ -175,6 +176,7 @@ export class LibP2PService extends WithTracer implements P2PService { private checkpointReceivedCallback: P2PCheckpointReceivedCallback; private gossipSubEventHandler: (e: CustomEvent) => void; + private ipChangedHandler?: (ip: string) => void; private instrumentation: P2PInstrumentation; @@ -443,8 +445,9 @@ export class LibP2PService extends WithTracer implements P2PService { topics: topicScoreParams, }), }) as (components: GossipSubComponents) => GossipSub, - components: (components: { connectionManager: ConnectionManager }) => ({ + components: (components: { connectionManager: ConnectionManager; addressManager: AddressManager }) => ({ connectionManager: components.connectionManager, + addressManager: components.addressManager, }), }, logger: createLibp2pComponentLogger(logger.module, logger.getBindings()), @@ -503,10 +506,10 @@ export class LibP2PService extends WithTracer implements P2PService { // Get listen & announce addresses for logging const { p2pIp, p2pPort } = this.config; - if (!p2pIp) { + if (!p2pIp && !this.config.queryForIp) { throw new Error('Announce address not provided.'); } - const announceTcpMultiaddr = convertToMultiaddr(p2pIp, p2pPort, 'tcp'); + const announceTcpMultiaddr = p2pIp ? convertToMultiaddr(p2pIp, p2pPort, 'tcp') : undefined; // Create request response protocol handlers const txHandler = reqRespTxHandler(this.mempools); @@ -560,6 +563,29 @@ export class LibP2PService extends WithTracer implements P2PService { if (!this.config.p2pDiscoveryDisabled) { await this.peerDiscoveryService.start(); } + + // When queryForIp is enabled and no static IP was configured, bridge discv5 IP discovery to libp2p. + // Discv5 discovers our public IP via peer WHOAREYOU exchanges (enrUpdate=true) and emits 'ip:changed'. + // We confirm the discovered address in the libp2p AddressManager so it appears in getMultiaddrs() + // and is pushed to all connected peers via the identify protocol. + if (this.config.queryForIp && !p2pIp) { + this.ipChangedHandler = (ip: string) => { + const addressManager = this.node.services.components.addressManager; + const newAddr = multiaddr(convertToMultiaddr(ip, this.config.p2pPort, 'tcp')); + + if (this.config.p2pIp) { + const oldAddr = multiaddr(convertToMultiaddr(this.config.p2pIp, this.config.p2pPort, 'tcp')); + addressManager.removeObservedAddr(oldAddr); + } + + addressManager.addObservedAddr(newAddr); + addressManager.confirmObservedAddr(newAddr); + this.config.p2pIp = ip; + this.logger.info('Public IP discovered via discv5', { ip }); + }; + this.peerDiscoveryService.on('ip:changed', this.ipChangedHandler); + } + this.discoveryRunningPromise = new RunningPromise( async () => { await this.peerManager.heartbeat(); @@ -572,7 +598,7 @@ export class LibP2PService extends WithTracer implements P2PService { this.logger.info(`Started P2P service`, { listen: this.config.listenAddress, port: this.config.p2pPort, - announce: announceTcpMultiaddr, + announce: announceTcpMultiaddr ?? 'pending (queryForIp=true)', peerId: this.node.peerId.toString(), }); } @@ -585,6 +611,12 @@ export class LibP2PService extends WithTracer implements P2PService { // Remove gossip sub listener this.node.services.pubsub.removeEventListener(GossipSubEvent.MESSAGE, this.gossipSubEventHandler); + // Remove ip:changed listener if registered + if (this.ipChangedHandler) { + this.peerDiscoveryService.off('ip:changed', this.ipChangedHandler); + this.ipChangedHandler = undefined; + } + // Stop peer manager this.logger.debug('Stopping peer manager...'); await this.peerManager.stop(); diff --git a/yarn-project/p2p/src/services/service.ts b/yarn-project/p2p/src/services/service.ts index 59594e169788..94f0977d9e41 100644 --- a/yarn-project/p2p/src/services/service.ts +++ b/yarn-project/p2p/src/services/service.ts @@ -196,6 +196,13 @@ export interface PeerDiscoveryService extends EventEmitter { on(event: 'peer:discovered', listener: (enr: ENR) => void): this; emit(event: 'peer:discovered', enr: ENR): boolean; + /** + * Event emitted when our public IP is discovered or changes via discv5 peer interactions. + * Only emitted when enrUpdate is enabled (i.e. queryForIp=true and no static p2pIp). + */ + on(event: 'ip:changed', listener: (ip: string) => void): this; + emit(event: 'ip:changed', ip: string): boolean; + getStatus(): PeerDiscoveryState; getEnr(): ENR | undefined; diff --git a/yarn-project/p2p/src/util.ts b/yarn-project/p2p/src/util.ts index 37bba2f5f0b9..5850b70bb920 100644 --- a/yarn-project/p2p/src/util.ts +++ b/yarn-project/p2p/src/util.ts @@ -7,7 +7,7 @@ import type { GossipSub } from '@chainsafe/libp2p-gossipsub'; import { generateKeyPair, marshalPrivateKey, unmarshalPrivateKey } from '@libp2p/crypto/keys'; import type { Identify } from '@libp2p/identify'; import type { PeerId, PrivateKey } from '@libp2p/interface'; -import type { ConnectionManager } from '@libp2p/interface-internal'; +import type { AddressManager, ConnectionManager } from '@libp2p/interface-internal'; import { createFromPrivKey } from '@libp2p/peer-id-factory'; import { resolve } from 'dns/promises'; import { promises as fs } from 'fs'; @@ -31,6 +31,10 @@ export interface PubSubLibp2p extends Pick & { score: Pick }; + components: { + connectionManager: ConnectionManager; + addressManager: AddressManager; + }; }; } @@ -39,6 +43,7 @@ export type FullLibp2p = Libp2p<{ pubsub: GossipSub; components: { connectionManager: ConnectionManager; + addressManager: AddressManager; }; }>; @@ -102,24 +107,15 @@ function addressToMultiAddressType(address: string): 'ip4' | 'ip6' | 'dns' { } } -export async function configureP2PClientAddresses( - _config: P2PConfig & DataStoreConfig, -): Promise { +export function configureP2PClientAddresses(_config: P2PConfig & DataStoreConfig): P2PConfig & DataStoreConfig { const config = { ..._config }; - const { p2pIp, queryForIp, p2pBroadcastPort, p2pPort } = config; + const { p2pBroadcastPort, p2pPort } = config; // If no broadcast port is provided, use the given p2p port as the broadcast port if (!p2pBroadcastPort) { config.p2pBroadcastPort = p2pPort; } - // check if no announce IP was provided - if (!p2pIp) { - if (queryForIp) { - const publicIp = await getPublicIp(); - config.p2pIp = publicIp; - } - } // TODO(md): guard against setting a local ip address as the announce ip return config; From 072c0256b7bdd5ea937ca28dc5076f9e44dfd9fa Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 17 Mar 2026 14:15:36 +0000 Subject: [PATCH 02/57] fix mocks --- yarn-project/p2p/src/test-helpers/mock-pubsub.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts index cf48654e0aff..fe52ac8686d9 100644 --- a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts +++ b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts @@ -15,6 +15,7 @@ import { type TopicValidatorResult, TypedEventEmitter, } from '@libp2p/interface'; +import type { AddressManager, ConnectionManager } from '@libp2p/interface-internal'; import type { P2PConfig } from '../config.js'; import type { MemPools } from '../mem_pools/interface.js'; @@ -212,6 +213,14 @@ export class MockPubSub implements PubSubLibp2p { get services() { return { pubsub: this.gossipSub, + components: { + addressManager: { + removeObservedAddr: () => {}, + addObservedAddr: () => {}, + confirmObservedAddr: () => {}, + } as unknown as AddressManager, + connectionManager: {} as unknown as ConnectionManager, + }, }; } From dd0c5f8f67fd08407e955a385d48ece3a75d0935 Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 18 Mar 2026 13:58:34 +0000 Subject: [PATCH 03/57] addtests --- .../services/libp2p/libp2p_service.test.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts index a0e812112a30..b3ce2a1010a0 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts @@ -24,6 +24,8 @@ import { ServerWorldStateSynchronizer } from '@aztec/world-state'; import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; import type { Message, PeerId } from '@libp2p/interface'; import { TopicValidatorResult } from '@libp2p/interface'; +import type { ConnectionManager } from '@libp2p/interface-internal'; +import { multiaddr } from '@multiformats/multiaddr'; import { type MockProxy, mock } from 'jest-mock-extended'; import { type P2PConfig, p2pConfigMappings } from '../../config.js'; @@ -36,6 +38,8 @@ import type { MemPools } from '../../mem_pools/interface.js'; import type { TxPoolV2 } from '../../mem_pools/tx_pool_v2/interfaces.js'; import type { TransactionValidator } from '../../msg_validators/tx_validator/factory.js'; import type { PubSubLibp2p } from '../../util.js'; +import { convertToMultiaddr } from '../../util.js'; +import { DummyPeerDiscoveryService } from '../dummy_service.js'; import type { PeerManagerInterface } from '../peer-manager/interface.js'; import type { ReqRespInterface } from '../reqresp/interface.js'; import { BitVector } from '../reqresp/protocols/block_txs/bitvector.js'; @@ -1037,6 +1041,150 @@ describe('LibP2PService', () => { expect(reportMessageValidationResultSpy).toHaveBeenCalledWith('msg-1', MOCK_PEER_ID, TopicValidatorResult.Reject); }); }); + + describe.only('discv5 ip:changed bridge (queryForIp)', () => { + const p2pPort = 40400; + const firstIp = '203.0.113.5'; + const secondIp = '198.51.100.2'; + + function createQueryForIpService() { + const peerDiscovery = new DummyPeerDiscoveryService(); + const addressManager = { + removeObservedAddr: jest.fn(), + addObservedAddr: jest.fn(), + confirmObservedAddr: jest.fn(), + }; + const mockPeerId = mock({ toString: () => MOCK_PEER_ID }); + const nodeState = { status: 'stopped' as string }; + const mockNode = { + get status() { + return nodeState.status; + }, + set status(v: string) { + nodeState.status = v; + }, + peerId: mockPeerId, + start: jest.fn(() => { + nodeState.status = 'started'; + }), + stop: jest.fn(() => { + nodeState.status = 'stopped'; + }), + services: { + pubsub: { + subscribe: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + getMeshPeers: jest.fn(() => []), + }, + components: { + addressManager, + connectionManager: {} as unknown as ConnectionManager, + }, + }, + } as unknown as PubSubLibp2p; + + const config: P2PConfig = { + ...getDefaultConfig(p2pConfigMappings), + seenMessageCacheSize: 1000, + debugP2PInstrumentMessages: false, + disableTransactions: true, + l1ChainId: 1, + rollupVersion: 1, + l1Contracts: { rollupAddress: EthAddress.random() }, + queryForIp: true, + p2pIp: undefined, + p2pPort, + p2pDiscoveryDisabled: true, + peerCheckIntervalMS: 60_000, // Long enough that heartbeat won't run during this unit test + }; + + const mockPeerManager = mock(); + mockPeerManager.initializePeers.mockResolvedValue(undefined); + mockPeerManager.stop.mockResolvedValue(undefined); + mockPeerManager.heartbeat.mockResolvedValue(undefined); + + const mockReqResp = mock(); + mockReqResp.start.mockResolvedValue(undefined); + mockReqResp.stop.mockResolvedValue(undefined); + + const mempools = mock(); + const archiver = mock(); + const epochCache = mock(); + const mockProofVerifier = mock({ + verifyProof: () => Promise.resolve({ valid: true, durationMs: 1, totalDurationMs: 1 }), + }); + const mockWorldStateSynchronizer = mock(); + + const service = new LibP2PService( + config, + mockNode, + peerDiscovery, + mockReqResp, + mockPeerManager, + mempools, + archiver, + epochCache, + mockProofVerifier, + mockWorldStateSynchronizer, + getTelemetryClient(), + createLogger('p2p:test:queryForIp'), + ); + + return { service, peerDiscovery, addressManager, config }; + } + + it('registers observed announce address and updates config when discv5 emits ip:changed', async () => { + const { service, peerDiscovery, addressManager, config } = createQueryForIpService(); + const expectedAddr = multiaddr(convertToMultiaddr(firstIp, p2pPort, 'tcp')); + + await service.start(); + peerDiscovery.emit('ip:changed', firstIp); + + expect(addressManager.addObservedAddr).toHaveBeenCalledWith(expectedAddr); + expect(addressManager.confirmObservedAddr).toHaveBeenCalledWith(expectedAddr); + expect(addressManager.removeObservedAddr).not.toHaveBeenCalled(); + expect(config.p2pIp).toBe(firstIp); + + await service.stop(); + }); + + it('removes previous observed address when ip:changed fires again with a new IP', async () => { + const { service, peerDiscovery, addressManager, config } = createQueryForIpService(); + const firstAddr = multiaddr(convertToMultiaddr(firstIp, p2pPort, 'tcp')); + const secondAddr = multiaddr(convertToMultiaddr(secondIp, p2pPort, 'tcp')); + + await service.start(); + peerDiscovery.emit('ip:changed', firstIp); + addressManager.removeObservedAddr.mockClear(); + addressManager.addObservedAddr.mockClear(); + addressManager.confirmObservedAddr.mockClear(); + + peerDiscovery.emit('ip:changed', secondIp); + + expect(addressManager.removeObservedAddr).toHaveBeenCalledWith(firstAddr); + expect(addressManager.addObservedAddr).toHaveBeenCalledWith(secondAddr); + expect(addressManager.confirmObservedAddr).toHaveBeenCalledWith(secondAddr); + expect(config.p2pIp).toBe(secondIp); + + await service.stop(); + }); + + it('unsubscribes from ip:changed on stop so later emits are ignored', async () => { + const { service, peerDiscovery, addressManager } = createQueryForIpService(); + + await service.start(); + peerDiscovery.emit('ip:changed', firstIp); + addressManager.addObservedAddr.mockClear(); + addressManager.confirmObservedAddr.mockClear(); + + await service.stop(); + peerDiscovery.emit('ip:changed', secondIp); + + expect(addressManager.addObservedAddr).not.toHaveBeenCalled(); + expect(addressManager.confirmObservedAddr).not.toHaveBeenCalled(); + }); + }); }); /** Mock type for tx objects used in block txs validation tests. */ From 8f0d87e4bd3c58f7c8eed89541e902f0ace2a6db Mon Sep 17 00:00:00 2001 From: spypsy Date: Wed, 18 Mar 2026 15:06:37 +0000 Subject: [PATCH 04/57] minor fixes --- yarn-project/p2p/src/client/factory.ts | 2 +- yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index aa154872dada..c8c0d36bbbfb 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -56,7 +56,7 @@ export async function createP2PClient( telemetry: TelemetryClient = getTelemetryClient(), deps: P2PClientDeps = {}, ) { - const config = await configureP2PClientAddresses({ + const config = configureP2PClientAddresses({ ...inputConfig, dataStoreMapSizeKb: inputConfig.p2pStoreMapSizeKb ?? inputConfig.dataStoreMapSizeKb, }); diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts index b3ce2a1010a0..f8e133e1a8dc 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts @@ -1042,7 +1042,7 @@ describe('LibP2PService', () => { }); }); - describe.only('discv5 ip:changed bridge (queryForIp)', () => { + describe('discv5 ip:changed bridge (queryForIp)', () => { const p2pPort = 40400; const firstIp = '203.0.113.5'; const secondIp = '198.51.100.2'; From 66c6ede0b58ebd03309ff993c841161d0b326a48 Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 20 Mar 2026 14:30:13 +0000 Subject: [PATCH 05/57] use local discoveredIp var --- .../p2p/src/services/libp2p/libp2p_service.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index 4e73dae51d43..555e0c7cec82 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -177,6 +177,9 @@ export class LibP2PService extends WithTracer implements P2PService { private gossipSubEventHandler: (e: CustomEvent) => void; private ipChangedHandler?: (ip: string) => void; + /** Discovered public IP address (set when queryForIp is enabled and no static IP was configured). */ + private discoveredP2pIp?: string; + private instrumentation: P2PInstrumentation; private telemetry: TelemetryClient; @@ -572,14 +575,16 @@ export class LibP2PService extends WithTracer implements P2PService { const addressManager = this.node.services.components.addressManager; const newAddr = multiaddr(convertToMultiaddr(ip, this.config.p2pPort, 'tcp')); - if (this.config.p2pIp) { - const oldAddr = multiaddr(convertToMultiaddr(this.config.p2pIp, this.config.p2pPort, 'tcp')); + // Remove old discovered IP if one exists + if (this.discoveredP2pIp) { + const oldAddr = multiaddr(convertToMultiaddr(this.discoveredP2pIp, this.config.p2pPort, 'tcp')); addressManager.removeObservedAddr(oldAddr); } addressManager.addObservedAddr(newAddr); addressManager.confirmObservedAddr(newAddr); - this.config.p2pIp = ip; + // Store discovered IP + this.discoveredP2pIp = ip; this.logger.info('Public IP discovered via discv5', { ip }); }; this.peerDiscoveryService.on('ip:changed', this.ipChangedHandler); From d0fc7513fe6b6fbc02ac97d5b5777fb8de40674a Mon Sep 17 00:00:00 2001 From: spypsy Date: Fri, 20 Mar 2026 15:19:05 +0000 Subject: [PATCH 06/57] update tests --- .../p2p/src/services/libp2p/libp2p_service.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts index f8e133e1a8dc..3ad0372f0adf 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.test.ts @@ -1134,8 +1134,8 @@ describe('LibP2PService', () => { return { service, peerDiscovery, addressManager, config }; } - it('registers observed announce address and updates config when discv5 emits ip:changed', async () => { - const { service, peerDiscovery, addressManager, config } = createQueryForIpService(); + it('registers observed announce address when discv5 emits ip:changed', async () => { + const { service, peerDiscovery, addressManager } = createQueryForIpService(); const expectedAddr = multiaddr(convertToMultiaddr(firstIp, p2pPort, 'tcp')); await service.start(); @@ -1144,13 +1144,12 @@ describe('LibP2PService', () => { expect(addressManager.addObservedAddr).toHaveBeenCalledWith(expectedAddr); expect(addressManager.confirmObservedAddr).toHaveBeenCalledWith(expectedAddr); expect(addressManager.removeObservedAddr).not.toHaveBeenCalled(); - expect(config.p2pIp).toBe(firstIp); await service.stop(); }); it('removes previous observed address when ip:changed fires again with a new IP', async () => { - const { service, peerDiscovery, addressManager, config } = createQueryForIpService(); + const { service, peerDiscovery, addressManager } = createQueryForIpService(); const firstAddr = multiaddr(convertToMultiaddr(firstIp, p2pPort, 'tcp')); const secondAddr = multiaddr(convertToMultiaddr(secondIp, p2pPort, 'tcp')); @@ -1165,7 +1164,6 @@ describe('LibP2PService', () => { expect(addressManager.removeObservedAddr).toHaveBeenCalledWith(firstAddr); expect(addressManager.addObservedAddr).toHaveBeenCalledWith(secondAddr); expect(addressManager.confirmObservedAddr).toHaveBeenCalledWith(secondAddr); - expect(config.p2pIp).toBe(secondIp); await service.stop(); }); From 1b635f572166bc53c4e0b9e7de405ead0491d246 Mon Sep 17 00:00:00 2001 From: Michal Rzeszutko Date: Mon, 23 Mar 2026 11:24:02 +0100 Subject: [PATCH 07/57] chore: deflake n_tps benchmark for LOW_VALUE_TPS=2 (#21578) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Restore the `LOW_VALUE_TPS=2` benchmark scenario** that was removed in PR #19920 after repeated setup timeouts - **Parallelize wallet creation** in `n_tps.test.ts` by switching from `timesAsync` (sequential) to `timesParallel`, cutting setup time proportionally to the number of wallets - **Add retry logic with backoff** to `deployAccountWithDiagnostics` that tracks pending transactions across retries, avoiding nullifier conflicts when a previous tx is still in the mempool under chaos mesh conditions - **Set `SEQ_BUILD_CHECKPOINT_IF_EMPTY=true`** in the TPS scenario environment so the chain keeps advancing during quiet periods (setup/teardown) ## Details ### Root cause With `LOW_VALUE_TPS=2`, the test needs 3 wallets (2 low + 1 high). Each wallet deployment is an on-chain transaction that must be mined. Under chaos mesh conditions (20% packet drop + latency), sequential deployment of accounts frequently exceeds the test timeout. ### Changes **`yarn-project/end-to-end/src/spartan/n_tps.test.ts`** - Replace `timesAsync` with `timesParallel` for wallet creation — each wallet gets an independent PXE/node client, so no shared state - Add try/catch around `getBlockNumber` polling to avoid crashing on transient RPC errors during setup **`yarn-project/end-to-end/src/spartan/setup_test_wallets.ts`** - Wrap the deploy-and-wait cycle in `retry()` with exponential backoff (5 attempts, [1,2,4,8,16]s delays) - Use a 600s per-attempt `waitForTx` timeout, with retries providing cumulative wait of ~3000s - Track `sentTxHash` across retries: on retry, check if the previous tx was dropped before deciding to re-send. If the tx is still pending, wait for it again instead of sending a duplicate (which would cause `NULLIFIER_CONFLICT` under chaos mesh) - Pre-check `getContract()` before each retry to skip re-deploy if a previous attempt succeeded but `waitForTx` timed out - Check `receipt.isDropped()` explicitly to clear the tracked tx and trigger a fresh send **`spartan/environments/tps-scenario.env`** - Add `SEQ_BUILD_CHECKPOINT_IF_EMPTY=true` to match `prove-n-tps-fake.env` — ensures empty checkpoint blocks are produced in quiet slots, keeping the chain live **`spartan/bootstrap.sh`** - Re-add `2` to `low_value_tps_list` to restore the benchmark scenario ## Test plan - CI should run the restored `low_0_1__high_2` TPS benchmark scenario end-to-end - Existing lower TPS scenarios (0.1, 0.2, 0.5, 1) should be unaffected by the retry/parallel changes Fixes A-492 --- spartan/bootstrap.sh | 2 +- spartan/environments/tps-scenario.env | 1 + .../end-to-end/src/spartan/n_tps.test.ts | 19 +++-- .../src/spartan/setup_test_wallets.ts | 84 ++++++++++++------- 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/spartan/bootstrap.sh b/spartan/bootstrap.sh index b6a2399fc90c..3c82170817ea 100755 --- a/spartan/bootstrap.sh +++ b/spartan/bootstrap.sh @@ -142,7 +142,7 @@ function network_tests { function network_bench_cmds { local high_value_tps=0.1 - local low_value_tps_list=(0.1 0.2 0.5 1) + local low_value_tps_list=(0.1 0.2 0.5 1 2) for low_value_tps in "${low_value_tps_list[@]}"; do local low_label=${low_value_tps/./_} diff --git a/spartan/environments/tps-scenario.env b/spartan/environments/tps-scenario.env index 688fa0c6a4e5..b208c5c0fec3 100644 --- a/spartan/environments/tps-scenario.env +++ b/spartan/environments/tps-scenario.env @@ -69,6 +69,7 @@ AZTEC_LOCAL_EJECTION_THRESHOLD=90000000000000000000 SEQ_MAX_TX_PER_CHECKPOINT=15 # approx 0.2 TPS SEQ_MIN_TX_PER_BLOCK=1 +SEQ_BUILD_CHECKPOINT_IF_EMPTY=true # Override L1 tx utils bump percentages for scenario tests VALIDATOR_L1_PRIORITY_FEE_BUMP_PERCENTAGE=0 diff --git a/yarn-project/end-to-end/src/spartan/n_tps.test.ts b/yarn-project/end-to-end/src/spartan/n_tps.test.ts index 7c3f861f0885..71c2d019ac62 100644 --- a/yarn-project/end-to-end/src/spartan/n_tps.test.ts +++ b/yarn-project/end-to-end/src/spartan/n_tps.test.ts @@ -2,7 +2,7 @@ import { NO_WAIT } from '@aztec/aztec.js/contracts'; import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; import { type AztecNode, createAztecNodeClient } from '@aztec/aztec.js/node'; import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; -import { times, timesAsync } from '@aztec/foundation/collection'; +import { times, timesParallel } from '@aztec/foundation/collection'; import { randomBigInt } from '@aztec/foundation/crypto/random'; import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -270,12 +270,17 @@ describe('sustained N TPS test', () => { await retryUntil( async () => { - const blockNumber = await aztecNode.getBlockNumber(); - if (blockNumber > INITIAL_L2_BLOCK_NUM) { - return true; + try { + const blockNumber = await aztecNode.getBlockNumber(); + if (blockNumber > INITIAL_L2_BLOCK_NUM) { + return true; + } + logger.info('Waiting for the first block to mine...', { blockNumber, threshold: INITIAL_L2_BLOCK_NUM }); + return false; + } catch (err) { + logger.warn('Failed to get block number from RPC', { error: String(err) }); + return false; } - logger.info('Waiting for the first block to mine...'); - return false; }, 'get block number', 60 * 60 * 3, // wait up to 3 hours @@ -285,7 +290,7 @@ describe('sustained N TPS test', () => { const initialBlockNumber = await aztecNode.getBlockNumber(); logger.info('Initial block mined', { blockNumber: initialBlockNumber }); - testWallets = await timesAsync(lowValueAccounts + highValueAccounts, i => { + testWallets = await timesParallel(lowValueAccounts + highValueAccounts, i => { logger.info(`Creating wallet and pxe for wallet ${i + 1}/${lowValueAccounts + highValueAccounts}`); return createWalletAndAztecNodeClient(rpcUrl, config.REAL_VERIFIER, logger); }); diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index 814ba7fb747a..227c80557723 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -10,7 +10,7 @@ import type { Wallet } from '@aztec/aztec.js/wallet'; import { createEthereumChain } from '@aztec/ethereum/chain'; import { createExtendedL1Client } from '@aztec/ethereum/client'; import type { Logger } from '@aztec/foundation/log'; -import { retryUntil } from '@aztec/foundation/retry'; +import { makeBackoff, retry, retryUntil } from '@aztec/foundation/retry'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing'; @@ -137,38 +137,58 @@ async function deployAccountWithDiagnostics( estimateGas?: boolean, ): Promise { const deployMethod = await account.getDeployMethod(); - let txHash; - try { - let gasSettings; - if (estimateGas) { - const sim = await deployMethod.simulate({ from: AztecAddress.ZERO, fee: { paymentMethod } }); - gasSettings = sim.estimatedGas; - logger.info(`${accountLabel} estimated gas: DA=${gasSettings.gasLimits.daGas} L2=${gasSettings.gasLimits.l2Gas}`); - } - const deployResult = await deployMethod.send({ - from: AztecAddress.ZERO, - fee: { paymentMethod, gasSettings }, - wait: NO_WAIT, - }); - txHash = deployResult.txHash; - await waitForTx(aztecNode, txHash, { timeout: 2400 }); - logger.info(`${accountLabel} deployed at ${account.address}`); - } catch (error) { - const blockNumber = await aztecNode.getBlockNumber(); - let receipt; - try { - receipt = await aztecNode.getTxReceipt(txHash); - } catch { - receipt = 'unavailable'; - } - logger.error(`${accountLabel} deployment failed`, { - txHash: txHash.toString(), - receipt: JSON.stringify(receipt), - currentBlockNumber: blockNumber, - error: String(error), - }); - throw error; + + let gasSettings: any; + if (estimateGas) { + const sim = await deployMethod.simulate({ from: AztecAddress.ZERO, fee: { paymentMethod } }); + gasSettings = sim.estimatedGas; + logger.info(`${accountLabel} estimated gas: DA=${gasSettings.gasLimits.daGas} L2=${gasSettings.gasLimits.l2Gas}`); } + + // Track the tx hash across retries so we don't re-send when the previous tx is still pending. + let sentTxHash: { txHash: any } | undefined; + + await retry( + async () => { + // Check if already deployed (handles case where previous attempt succeeded but waitForTx timed out) + const existing = await aztecNode.getContract(account.address); + if (existing) { + logger.info(`${accountLabel} already deployed at ${account.address}, skipping`); + return; + } + + // If we already sent a tx, check if it was dropped before deciding to re-send. + if (sentTxHash) { + const prevReceipt = await aztecNode.getTxReceipt(sentTxHash.txHash); + if (prevReceipt.isDropped()) { + logger.info(`${accountLabel} previous tx ${sentTxHash.txHash} was dropped, re-sending`); + sentTxHash = undefined; + } else { + logger.info(`${accountLabel} previous tx ${sentTxHash.txHash} still pending, waiting again...`); + } + } + + if (!sentTxHash) { + const deployResult = await deployMethod.send({ + from: AztecAddress.ZERO, + fee: { paymentMethod, gasSettings }, + wait: NO_WAIT, + }); + sentTxHash = { txHash: deployResult.txHash }; + logger.info(`${accountLabel} tx sent`, { txHash: sentTxHash.txHash.toString() }); + } + + const receipt = await waitForTx(aztecNode, sentTxHash.txHash, { timeout: 600 }); + if (receipt.isDropped()) { + sentTxHash = undefined; + throw new Error(`${accountLabel} tx was dropped, retrying...`); + } + logger.info(`${accountLabel} deployed at ${account.address}`); + }, + `deploy ${accountLabel}`, + makeBackoff([1, 2, 4, 8, 16]), + logger, + ); } async function deployAccountsInBatches( From b0f1b85565f00d1887f4bc35010fc42920987dad Mon Sep 17 00:00:00 2001 From: Martin Verzilli Date: Mon, 23 Mar 2026 12:21:35 +0100 Subject: [PATCH 08/57] fix: explicitly handle initial block case for getBlockHashMembershipWitness (#21836) --- .../aztec-node/src/aztec-node/server.test.ts | 24 +++++++++++++++++++ .../aztec-node/src/aztec-node/server.ts | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 37f3adca164f..0304ac981a5d 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -641,6 +641,30 @@ describe('aztec node', () => { expect(result).toBe(snapshotMerkleTreeOps); }); }); + + describe('getBlockHashMembershipWitness', () => { + let initialHeader: BlockHeader; + + beforeEach(() => { + lastBlockNumber = BlockNumber(5); + initialHeader = BlockHeader.empty({ + globalVariables: GlobalVariables.empty({ blockNumber: BlockNumber.ZERO }), + }); + merkleTreeOps.getInitialHeader.mockReturnValue(initialHeader); + }); + + it('returns undefined when reference block is the initial block hash', async () => { + // The initial block (block 0) has an empty archive — no block hashes exist in it. + // getBlockHashMembershipWitness computes referenceBlockNumber - 1, which would be 0 - 1 = -1. + // This should return undefined (empty archive has no witnesses) rather than crashing. + const initialHash = await initialHeader.hash(); + const initialBlockHash = new BlockHash(initialHash); + const someBlockHash = BlockHash.random(); + + const result = await node.getBlockHashMembershipWitness(initialBlockHash, someBlockHash); + expect(result).toBeUndefined(); + }); + }); }); describe('simulatePublicCalls', () => { diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index be709fdc6557..bb885ea8b15a 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1053,6 +1053,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { // 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(referenceBlock); + if (referenceBlockNumber === BlockNumber.ZERO) { + // Block 0 (the initial block) has an empty archive, so no membership witness can exist. + return undefined; + } const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1)); const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); return pathAndIndex === undefined From 2343ec8d5b797c0168db0b5148d2432aaf2d25c0 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Mon, 23 Mar 2026 10:51:13 -0300 Subject: [PATCH 09/57] fix(aztec-up): narrow PATH cleanup regex to avoid removing user PATH entries (#21828) --- aztec-up/bin/0.0.1/aztec-install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aztec-up/bin/0.0.1/aztec-install b/aztec-up/bin/0.0.1/aztec-install index 7013482ae380..f67e1036a50f 100755 --- a/aztec-up/bin/0.0.1/aztec-install +++ b/aztec-up/bin/0.0.1/aztec-install @@ -194,7 +194,7 @@ function update_path_env_var { if grep -q '\.aztec' "$shell_profile" 2>/dev/null; then # Remove old aztec PATH entries. local tmp_file=$(mktemp) - grep -Ev 'export PATH=.*/\.aztec/' "$shell_profile" > "$tmp_file" || true + grep -Ev 'export PATH="\$(HOME/\.aztec/|PATH:.*\.aztec/)' "$shell_profile" > "$tmp_file" || true mv "$tmp_file" "$shell_profile" fi From 1b574021777963ac0b387b5f0ee5ab614c7cc126 Mon Sep 17 00:00:00 2001 From: Aztec Bot <49558828+AztecBot@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:07:05 -0400 Subject: [PATCH 10/57] fix(sequencer): use wall-clock timestamp for simulation when pipelining is disabled (#21888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes next-net being stuck at block 1. Every checkpoint proposal fails at the `eth_simulateV1` step with `"block timestamps must be in order"`. ## Root Cause `validateCheckpointForSubmission` (changed in PR #21026) uses `checkpoint.header.timestamp` as the simulation time override. This is the **slot start time**, which works when pipelining is enabled (targetSlot = currentSlot + 1, so the timestamp is ~48s in the future), but fails when pipelining is disabled (targetSlot = currentSlot, so the timestamp is the start of the current slot — always in the past by the time the simulation runs after ~24s of building). ## Fix Branch on pipelining: use `checkpoint.header.timestamp` when pipelining is enabled (always in the future), use `getNextL1SlotTimestamp()` (wall-clock-derived, always in the future) when pipelining is disabled. ```typescript const ts = this.epochCache.isProposerPipeliningEnabled() ? checkpoint.header.timestamp : getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants); ``` ## Evidence From next-net logs at slot 300: - `blockOverrides.time` = `0x69c11a91` = `checkpoint.header.timestamp + 1` = slot start + 1 - L1 latest block ≈ 24s ahead of slot start (2 Sepolia blocks of build time) - Pipelining disabled: `"target slot 319 during wall-clock slot 319"` - All 8 validators attest correctly, but no checkpoint lands on L1 Analysis: https://gist.github.com/AztecBot/6774618b735f2dd32004254e5170fb70 --- .../src/publisher/sequencer-publisher.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index 284b83c3870b..52f007683fb2 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -821,9 +821,14 @@ export class SequencerPublisher { attestationsAndSignersSignature: Signature, options: { forcePendingCheckpointNumber?: CheckpointNumber }, ): Promise { - // Anchor the simulation timestamp to the checkpoint's own slot start time - // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late. - const ts = checkpoint.header.timestamp; + // When pipelining, the checkpoint targets the next slot so its timestamp is in the future. + // Without pipelining, the checkpoint targets the current slot so its timestamp is in the past + // by the time we simulate (~24s of build time), causing eth_simulateV1 to reject it. + // In that case, use the latest L1 block timestamp + one ethereum slot, which is just ahead + // of L1 and still within the same L2 slot. + const ts = this.epochCache.isProposerPipeliningEnabled() + ? checkpoint.header.timestamp + : (await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration; const blobFields = checkpoint.toBlobFields(); const blobs = await getBlobsPerL1Block(blobFields); const blobInput = getPrefixedEthBlobCommitments(blobs); From 5c51ac3eef2884907e86966e0a0bb99cc4bf96c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 23 Mar 2026 13:19:08 -0300 Subject: [PATCH 11/57] fix: use anchor block on getL1ToL2MsgWitness (#21872) I made the param be optional in the fn since it has other unrelated callsites (e.g. the CLI). I also merged both node roundtrips into a single one. --- .../oracle/utility_execution_oracle.ts | 3 ++- .../stdlib/src/messaging/l1_to_l2_message.ts | 21 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 12422b44540e..04d40ac5f254 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -380,7 +380,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } /** - * Fetches a message from the executionStore, given its key. + * Returns the membership witness of an un-nullified L1 to L2 message. * @param contractAddress - Address of a contract by which the message was emitted. * @param messageHash - Hash of the message. * @param secret - Secret used to compute a nullifier. @@ -393,6 +393,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra contractAddress, messageHash, secret, + await this.anchorBlockHeader.hash(), ); return new MessageLoadOracleInputs(messageIndex, siblingPath); diff --git a/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts b/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts index a7e7392e4084..67590af73d66 100644 --- a/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts +++ b/yarn-project/stdlib/src/messaging/l1_to_l2_message.ts @@ -6,6 +6,7 @@ import { bufferToHex } from '@aztec/foundation/string'; import { SiblingPath } from '@aztec/foundation/trees'; import type { AztecAddress } from '../aztec-address/index.js'; +import type { BlockParameter } from '../block/block_parameter.js'; import { computeL1ToL2MessageNullifier } from '../hash/hash.js'; import type { AztecNode } from '../interfaces/aztec-node.js'; import { MerkleTreeId } from '../trees/merkle_tree_id.js'; @@ -79,20 +80,22 @@ export async function getNonNullifiedL1ToL2MessageWitness( contractAddress: AztecAddress, messageHash: Fr, secret: Fr, + referenceBlock: BlockParameter = 'latest', ): Promise<[bigint, SiblingPath]> { - const response = await node.getL1ToL2MessageMembershipWitness('latest', messageHash); - if (!response) { - throw new Error(`No L1 to L2 message found for message hash ${messageHash.toString()}`); - } + const messageNullifier = await computeL1ToL2MessageNullifier(contractAddress, messageHash, secret); - const [messageIndex, siblingPath] = response; + const [l1ToL2Response, nullifierResponse] = await Promise.all([ + node.getL1ToL2MessageMembershipWitness(referenceBlock, messageHash), + node.findLeavesIndexes(referenceBlock, MerkleTreeId.NULLIFIER_TREE, [messageNullifier]), + ]); - const messageNullifier = await computeL1ToL2MessageNullifier(contractAddress, messageHash, secret); + if (!l1ToL2Response) { + throw new Error(`No L1 to L2 message found for message hash ${messageHash.toString()}`); + } - const [nullifierIndex] = await node.findLeavesIndexes('latest', MerkleTreeId.NULLIFIER_TREE, [messageNullifier]); - if (nullifierIndex !== undefined) { + if (nullifierResponse[0] !== undefined) { throw new Error(`No non-nullified L1 to L2 message found for message hash ${messageHash.toString()}`); } - return [messageIndex, siblingPath]; + return l1ToL2Response; } From b325b433d3f6248d4e2607ad3100513e63aaf2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Mon, 23 Mar 2026 13:19:22 -0300 Subject: [PATCH 12/57] fix: make sure queries are not made ahead of the anchor block (#21874) This should prevent issues were users accidentally make queries in the future. Not sure if we should add a dedicated error message with docs link etc. --- .../oracle/utility_execution_oracle.ts | 77 ++++++++++++++----- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 04d40ac5f254..02a38c35b7ab 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -173,16 +173,18 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra /** * Fetches the index and sibling path of a leaf at a given block from the note hash tree. - * @param anchorBlockHash - The hash of a block that contains the note hash tree root in which to find the membership - * witness. + * @param blockHash - The hash of a block that contains the note hash tree root in which to find the + * membership witness. * @param noteHash - The note hash to find in the note hash tree. * @returns The membership witness containing the leaf index and sibling path */ public getNoteHashMembershipWitness( - anchorBlockHash: BlockHash, + blockHash: BlockHash, noteHash: Fr, ): Promise | undefined> { - return this.aztecNode.getNoteHashMembershipWitness(anchorBlockHash, noteHash); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getNoteHashMembershipWitness(blockHash, noteHash), + ); } /** @@ -191,16 +193,21 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * Block hashes are the leaves of the archive tree. Each time a new block is added to the chain, * its block hash is appended as a new leaf to the archive tree. * - * @param anchorBlockHash - The hash of a block that contains the archive tree root in which to find the membership + * @param referenceBlockHash - The hash of a block that contains the archive tree root in which to find the membership * witness. * @param blockHash - The block hash to find in the archive tree. * @returns The membership witness containing the leaf index and sibling path */ public getBlockHashMembershipWitness( - anchorBlockHash: BlockHash, + referenceBlockHash: BlockHash, blockHash: BlockHash, ): Promise | undefined> { - return this.aztecNode.getBlockHashMembershipWitness(anchorBlockHash, blockHash); + // Note that we validate that the reference block hash is at or before the anchor block - we don't test the block + // hash at all. If the block hash did not exist by the reference block hash, then the node will not return the + // membership witness as there is none. + return this.#queryWithBlockHashNotAfterAnchor(referenceBlockHash, () => + this.aztecNode.getBlockHashMembershipWitness(referenceBlockHash, blockHash), + ); } /** @@ -213,7 +220,9 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra blockHash: BlockHash, nullifier: Fr, ): Promise { - return this.aztecNode.getNullifierMembershipWitness(blockHash, nullifier); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getNullifierMembershipWitness(blockHash, nullifier), + ); } /** @@ -229,7 +238,9 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra blockHash: BlockHash, nullifier: Fr, ): Promise { - return this.aztecNode.getLowNullifierMembershipWitness(blockHash, nullifier); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getLowNullifierMembershipWitness(blockHash, nullifier), + ); } /** @@ -239,7 +250,9 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * @returns - The witness */ public getPublicDataWitness(blockHash: BlockHash, leafSlot: Fr): Promise { - return this.aztecNode.getPublicDataWitness(blockHash, leafSlot); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, () => + this.aztecNode.getPublicDataWitness(blockHash, leafSlot), + ); } /** @@ -406,25 +419,27 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * @param startStorageSlot - The starting storage slot. * @param numberOfElements - Number of elements to read from the starting storage slot. */ - public async storageRead( + public storageRead( blockHash: BlockHash, contractAddress: AztecAddress, startStorageSlot: Fr, numberOfElements: number, ) { - const slots = Array(numberOfElements) - .fill(0) - .map((_, i) => new Fr(startStorageSlot.value + BigInt(i))); + return this.#queryWithBlockHashNotAfterAnchor(blockHash, async () => { + const slots = Array(numberOfElements) + .fill(0) + .map((_, i) => new Fr(startStorageSlot.value + BigInt(i))); - const values = await Promise.all( - slots.map(storageSlot => this.aztecNode.getPublicStorageAt(blockHash, contractAddress, storageSlot)), - ); + const values = await Promise.all( + slots.map(storageSlot => this.aztecNode.getPublicStorageAt(blockHash, contractAddress, storageSlot)), + ); - this.logger.debug( - `Oracle storage read: slots=[${slots.map(slot => slot.toString()).join(', ')}] address=${contractAddress.toString()} values=[${values.join(', ')}]`, - ); + this.logger.debug( + `Oracle storage read: slots=[${slots.map(slot => slot.toString()).join(', ')}] address=${contractAddress.toString()} values=[${values.join(', ')}]`, + ); - return values; + return values; + }); } /** @@ -698,4 +713,24 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra public getOffchainEffects(): OffchainEffect[] { return this.offchainEffects; } + + /** Runs a query concurrently with a validation that the block hash is not ahead of the anchor block. */ + async #queryWithBlockHashNotAfterAnchor(blockHash: BlockHash, query: () => Promise): Promise { + const [response] = await Promise.all([ + query(), + (async () => { + const header = await this.aztecNode.getBlockHeader(blockHash); + if (!header) { + throw new Error(`Could not find block header for block hash ${blockHash}`); + } + + if (header.getBlockNumber() > this.anchorBlockHeader.getBlockNumber()) { + throw new Error( + `Made a node query with a reference block hash ${blockHash} with block number ${header.getBlockNumber()}, which is ahead of the anchor block number ${this.anchorBlockHeader.getBlockNumber()} (from anchor block hash ${await this.anchorBlockHeader.hash()}).`, + ); + } + })(), + ]); + return response; + } } From 32a57967f5b57d3de440c8919ff7df5e11a45a1a Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Mon, 23 Mar 2026 14:57:46 -0300 Subject: [PATCH 13/57] fix(aztec-up): strip leading `v` prefix from version strings (#21813) --- aztec-up/bin/0.0.1/aztec-install | 14 +++++++++++--- aztec-up/bin/0.0.1/aztec-up | 6 ++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/aztec-up/bin/0.0.1/aztec-install b/aztec-up/bin/0.0.1/aztec-install index f67e1036a50f..ae715adfb17d 100755 --- a/aztec-up/bin/0.0.1/aztec-install +++ b/aztec-up/bin/0.0.1/aztec-install @@ -30,12 +30,20 @@ VERSION=${VERSION:-0.0.1} # Install URI (root, not version-specific) INSTALL_URI="${INSTALL_URI:-https://install.aztec-labs.com}" +# Check if version string is valid semver +function is_semver { + local version="$1" + local semver_regex='^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.]+)?$' + [[ "$version" =~ $semver_regex ]] +} + # Resolve alias (like "nightly") to actual version number. function resolve_version { local version="$1" - local semver_regex='^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.]+)?$' - if [[ "$version" =~ $semver_regex ]]; then - echo "$version" + # Strip leading v from semver-like inputs (v0.85.0 -> 0.85.0), but not aliases (v4-nightly) + local stripped="${version#v}" + if is_semver "$stripped"; then + echo "$stripped" else local resolved if ! resolved=$(curl -fsSL "$INSTALL_URI/aliases/$version" 2>/dev/null); then diff --git a/aztec-up/bin/0.0.1/aztec-up b/aztec-up/bin/0.0.1/aztec-up index edf35680e404..c2c9611e768b 100755 --- a/aztec-up/bin/0.0.1/aztec-up +++ b/aztec-up/bin/0.0.1/aztec-up @@ -103,8 +103,10 @@ function is_semver { # Resolve alias (like "nightly") to actual version number function resolve_version { local version="$1" - if is_semver "$version"; then - echo "$version" + # Strip leading v from semver-like inputs (v0.85.0 -> 0.85.0), but not aliases (v4-nightly) + local stripped="${version#v}" + if is_semver "$stripped"; then + echo "$stripped" else # Fetch alias file to get actual version local resolved From fad6d5caa0e61c61fbb9d4512025adbe9b32cb55 Mon Sep 17 00:00:00 2001 From: Gregorio Juliana Date: Tue, 24 Mar 2026 08:00:43 +0100 Subject: [PATCH 14/57] fix: interactions clean up (#21933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR attempts to improve our interaction classes. Closes: https://github.com/AztecProtocol/aztec-packages/issues/21875 1. ) Addresses the aforementioned bug, where `BatchCall` wasn't forwarding the baked in interaction data (authwitnesses, capsules, extrahashedargs) 2. ) Removes the stale `returnReceipt` option from `DeployMethod`. This is a remnant from when we were just returning the deployed contract. Now that the return type is complex object, it just doesn't make sense to have extra complexity --------- Co-authored-by: Jan Beneš --- .../docs/resources/migration_notes.md | 22 ++++++ .../ts/recursive_verification/index.ts | 2 +- yarn-project/aztec.js/src/api/contract.ts | 13 +--- .../aztec.js/src/contract/batch_call.test.ts | 25 +++++++ .../aztec.js/src/contract/batch_call.ts | 22 +++++- .../aztec.js/src/contract/deploy_method.ts | 68 ++++--------------- .../src/wallet/deploy_account_method.ts | 4 +- .../client_flows/account_deployments.test.ts | 2 +- .../client_flows/client_flows_benchmark.ts | 48 +++++++------ .../bench/client_flows/deployments.test.ts | 2 +- .../bench/client_flows/storage_proof.test.ts | 3 +- .../src/composed/e2e_persistence.test.ts | 5 +- .../src/composed/ha/e2e_ha_full.test.ts | 4 -- .../end-to-end/src/e2e_2_pxes.test.ts | 5 +- .../end-to-end/src/e2e_avm_simulator.test.ts | 5 +- .../src/e2e_contract_updates.test.ts | 5 +- .../src/e2e_deploy_contract/legacy.test.ts | 16 ++--- .../src/e2e_fees/account_init.test.ts | 6 +- .../src/e2e_fees/gas_estimation.test.ts | 6 +- .../e2e_multi_validator_node.test.ts | 2 - .../end-to-end/src/e2e_simple.test.ts | 1 - .../src/fixtures/e2e_prover_test.ts | 6 +- .../end-to-end/src/fixtures/token_utils.ts | 5 +- .../src/spartan/setup_test_wallets.ts | 12 ++-- 24 files changed, 139 insertions(+), 150 deletions(-) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index b41bea27caa9..e349612b6216 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,27 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }` + +The `returnReceipt` option in deploy wait options has been removed. `DeployMethod.send()` now always returns an object with `contract`, `receipt`, and `instance` at the top level, provided the user waits for the transaction to be included. + +The `DeployTxReceipt` and `DeployWaitOptions` types have been removed. + +**Migration:** + +```diff +- const { +- receipt: { contract, instance }, +- } = await MyContract.deploy(wallet, ...args).send({ +- from: address, +- wait: { returnReceipt: true }, +- }); + ++ const { contract, instance } = await MyContract.deploy(wallet, ...args).send({ ++ from: address, ++ }); +``` + ### [aztec.js] `isContractInitialized` is now `initializationStatus` tri-state enum `ContractMetadata.isContractInitialized` has been renamed to `ContractMetadata.initializationStatus` and changed from `boolean | undefined` to a `ContractInitializationStatus` enum with values `INITIALIZED`, `UNINITIALIZED`, and `UNKNOWN`. @@ -86,6 +107,7 @@ The `scope` field in `ExecuteUtilityOptions` has been renamed to `scopes` and ch ``` **Impact**: Any code that calls `wallet.executeUtility` directly must update the options object. Wallets must update to adapt to the new interface + ### [Aztec.nr] `attempt_note_discovery` now takes two separate functions instead of one The `attempt_note_discovery` function (and related discovery functions like `do_sync_state`, `process_message_ciphertext`) now takes separate `compute_note_hash` and `compute_note_nullifier` arguments instead of a single combined `compute_note_hash_and_nullifier`. The corresponding type aliases are now `ComputeNoteHash` and `ComputeNoteNullifier` (instead of `ComputeNoteHashAndNullifier`). diff --git a/docs/examples/ts/recursive_verification/index.ts b/docs/examples/ts/recursive_verification/index.ts index bd14e5dec607..d675db600dda 100644 --- a/docs/examples/ts/recursive_verification/index.ts +++ b/docs/examples/ts/recursive_verification/index.ts @@ -37,7 +37,7 @@ export const setupWallet = async (): Promise => { try { // Create wallet with embedded PXE // The wallet manages accounts and connects to the node - let wallet = await EmbeddedWallet.create(NODE_URL); + let wallet = await EmbeddedWallet.create(NODE_URL, { ephemeral: true }); // Register the sponsored FPC so the wallet knows about it await wallet.registerContract(sponsoredFPC, SponsoredFPCContract.artifact); diff --git a/yarn-project/aztec.js/src/api/contract.ts b/yarn-project/aztec.js/src/api/contract.ts index 4443cae6d60c..b8cd3953e84c 100644 --- a/yarn-project/aztec.js/src/api/contract.ts +++ b/yarn-project/aztec.js/src/api/contract.ts @@ -10,16 +10,9 @@ * or can be queried via `simulate()`. * * ```ts - * // Deploy and get the contract instance directly (default behavior) - * const contract = await Contract.deploy(wallet, MyContractArtifact, [...constructorArgs]).send({ from: accountAddress }); + * // Deploy and get the contract, receipt, and instance + * const { contract, receipt, instance } = await Contract.deploy(wallet, MyContractArtifact, [...constructorArgs]).send({ from: accountAddress }); * console.log(`Contract deployed at ${contract.address}`); - * - * // Or get the full receipt with contract and instance - * const receipt = await Contract.deploy(wallet, MyContractArtifact, [...constructorArgs]).send({ - * from: accountAddress, - * wait: { returnReceipt: true } - * }); - * console.log(`Contract deployed at ${receipt.contract.address}`); * ``` * * ```ts @@ -74,8 +67,6 @@ export { type DeployOptions, type DeployResultMined, type DeployReturn, - type DeployTxReceipt, - type DeployWaitOptions, type DeployInteractionWaitOptions, DeployMethod, type RequestDeployOptions, diff --git a/yarn-project/aztec.js/src/contract/batch_call.test.ts b/yarn-project/aztec.js/src/contract/batch_call.test.ts index 189ba8734436..39f65adf2e96 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.test.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.test.ts @@ -1,8 +1,11 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; +import { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + Capsule, ExecutionPayload, + HashedValues, OFFCHAIN_MESSAGE_IDENTIFIER, type OffchainEffect, TxSimulationResult, @@ -405,5 +408,27 @@ describe('BatchCall', () => { expect(result.calls[1]).toEqual(payload.calls[0]); expect(mockPaymentMethod.getExecutionPayload).toHaveBeenCalledTimes(1); }); + + it('should propagate authWitnesses, capsules, and extraHashedArgs into the execution payload', async () => { + const contractAddress = await AztecAddress.random(); + const payload = createPrivateExecutionPayload('func', [Fr.random()], contractAddress); + + const authWitness = AuthWitness.random(); + const capsule = new Capsule(await AztecAddress.random(), Fr.random(), [Fr.random()]); + const extraHashedArgs = [HashedValues.random()]; + + batchCall = new BatchCall(wallet, [payload], extraHashedArgs); + // Inject authWitnesses and capsules into the interaction (as BaseContractInteraction exposes these) + (batchCall as any).authWitnesses = [authWitness]; + (batchCall as any).capsules = [capsule]; + + const result = await batchCall.request(); + + expect(result.calls).toHaveLength(1); + expect(result.calls[0]).toEqual(payload.calls[0]); + expect(result.authWitnesses).toContainEqual(authWitness); + expect(result.capsules).toContainEqual(capsule); + expect(result.extraHashedArgs).toContainEqual(extraHashedArgs[0]); + }); }); }); diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index d64b13e58ecf..596e3f6d1d99 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -1,5 +1,11 @@ import { type FunctionCall, FunctionType, decodeFromAbi } from '@aztec/stdlib/abi'; -import { ExecutionPayload, TxSimulationResult, UtilityExecutionResult, mergeExecutionPayloads } from '@aztec/stdlib/tx'; +import { + ExecutionPayload, + HashedValues, + TxSimulationResult, + UtilityExecutionResult, + mergeExecutionPayloads, +} from '@aztec/stdlib/tx'; import type { BatchedMethod, Wallet } from '../wallet/wallet.js'; import { BaseContractInteraction } from './base_contract_interaction.js'; @@ -19,6 +25,7 @@ export class BatchCall extends BaseContractInteraction { constructor( wallet: Wallet, protected interactions: (BaseContractInteraction | ExecutionPayload)[], + private extraHashedArgs: HashedValues[] = [], ) { super(wallet); } @@ -34,9 +41,18 @@ export class BatchCall extends BaseContractInteraction { const feeExecutionPayload = options.fee?.paymentMethod ? await options.fee.paymentMethod.getExecutionPayload() : undefined; + const { authWitnesses, capsules } = options; + + // Propagates the included authwitnesses, capsules, and extraHashedArgs potentially baked into the interaction + const initialExecutionPayload = new ExecutionPayload( + [], + this.authWitnesses.concat(authWitnesses ?? []), + this.capsules.concat(capsules ?? []), + this.extraHashedArgs, + ); const finalExecutionPayload = feeExecutionPayload - ? mergeExecutionPayloads([feeExecutionPayload, ...requests]) - : mergeExecutionPayloads([...requests]); + ? mergeExecutionPayloads([initialExecutionPayload, feeExecutionPayload, ...requests]) + : mergeExecutionPayloads([initialExecutionPayload, ...requests]); return finalExecutionPayload; } diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 5e6b278615fc..3f520e9155ee 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -30,7 +30,6 @@ import { type SimulationInteractionFeeOptions, type SimulationResult, type TxSendResultImmediate, - type TxSendResultMined, extractOffchainOutput, toProfileOptions, toSendOptions, @@ -38,22 +37,13 @@ import { } from './interaction_options.js'; import type { WaitOpts } from './wait_opts.js'; -/** - * Wait options specific to deployment transactions. - * Extends WaitOpts with a flag to return the full receipt instead of just the contract. - */ -export type DeployWaitOptions = WaitOpts & { - /** If true, return the full DeployTxReceipt instead of just the contract. Defaults to false. */ - returnReceipt?: boolean; -}; - /** * Type for wait options in deployment interactions. * - NO_WAIT symbol: Don't wait, return TxHash immediately - * - DeployWaitOptions: Wait with custom options + * - WaitOpts: Wait with custom options * - undefined: Wait with default options */ -export type DeployInteractionWaitOptions = NoWait | DeployWaitOptions | undefined; +export type DeployInteractionWaitOptions = NoWait | WaitOpts | undefined; /** * Options for deploying a contract on the Aztec network. @@ -96,7 +86,7 @@ export type DeployOptions = /** * Options for waiting for the transaction to be mined. * - undefined (default): wait with default options and return the contract instance - * - DeployWaitOptions: wait with custom options and return contract or receipt based on returnReceipt flag + * - WaitOpts: wait with custom options * - NO_WAIT: return TxHash immediately without waiting */ wait?: W; @@ -120,40 +110,20 @@ export type SimulateDeployOptions = Omit & { includeMetadata?: boolean; }; -/** Receipt for a deployment transaction with the deployed contract instance. */ -export type DeployTxReceipt = TxReceipt & { - /** Type-safe wrapper around the deployed contract instance, linked to the deployment wallet */ - contract: TContract; - /** The deployed contract instance with address and metadata. */ - instance: ContractInstanceWithAddress; -}; - -/** Wait options that request a full receipt instead of just the contract instance. */ -type WaitWithReturnReceipt = { - /** Request the full receipt instead of just the contract instance. */ - returnReceipt: true; -}; - -/** - * Represents the result type of deploying a contract. - * - If wait is NO_WAIT, returns TxHash immediately. - * - If wait has returnReceipt: true, returns DeployTxReceipt after waiting. - * - Otherwise (undefined or DeployWaitOptions without returnReceipt), returns TContract after waiting. - */ /** Result of deploying a contract when waiting for mining (default case). */ export type DeployResultMined = { /** The deployed contract instance. */ contract: TContract; + /** The deployed contract instance with address and metadata. */ + instance: ContractInstanceWithAddress; /** The deploy transaction receipt. */ - receipt: DeployTxReceipt; + receipt: TxReceipt; } & OffchainOutput; /** Conditional return type for deploy based on wait options. */ export type DeployReturn = W extends NoWait ? TxSendResultImmediate - : W extends WaitWithReturnReceipt - ? TxSendResultMined> - : DeployResultMined; + : DeployResultMined; /** * Contract interaction for deployment. @@ -234,20 +204,13 @@ export class DeployMethod extends } /** - * Converts DeployOptions to SendOptions, stripping out the returnReceipt flag if present. - * @param options - Deploy options with wait parameter - * @returns Send options with wait parameter + * Converts DeployOptions to SendOptions. + * @param options - Deploy options with wait parameter. */ protected convertDeployOptionsToSendOptions( options: DeployOptions, - // eslint-disable-next-line jsdoc/require-jsdoc - ): SendOptions { - return { - ...toSendOptions({ - ...options, - wait: options.wait as any, - }), - } as any; + ): SendOptions { + return toSendOptions({ ...options, wait: options.wait as any }) as any; } /** @@ -354,7 +317,7 @@ export class DeployMethod extends * By default, waits for the transaction to be mined and returns the deployed contract instance. * * @param options - An object containing various deployment options such as contractAddressSalt and from. - * @returns TxHash (if wait is NO_WAIT), TContract (if wait is undefined or doesn't have returnReceipt), or DeployTxReceipt (if wait.returnReceipt is true) + * @returns TxHash (if wait is NO_WAIT), or DeployResultMined with contract, receipt, and instance (otherwise) */ // Overload for when wait is not specified at all - returns the contract public override send(options: DeployOptionsWithoutWait): Promise>; @@ -383,12 +346,7 @@ export class DeployMethod extends const instance = await this.getInstance(options); const contract = this.postDeployCtor(instance, this.wallet) as TContract; - // Return full receipt if requested, otherwise just the contract - if (options.wait && typeof options.wait === 'object' && options.wait.returnReceipt) { - return { receipt: { ...receipt, contract, instance }, ...offchainOutput }; - } - - return { contract, receipt, ...offchainOutput }; + return { contract, receipt, instance, ...offchainOutput }; } /** diff --git a/yarn-project/aztec.js/src/wallet/deploy_account_method.ts b/yarn-project/aztec.js/src/wallet/deploy_account_method.ts index 73f57bb46c33..c77b45f8c338 100644 --- a/yarn-project/aztec.js/src/wallet/deploy_account_method.ts +++ b/yarn-project/aztec.js/src/wallet/deploy_account_method.ts @@ -23,7 +23,6 @@ import { NO_FROM, type ProfileInteractionOptions, } from '../contract/interaction_options.js'; -import type { WaitOpts } from '../contract/wait_opts.js'; import type { FeePaymentMethod } from '../fee/fee_payment_method.js'; import { AccountEntrypointMetaPaymentMethod } from './account_entrypoint_meta_payment_method.js'; import type { ProfileOptions, SendOptions, SimulateOptions, Wallet } from './index.js'; @@ -170,8 +169,7 @@ export class DeployAccountMethod exte protected override convertDeployOptionsToSendOptions( options: DeployOptions, - // eslint-disable-next-line jsdoc/require-jsdoc - ): SendOptions { + ): SendOptions { return super.convertDeployOptionsToSendOptions(this.injectContractAddressIntoScopes(options)); } diff --git a/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts b/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts index ef092df34035..ac234af7c3a9 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/account_deployments.test.ts @@ -92,7 +92,7 @@ describe('Deployment benchmark', () => { if (process.env.SANITY_CHECKS) { // Ensure we paid a fee - const { receipt } = await deploymentInteraction.send({ ...options, wait: { returnReceipt: true } }); + const { receipt } = await deploymentInteraction.send({ ...options }); expect(receipt.transactionFee!).toBeGreaterThan(0n); } }); diff --git a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts index 5f8a9b8ec908..fdd1d29c130d 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/client_flows_benchmark.ts @@ -250,11 +250,14 @@ export class ClientFlowsBenchmark { async applyDeployBananaToken() { this.logger.info('Applying banana token deployment'); - const { - receipt: { contract: bananaCoin, instance: bananaCoinInstance }, - } = await BananaCoin.deploy(this.adminWallet, this.adminAddress, 'BC', 'BC', 18n).send({ + const { contract: bananaCoin, instance: bananaCoinInstance } = await BananaCoin.deploy( + this.adminWallet, + this.adminAddress, + 'BC', + 'BC', + 18n, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); this.logger.info(`BananaCoin deployed at ${bananaCoin.address}`); this.bananaCoin = bananaCoin; @@ -263,11 +266,14 @@ export class ClientFlowsBenchmark { async applyDeployCandyBarToken() { this.logger.info('Applying candy bar token deployment'); - const { - receipt: { contract: candyBarCoin, instance: candyBarCoinInstance }, - } = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'CBC', 'CBC', 18n).send({ + const { contract: candyBarCoin, instance: candyBarCoinInstance } = await TokenContract.deploy( + this.adminWallet, + this.adminAddress, + 'CBC', + 'CBC', + 18n, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); this.logger.info(`CandyBarCoin deployed at ${candyBarCoin.address}`); this.candyBarCoin = candyBarCoin; @@ -280,11 +286,12 @@ export class ClientFlowsBenchmark { expect((await this.context.wallet.getContractMetadata(feeJuiceContract.address)).isContractPublished).toBe(true); const bananaCoin = this.bananaCoin; - const { - receipt: { contract: bananaFPC, instance: bananaFPCInstance }, - } = await FPCContract.deploy(this.adminWallet, bananaCoin.address, this.adminAddress).send({ + const { contract: bananaFPC, instance: bananaFPCInstance } = await FPCContract.deploy( + this.adminWallet, + bananaCoin.address, + this.adminAddress, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); this.logger.info(`BananaPay deployed at ${bananaFPC.address}`); @@ -348,20 +355,21 @@ export class ClientFlowsBenchmark { public async applyDeployAmm() { this.logger.info('Applying AMM deployment'); - const { - receipt: { contract: liquidityToken, instance: liquidityTokenInstance }, - } = await TokenContract.deploy(this.adminWallet, this.adminAddress, 'LPT', 'LPT', 18n).send({ + const { contract: liquidityToken, instance: liquidityTokenInstance } = await TokenContract.deploy( + this.adminWallet, + this.adminAddress, + 'LPT', + 'LPT', + 18n, + ).send({ from: this.adminAddress, - wait: { returnReceipt: true }, }); - const { - receipt: { contract: amm, instance: ammInstance }, - } = await AMMContract.deploy( + const { contract: amm, instance: ammInstance } = await AMMContract.deploy( this.adminWallet, this.bananaCoin.address, this.candyBarCoin.address, liquidityToken.address, - ).send({ from: this.adminAddress, wait: { returnReceipt: true } }); + ).send({ from: this.adminAddress }); this.logger.info(`AMM deployed at ${amm.address}`); await liquidityToken.methods.set_minter(amm.address, true).send({ from: this.adminAddress }); this.liquidityToken = liquidityToken; diff --git a/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts b/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts index f75f4d011751..7cf37f32763a 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/deployments.test.ts @@ -86,7 +86,7 @@ describe('Deployment benchmark', () => { if (process.env.SANITY_CHECKS) { // Ensure we paid a fee - const { receipt } = await deploymentInteraction.send({ ...options, wait: { returnReceipt: true } }); + const { receipt } = await deploymentInteraction.send({ ...options }); expect(receipt.transactionFee!).toBeGreaterThan(0n); } }); diff --git a/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts b/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts index c46f17ccf444..d4fcd63bb74d 100644 --- a/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts +++ b/yarn-project/end-to-end/src/bench/client_flows/storage_proof.test.ts @@ -34,9 +34,8 @@ describe('Storage proof benchmark', () => { await t.applyFPCSetup(); await t.applyDeploySponsoredFPC(); - const { receipt: deployed } = await StorageProofTestContract.deploy(t.adminWallet).send({ + const deployed = await StorageProofTestContract.deploy(t.adminWallet).send({ from: t.adminAddress, - wait: { returnReceipt: true }, }); storageProofContract = deployed.contract; storageProofInstance = deployed.instance; diff --git a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts index 3f60809d3f8f..31b9667d0190 100644 --- a/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_persistence.test.ts @@ -68,11 +68,8 @@ describe('Aztec persistence', () => { owner = initialFundedAccounts[0]; ownerAddress = owner.address; - const { - receipt: { contract, instance }, - } = await TokenBlacklistContract.deploy(wallet, ownerAddress).send({ + const { contract, instance } = await TokenBlacklistContract.deploy(wallet, ownerAddress).send({ from: ownerAddress, - wait: { returnReceipt: true }, }); contractInstance = instance; contractAddress = contract.address; diff --git a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts index eb779d1c82a0..93f674ed6a73 100644 --- a/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts +++ b/yarn-project/end-to-end/src/composed/ha/e2e_ha_full.test.ts @@ -326,7 +326,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, receipt, { @@ -445,7 +444,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, ownerAddress, 42).send({ from: ownerAddress, contractAddressSalt: Fr.random(), - wait: { returnReceipt: true }, }); expect(receipt.blockNumber).toBeDefined(); logger.info(`Transaction mined in block ${receipt.blockNumber}`); @@ -604,7 +602,6 @@ describe('HA Full Setup', () => { const receipt = await deployer.deploy(ownerAddress, ownerAddress, 201).send({ from: ownerAddress, contractAddressSalt: new Fr(201), - wait: { returnReceipt: true }, }); expect(receipt.receipt.blockNumber).toBeDefined(); const [block] = await aztecNode.getCheckpointedBlocks(receipt.receipt.blockNumber!, 1); @@ -647,7 +644,6 @@ describe('HA Full Setup', () => { const { receipt } = await deployer.deploy(ownerAddress, ownerAddress, i + 100).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(i + 100)), - wait: { returnReceipt: true }, }); expect(receipt.blockNumber).toBeDefined(); diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index fd4ee33f95bc..b9e15d0378cc 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -108,11 +108,8 @@ describe('e2e_2_pxes', () => { const deployChildContractViaServerA = async () => { logger.info(`Deploying Child contract...`); - const { - receipt: { instance }, - } = await ChildContract.deploy(walletA).send({ + const { instance } = await ChildContract.deploy(walletA).send({ from: accountAAddress, - wait: { returnReceipt: true }, }); logger.info('Child contract deployed'); diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index b40af1162298..58e7980edcea 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -36,11 +36,8 @@ describe('e2e_avm_simulator', () => { let secondAvmContract: AvmTestContract; beforeEach(async () => { - ({ - receipt: { contract: avmContract, instance: avmContractInstance }, - } = await AvmTestContract.deploy(wallet).send({ + ({ contract: avmContract, instance: avmContractInstance } = await AvmTestContract.deploy(wallet).send({ from: defaultAccountAddress, - wait: { returnReceipt: true }, })); ({ contract: secondAvmContract } = await AvmTestContract.deploy(wallet).send({ from: defaultAccountAddress })); }); diff --git a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts index e34cedae2e97..01342cd6cf66 100644 --- a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts +++ b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts @@ -110,12 +110,9 @@ describe('e2e_contract_updates', () => { } sequencer = maybeSequencer; - ({ - receipt: { contract, instance }, - } = await UpdatableContract.deploy(wallet, constructorArgs[0]).send({ + ({ contract, instance } = await UpdatableContract.deploy(wallet, constructorArgs[0]).send({ from: defaultAccountAddress, contractAddressSalt: salt, - wait: { returnReceipt: true }, })); const registerMethod = await publishContractClass(wallet, UpdatedContractArtifact); diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts index ae2c526cdbce..3764964f4f82 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/legacy.test.ts @@ -36,10 +36,8 @@ describe('e2e_deploy_contract legacy', () => { deployer: defaultAccountAddress, }); const deployer = new ContractDeployer(TestContractArtifact, wallet); - const { receipt } = await deployer - .deploy() - .send({ from: defaultAccountAddress, contractAddressSalt: salt, wait: { returnReceipt: true } }); - expect(receipt.contract.address).toEqual(deploymentData.address); + const { contract } = await deployer.deploy().send({ from: defaultAccountAddress, contractAddressSalt: salt }); + expect(contract.address).toEqual(deploymentData.address); const { instance, isContractPublished } = await wallet.getContractMetadata(deploymentData.address); expect(instance).toBeDefined(); expect(isContractPublished).toBe(true); @@ -65,11 +63,11 @@ describe('e2e_deploy_contract legacy', () => { for (let index = 0; index < 2; index++) { logger.info(`Deploying contract ${index + 1}...`); - const { receipt } = await deployer + const { contract: deployed } = await deployer .deploy() - .send({ from: defaultAccountAddress, contractAddressSalt: Fr.random(), wait: { returnReceipt: true } }); + .send({ from: defaultAccountAddress, contractAddressSalt: Fr.random() }); logger.info(`Sending TX to contract ${index + 1}...`); - await receipt.contract.methods + await deployed.methods .get_master_incoming_viewing_public_key(defaultAccountAddress) .send({ from: defaultAccountAddress }); } @@ -104,8 +102,8 @@ describe('e2e_deploy_contract legacy', () => { }; const [goodTxPromiseResult, badTxReceiptResult] = await Promise.allSettled([ - goodDeploy.send({ ...firstOpts, wait: { returnReceipt: true } }), - badDeploy.send({ ...secondOpts, wait: { dontThrowOnRevert: true, returnReceipt: true } }), + goodDeploy.send({ ...firstOpts }), + badDeploy.send({ ...secondOpts, wait: { dontThrowOnRevert: true } }), ]); expect(goodTxPromiseResult.status).toBe('fulfilled'); diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index bf2bc79020cd..253d6948cdfa 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -90,7 +90,7 @@ describe('e2e_fees account_init', () => { const [bobsInitialGas] = await t.getGasBalanceFn(bobsAddress); expect(bobsInitialGas).toEqual(mintAmount); - const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, wait: { returnReceipt: true } }); + const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM }); expect(tx.transactionFee!).toBeGreaterThan(0n); await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([bobsInitialGas - tx.transactionFee!]); @@ -102,7 +102,6 @@ describe('e2e_fees account_init', () => { const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, fee: { paymentMethod }, - wait: { returnReceipt: true }, }); expect(tx.transactionFee!).toBeGreaterThan(0n); await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([claim.claimAmount - tx.transactionFee!]); @@ -122,7 +121,6 @@ describe('e2e_fees account_init', () => { const { receipt: tx } = await bobsDeployMethod.send({ from: NO_FROM, fee: { paymentMethod }, - wait: { returnReceipt: true }, }); const actualFee = tx.transactionFee!; expect(actualFee).toBeGreaterThan(0n); @@ -152,7 +150,6 @@ describe('e2e_fees account_init', () => { from: NO_FROM, skipInstancePublication: false, fee: { paymentMethod }, - wait: { returnReceipt: true }, }); const actualFee = tx.transactionFee!; @@ -195,7 +192,6 @@ describe('e2e_fees account_init', () => { skipInstancePublication: true, skipInitialization: false, universalDeploy: true, - wait: { returnReceipt: true }, }); // alice paid in Fee Juice diff --git a/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts index b456cd167784..190dbf2b97aa 100644 --- a/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/gas_estimation.test.ts @@ -1,5 +1,4 @@ import type { AztecAddress } from '@aztec/aztec.js/addresses'; -import type { DeployTxReceipt } from '@aztec/aztec.js/contracts'; import { type FeePaymentMethod, PublicFeePaymentMethod } from '@aztec/aztec.js/fee'; import type { AztecNode } from '@aztec/aztec.js/node'; import type { Wallet } from '@aztec/aztec.js/wallet'; @@ -186,7 +185,6 @@ describe('e2e_fees gas_estimation', () => { from: aliceAddress, fee: { gasSettings: limits ? { ...gasSettings, ...limits } : gasSettings }, skipClassPublication: true, - wait: { returnReceipt: true }, }; }; @@ -201,10 +199,10 @@ describe('e2e_fees gas_estimation', () => { const estimatedGas = sim3.estimatedGas!; logGasEstimate(estimatedGas); - const [{ receipt: withEstimate }, { receipt: withoutEstimate }] = (await Promise.all([ + const [{ receipt: withEstimate }, { receipt: withoutEstimate }] = await Promise.all([ deployMethod().send(deployOpts(estimatedGas)), deployMethod().send(deployOpts()), - ])) as unknown as { receipt: DeployTxReceipt }[]; + ]); // Estimation should yield that teardown has no cost, so should send the tx with zero for teardown expect(withEstimate.transactionFee!).toEqual(withoutEstimate.transactionFee!); diff --git a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts index 53060168826d..9abc4c4b8b38 100644 --- a/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts +++ b/yarn-project/end-to-end/src/e2e_multi_validator/e2e_multi_validator_node.test.ts @@ -117,7 +117,6 @@ describe('e2e_multi_validator_node', () => { const { receipt: tx } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, tx, { provenTimeout: (config.aztecProofSubmissionEpochs + 1) * config.aztecEpochDuration * config.aztecSlotDuration, @@ -178,7 +177,6 @@ describe('e2e_multi_validator_node', () => { const { receipt: tx } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, tx, { provenTimeout: (config.aztecProofSubmissionEpochs + 1) * config.aztecEpochDuration * config.aztecSlotDuration, diff --git a/yarn-project/end-to-end/src/e2e_simple.test.ts b/yarn-project/end-to-end/src/e2e_simple.test.ts index e38df666d579..2943d35e83a4 100644 --- a/yarn-project/end-to-end/src/e2e_simple.test.ts +++ b/yarn-project/end-to-end/src/e2e_simple.test.ts @@ -75,7 +75,6 @@ describe('e2e_simple', () => { const { receipt: txReceipt } = await deployer.deploy(ownerAddress, sender, 1).send({ from: ownerAddress, contractAddressSalt: new Fr(BigInt(1)), - wait: { returnReceipt: true }, }); await waitForProven(aztecNode, txReceipt, { provenTimeout: (config.aztecProofSubmissionEpochs + 1) * config.aztecEpochDuration * config.aztecSlotDuration, diff --git a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts index 7dc8268ce083..2796891bd9ed 100644 --- a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts @@ -104,15 +104,13 @@ export class FullProverTest { await publicDeployAccounts(this.wallet, this.accounts.slice(0, 2)); this.logger.info('Applying base setup: deploying token contract'); - const { - receipt: { contract: asset, instance }, - } = await TokenContract.deploy( + const { contract: asset, instance } = await TokenContract.deploy( this.wallet, this.accounts[0], FullProverTest.TOKEN_NAME, FullProverTest.TOKEN_SYMBOL, FullProverTest.TOKEN_DECIMALS, - ).send({ from: this.accounts[0], wait: { returnReceipt: true } }); + ).send({ from: this.accounts[0] }); this.logger.verbose(`Token deployed to ${asset.address}`); this.fakeProofsAsset = asset; diff --git a/yarn-project/end-to-end/src/fixtures/token_utils.ts b/yarn-project/end-to-end/src/fixtures/token_utils.ts index a0d67993b7eb..fafa3f7d5b2c 100644 --- a/yarn-project/end-to-end/src/fixtures/token_utils.ts +++ b/yarn-project/end-to-end/src/fixtures/token_utils.ts @@ -6,11 +6,8 @@ import { TokenContract } from '@aztec/noir-contracts.js/Token'; export async function deployToken(wallet: Wallet, admin: AztecAddress, initialAdminBalance: bigint, logger: Logger) { logger.info(`Deploying Token contract...`); - const { - receipt: { contract, instance }, - } = await TokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send({ + const { contract, instance } = await TokenContract.deploy(wallet, admin, 'TokenName', 'TokenSymbol', 18).send({ from: admin, - wait: { returnReceipt: true }, }); if (initialAdminBalance > 0n) { diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index 6827bd11fd92..f92ffc0c1cfc 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -346,14 +346,18 @@ async function deployTokenAndMint( logger: Logger, ) { logger.verbose(`Deploying TokenContract...`); - const { - receipt: { contract: tokenContract }, - } = await TokenContract.deploy(wallet, admin, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS).send({ + const { contract: tokenContract } = await TokenContract.deploy( + wallet, + admin, + TOKEN_NAME, + TOKEN_SYMBOL, + TOKEN_DECIMALS, + ).send({ from: admin, fee: { paymentMethod, }, - wait: { timeout: 600, returnReceipt: true }, + wait: { timeout: 600 }, }); const tokenAddress = tokenContract.address; From 46ede19d52e10732a11e5a6c2972f84a656be36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Tue, 24 Mar 2026 17:06:44 +0700 Subject: [PATCH 15/57] fix(stdlib): decode `EthAddress`, `FunctionSelector` and wrapped field structs in `AbiDecoder` (#21926) --- .../test/abi_types_contract/src/main.nr | 29 ++++- .../end-to-end/src/e2e_abi_types.test.ts | 37 ++++++- yarn-project/stdlib/src/abi/decoder.test.ts | 68 +++++++++++- yarn-project/stdlib/src/abi/decoder.ts | 101 ++++-------------- .../stdlib/src/abi/function_selector.ts | 2 +- .../src/abi/function_signature_decoder.ts | 77 +++++++++++++ yarn-project/stdlib/src/abi/index.ts | 1 + 7 files changed, 233 insertions(+), 82 deletions(-) create mode 100644 yarn-project/stdlib/src/abi/function_signature_decoder.ts diff --git a/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr index 2952925403c6..df9a4145a001 100644 --- a/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/abi_types_contract/src/main.nr @@ -7,7 +7,14 @@ use aztec::macros::aztec; #[aztec] pub contract AbiTypes { - use aztec::{macros::functions::external, protocol::traits::{Deserialize, Serialize}}; + use aztec::{ + macros::functions::external, + protocol::{ + abis::function_selector::FunctionSelector, + address::EthAddress, + traits::{Deserialize, Serialize}, + }, + }; #[derive(Serialize, Deserialize, Eq)] pub struct CustomStruct { @@ -17,6 +24,11 @@ pub contract AbiTypes { pub z: i64, } + #[derive(Serialize, Deserialize, Eq)] + pub struct WrappedField { + pub inner: Field, + } + #[external("public")] fn return_public_parameters( a: bool, @@ -49,4 +61,19 @@ pub contract AbiTypes { ) -> (bool, Field, u64, i64, CustomStruct) { (a, b, c, d, e) } + + #[external("utility")] + unconstrained fn return_eth_address(addr: EthAddress) -> EthAddress { + addr + } + + #[external("utility")] + unconstrained fn return_function_selector(selector: FunctionSelector) -> FunctionSelector { + selector + } + + #[external("utility")] + unconstrained fn return_wrapped_field(wrapped: WrappedField) -> WrappedField { + wrapped + } } diff --git a/yarn-project/end-to-end/src/e2e_abi_types.test.ts b/yarn-project/end-to-end/src/e2e_abi_types.test.ts index a7702f78c720..0244327b214a 100644 --- a/yarn-project/end-to-end/src/e2e_abi_types.test.ts +++ b/yarn-project/end-to-end/src/e2e_abi_types.test.ts @@ -1,4 +1,6 @@ -import { AztecAddress } from '@aztec/aztec.js/addresses'; +import { FunctionSelector } from '@aztec/aztec.js/abi'; +import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; +import { Fr } from '@aztec/aztec.js/fields'; import type { Wallet } from '@aztec/aztec.js/wallet'; import { MAX_FIELD_VALUE } from '@aztec/constants'; import { AbiTypesContract } from '@aztec/noir-test-contracts.js/AbiTypes'; @@ -84,6 +86,39 @@ describe('AbiTypes', () => { ]); }); + it('decodes EthAddress return value', async () => { + const ethAddr = EthAddress.fromString('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'); + + const { result } = await abiTypesContract.methods + .return_eth_address(ethAddr) + .simulate({ from: defaultAccountAddress }); + + expect(result).toBeInstanceOf(EthAddress); + expect(result).toEqual(ethAddr); + }); + + it('decodes FunctionSelector return value', async () => { + const selector = FunctionSelector.fromField(new Fr(0xdeadbeefn)); + + const { result } = await abiTypesContract.methods + .return_function_selector(selector) + .simulate({ from: defaultAccountAddress }); + + expect(result).toBeInstanceOf(FunctionSelector); + expect(result).toEqual(selector); + }); + + it('decodes wrapped field struct as Fr', async () => { + const value = new Fr(42n); + + const { result } = await abiTypesContract.methods + .return_wrapped_field(42n) + .simulate({ from: defaultAccountAddress }); + + expect(result).toBeInstanceOf(Fr); + expect(result).toEqual(value); + }); + it('passes utility parameters', async () => { const { result: minResult } = await abiTypesContract.methods .return_utility_parameters(false, 0n, 0n, I64_MIN, { w: 0n, x: false, y: 0n, z: I64_MIN }) diff --git a/yarn-project/stdlib/src/abi/decoder.test.ts b/yarn-project/stdlib/src/abi/decoder.test.ts index 03bd867cd98f..6a19c8483296 100644 --- a/yarn-project/stdlib/src/abi/decoder.test.ts +++ b/yarn-project/stdlib/src/abi/decoder.test.ts @@ -1,8 +1,10 @@ import { Fr } from '@aztec/foundation/curves/bn254'; +import { EthAddress } from '@aztec/foundation/eth-address'; import { AztecAddress } from '../aztec-address/index.js'; import type { ABIParameterVisibility, FunctionArtifact } from './abi.js'; -import { decodeFromAbi, decodeFunctionSignature, decodeFunctionSignatureWithParameterNames } from './decoder.js'; +import { decodeFromAbi } from './decoder.js'; +import { decodeFunctionSignature, decodeFunctionSignatureWithParameterNames } from './function_signature_decoder.js'; describe('abi/decoder', () => { // Copied from noir-contracts/contracts/test_contract/target/Test.json @@ -325,4 +327,68 @@ describe('decoder', () => { expect(decoded).toBeUndefined(); }); + + it('decodes EthAddress struct as EthAddress instance', () => { + const field = new Fr(0xdeadbeefn); + const decoded = decodeFromAbi( + [ + { + kind: 'struct', + path: 'aztec::protocol_types::address::EthAddress', + fields: [{ name: 'inner', type: { kind: 'field' } }], + }, + ], + [field], + ); + + expect(decoded).toBeInstanceOf(EthAddress); + expect(decoded).toEqual(EthAddress.fromField(field)); + }); + + it('decodes wrapped field struct as Fr', () => { + const field = new Fr(42n); + const decoded = decodeFromAbi( + [ + { + kind: 'struct', + path: 'some::custom::WrappedType', + fields: [{ name: 'inner', type: { kind: 'field' } }], + }, + ], + [field], + ); + + expect(decoded).toBeInstanceOf(Fr); + expect(decoded).toEqual(field); + }); + + it('decodes EthAddress inside a larger struct', () => { + const addressField = new Fr(0x1234n); + const amountField = new Fr(100n); + const decoded = decodeFromAbi( + [ + { + kind: 'struct', + path: 'MyContract::MyEvent', + fields: [ + { + name: 'recipient', + type: { + kind: 'struct', + path: 'aztec::protocol_types::address::EthAddress', + fields: [{ name: 'inner', type: { kind: 'field' } }], + }, + }, + { name: 'amount', type: { kind: 'field' } }, + ], + }, + ], + [addressField, amountField], + ); + + expect(decoded).toEqual({ + recipient: EthAddress.fromField(addressField), + amount: 100n, + }); + }); }); diff --git a/yarn-project/stdlib/src/abi/decoder.ts b/yarn-project/stdlib/src/abi/decoder.ts index a373a8ac05e1..fb027c407db6 100644 --- a/yarn-project/stdlib/src/abi/decoder.ts +++ b/yarn-project/stdlib/src/abi/decoder.ts @@ -1,8 +1,17 @@ import { Fr } from '@aztec/foundation/curves/bn254'; +import { EthAddress } from '@aztec/foundation/eth-address'; import { AztecAddress } from '../aztec-address/index.js'; -import type { ABIParameter, ABIVariable, AbiType } from './abi.js'; -import { isAztecAddressStruct, isOptionStruct, parseSignedInt } from './utils.js'; +import type { AbiType } from './abi.js'; +import { FunctionSelector } from './function_selector.js'; +import { + isAztecAddressStruct, + isEthAddressStruct, + isFunctionSelectorStruct, + isOptionStruct, + isWrappedFieldStruct, + parseSignedInt, +} from './utils.js'; /** * The type of our decoded ABI. @@ -12,6 +21,9 @@ export type AbiDecoded = | boolean | string | AztecAddress + | EthAddress + | FunctionSelector + | Fr | AbiDecoded[] | { [key: string]: AbiDecoded } | undefined; @@ -58,6 +70,15 @@ class AbiDecoder { if (isAztecAddressStruct(abiType)) { return new AztecAddress(this.getNextField().toBuffer()); } + if (isEthAddressStruct(abiType)) { + return EthAddress.fromField(this.getNextField()); + } + if (isFunctionSelectorStruct(abiType)) { + return FunctionSelector.fromField(this.getNextField()); + } + if (isWrappedFieldStruct(abiType)) { + return this.getNextField(); + } if (isOptionStruct(abiType)) { const isSome = this.decodeNext(abiType.fields[0].type); const value = this.decodeNext(abiType.fields[1].type); @@ -123,79 +144,3 @@ class AbiDecoder { export function decodeFromAbi(typ: AbiType[], buffer: Fr[]) { return new AbiDecoder(typ, buffer.slice()).decode(); } - -/** - * Decodes the signature of a function from the name and parameters. - */ -export class FunctionSignatureDecoder { - private separator: string; - constructor( - private name: string, - private parameters: ABIParameter[], - private includeNames = false, - ) { - this.separator = includeNames ? ', ' : ','; - } - - /** - * Decodes a single function parameter type for the function signature. - * @param param - The parameter type to decode. - * @returns A string representing the parameter type. - */ - private getParameterType(param: AbiType): string { - switch (param.kind) { - case 'field': - return 'Field'; - case 'integer': - return param.sign === 'signed' ? `i${param.width}` : `u${param.width}`; - case 'boolean': - return 'bool'; - case 'array': - return `[${this.getParameterType(param.type)};${param.length}]`; - case 'string': - return `str<${param.length}>`; - case 'struct': - return `(${param.fields.map(field => `${this.decodeParameter(field)}`).join(this.separator)})`; - default: - throw new Error(`Unsupported type: ${param.kind}`); - } - } - - /** - * Decodes a single function parameter for the function signature. - * @param param - The parameter to decode. - * @returns A string representing the parameter type and optionally its name. - */ - private decodeParameter(param: ABIVariable): string { - const type = this.getParameterType(param.type); - return this.includeNames ? `${param.name}: ${type}` : type; - } - - /** - * Decodes all the parameters and build the function signature - * @returns The function signature. - */ - public decode(): string { - return `${this.name}(${this.parameters.map(param => this.decodeParameter(param)).join(this.separator)})`; - } -} - -/** - * Decodes a function signature from the name and parameters. - * @param name - The name of the function. - * @param parameters - The parameters of the function. - * @returns - The function signature. - */ -export function decodeFunctionSignature(name: string, parameters: ABIParameter[]) { - return new FunctionSignatureDecoder(name, parameters).decode(); -} - -/** - * Decodes a function signature from the name and parameters including parameter names. - * @param name - The name of the function. - * @param parameters - The parameters of the function. - * @returns - The user-friendly function signature. - */ -export function decodeFunctionSignatureWithParameterNames(name: string, parameters: ABIParameter[]) { - return new FunctionSignatureDecoder(name, parameters, true).decode(); -} diff --git a/yarn-project/stdlib/src/abi/function_selector.ts b/yarn-project/stdlib/src/abi/function_selector.ts index 860e11cf4713..3110681ea155 100644 --- a/yarn-project/stdlib/src/abi/function_selector.ts +++ b/yarn-project/stdlib/src/abi/function_selector.ts @@ -6,7 +6,7 @@ import { type ZodFor, hexSchemaFor } from '@aztec/foundation/schemas'; import { BufferReader, FieldReader, TypeRegistry } from '@aztec/foundation/serialize'; import type { ABIParameter } from './abi.js'; -import { decodeFunctionSignature } from './decoder.js'; +import { decodeFunctionSignature } from './function_signature_decoder.js'; import { Selector } from './selector.js'; /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ diff --git a/yarn-project/stdlib/src/abi/function_signature_decoder.ts b/yarn-project/stdlib/src/abi/function_signature_decoder.ts new file mode 100644 index 000000000000..0a0358312d5e --- /dev/null +++ b/yarn-project/stdlib/src/abi/function_signature_decoder.ts @@ -0,0 +1,77 @@ +import type { ABIParameter, ABIVariable, AbiType } from './abi.js'; + +/** + * Decodes the signature of a function from the name and parameters. + */ +export class FunctionSignatureDecoder { + private separator: string; + constructor( + private name: string, + private parameters: ABIParameter[], + private includeNames = false, + ) { + this.separator = includeNames ? ', ' : ','; + } + + /** + * Decodes a single function parameter type for the function signature. + * @param param - The parameter type to decode. + * @returns A string representing the parameter type. + */ + private getParameterType(param: AbiType): string { + switch (param.kind) { + case 'field': + return 'Field'; + case 'integer': + return param.sign === 'signed' ? `i${param.width}` : `u${param.width}`; + case 'boolean': + return 'bool'; + case 'array': + return `[${this.getParameterType(param.type)};${param.length}]`; + case 'string': + return `str<${param.length}>`; + case 'struct': + return `(${param.fields.map(field => `${this.decodeParameter(field)}`).join(this.separator)})`; + default: + throw new Error(`Unsupported type: ${param.kind}`); + } + } + + /** + * Decodes a single function parameter for the function signature. + * @param param - The parameter to decode. + * @returns A string representing the parameter type and optionally its name. + */ + private decodeParameter(param: ABIVariable): string { + const type = this.getParameterType(param.type); + return this.includeNames ? `${param.name}: ${type}` : type; + } + + /** + * Decodes all the parameters and build the function signature + * @returns The function signature. + */ + public decode(): string { + return `${this.name}(${this.parameters.map(param => this.decodeParameter(param)).join(this.separator)})`; + } +} + +/** + * Decodes a function signature from the name and parameters. + * @param name - The name of the function. + * @param parameters - The parameters of the function. + * @returns - The function signature. + */ +export function decodeFunctionSignature(name: string, parameters: ABIParameter[]) { + return new FunctionSignatureDecoder(name, parameters).decode(); +} + +/** + * Decodes a function signature from the name and parameters including parameter names. + * @param name - The name of the function. + * @param parameters - The parameters of the function. + * @returns - The user-friendly function signature. + */ +export function decodeFunctionSignatureWithParameterNames(name: string, parameters: ABIParameter[]) { + return new FunctionSignatureDecoder(name, parameters, true).decode(); +} diff --git a/yarn-project/stdlib/src/abi/index.ts b/yarn-project/stdlib/src/abi/index.ts index bdfd1ea92dd7..54f68f25efb7 100644 --- a/yarn-project/stdlib/src/abi/index.ts +++ b/yarn-project/stdlib/src/abi/index.ts @@ -1,6 +1,7 @@ export * from './abi.js'; export * from './buffer.js'; export * from './decoder.js'; +export * from './function_signature_decoder.js'; export * from './encoder.js'; export * from './authorization_selector.js'; export * from './event_metadata_definition.js'; From cbea6bd6b6d7d0438ada5005c709865230a8b14e Mon Sep 17 00:00:00 2001 From: Aztec Bot <49558828+AztecBot@users.noreply.github.com> Date: Tue, 24 Mar 2026 06:47:00 -0400 Subject: [PATCH 16/57] fix: disable caching of error responses (300-499) on R2 custom domain (#21939) --- spartan/terraform/cloudflare/main.tf | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/spartan/terraform/cloudflare/main.tf b/spartan/terraform/cloudflare/main.tf index b1c12316ee9c..0d73a8d90d64 100644 --- a/spartan/terraform/cloudflare/main.tf +++ b/spartan/terraform/cloudflare/main.tf @@ -37,6 +37,35 @@ resource "cloudflare_r2_custom_domain" "aztec_labs_snapshots_com" { enabled = true } +# Do not cache 404s +resource "cloudflare_ruleset" "cache_settings" { + zone_id = var.R2_ZONE_ID + kind = "zone" + name = "R2 cache settings" + phase = "http_request_cache_settings" + + rules = [ + { + ref = "no_cache_404" + description = "Do not cache 404 responses for R2 custom domain" + expression = "(http.host eq \"${var.DOMAIN}\")" + action = "set_cache_settings" + action_parameters = { + cache = true + edge_ttl = { + mode = "respect_origin" + status_code_ttl = [ + { + status_code = 404 + value = 0 + } + ] + } + } + } + ] +} + locals { top_level_folders = toset([ "devnet", From 35ac53209bf3fc2cb17dcec364a6878ab7ebd7f4 Mon Sep 17 00:00:00 2001 From: Esau Date: Tue, 24 Mar 2026 18:45:17 +0800 Subject: [PATCH 17/57] init ab --- docs/examples/bootstrap.sh | 5 +++++ docs/examples/ts/bootstrap.sh | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/docs/examples/bootstrap.sh b/docs/examples/bootstrap.sh index efe8d2f6e368..b1bc676d6041 100755 --- a/docs/examples/bootstrap.sh +++ b/docs/examples/bootstrap.sh @@ -11,6 +11,11 @@ export STRIP_AZTEC_NR_PREFIX=${STRIP_AZTEC_NR_PREFIX:-"$REPO_ROOT/noir-projects/ export BB_HASH=${BB_HASH:-$("$REPO_ROOT/barretenberg/cpp/bootstrap.sh" hash)} export NOIR_HASH=${NOIR_HASH:-$("$REPO_ROOT/noir/bootstrap.sh" hash)} +# Safety net: ensure all TS example yarn.lock files are empty on exit. +# Both validate-ts and execute-examples (via Docker volume mount) can populate +# these files, and their per-project cleanup may not run if processes are killed. +trap 'for lf in "$REPO_ROOT"/docs/examples/ts/*/yarn.lock; do [ -f "$lf" ] && > "$lf"; done' EXIT + function compile-circuits { echo_header "Compiling vanilla Noir circuits" local CIRCUITS_DIR="$REPO_ROOT/docs/examples/circuits" diff --git a/docs/examples/ts/bootstrap.sh b/docs/examples/ts/bootstrap.sh index 606d2e5f3767..302614922013 100755 --- a/docs/examples/ts/bootstrap.sh +++ b/docs/examples/ts/bootstrap.sh @@ -11,6 +11,10 @@ export BUILDER_CLI="$REPO_ROOT/yarn-project/builder/dest/bin/cli.js" # Set parallel flags for concurrent validation export PARALLEL_FLAGS="-j${PARALLELISM:-4} --halt now,fail=1" +# Ensure all yarn.lock files are empty on exit. The per-project cleanup trap +# handles the normal case, but parallel's --halt can kill jobs before their trap runs. +trap 'for lf in */yarn.lock; do [ -f "$lf" ] && > "$lf"; done' EXIT + # Validate config.yaml structure before processing validate_config() { local config_file=$1 From 227895e56d67e23015761dc3c876f1e29d7220b1 Mon Sep 17 00:00:00 2001 From: AztecBot <49558828+AztecBot@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:55:56 +0000 Subject: [PATCH 18/57] fix: correct LMDB has() logic inversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes a logic error in `LMDBStoreWrapper::has()` where `std::find` result was compared against `values->begin()` instead of `values->end()`, causing inverted existence checks. **Bug**: `std::find` returns `end()` when not found, but the code compared against `begin()`: - Value found at position 0: `begin() != begin()` → **false** (wrong) - Value not found: `end() != begin()` → **true** (wrong) **Fix**: Changed `!= values->begin()` to `!= values->end()`. **Refactor**: Moved the `has()` logic from the NAPI wrapper (`LMDBStoreWrapper`) down to `LMDBStore` so it can be tested without Node.js. The wrapper now delegates to `_store->has()`. **Tests**: Added 7 new tests covering: - Missing key → false - Existing key (key-only check) → true - Value present → true - Value missing → false - All-of semantics (all requested values must be present) - Mixed entries in a single call - Duplicate key deduplication All 47 lmdblib tests pass (40 existing + 7 new). Resolves https://linear.app/aztec-labs/issue/A-846/claude-lmdb-logic-error --- .../src/barretenberg/lmdblib/lmdb_store.cpp | 47 +++++++ .../src/barretenberg/lmdblib/lmdb_store.hpp | 1 + .../barretenberg/lmdblib/lmdb_store.test.cpp | 129 ++++++++++++++++++ .../lmdb_store/lmdb_store_wrapper.cpp | 47 +------ 4 files changed, 178 insertions(+), 46 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp index 5e02b62258c0..ef886c47e147 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp @@ -5,10 +5,12 @@ #include "barretenberg/lmdblib/lmdb_write_transaction.hpp" #include "barretenberg/lmdblib/types.hpp" #include "lmdb.h" +#include #include #include #include #include +#include #include namespace bb::lmdblib { @@ -176,6 +178,51 @@ void LMDBStore::get(KeysVector& keys, OptionalValuesVector& values, LMDBDatabase } } +void LMDBStore::has(const KeyOptionalValuesVector& entries, std::vector& results, const std::string& name) +{ + auto string_cmp = [](const Key& a, const Key& b) { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); + }; + + std::set key_set(string_cmp); + for (const auto& entry : entries) { + key_set.insert(entry.first); + } + + KeysVector keys(key_set.begin(), key_set.end()); + OptionalValuesVector vals; + get(keys, vals, name); + + results.reserve(entries.size()); + + for (const auto& entry : entries) { + const auto& key = entry.first; + const auto& requested_values = entry.second; + + const auto key_it = std::find(keys.begin(), keys.end(), key); + if (key_it == keys.end()) { + results.push_back(false); + continue; + } + + const auto& values = vals[static_cast(key_it - keys.begin())]; + + if (!values.has_value()) { + results.push_back(false); + continue; + } + + if (!requested_values.has_value()) { + results.push_back(true); + continue; + } + + results.push_back(std::all_of(requested_values->begin(), requested_values->end(), [&](const auto& val) { + return std::find(values->begin(), values->end(), val) != values->end(); + })); + } +} + LMDBStore::Cursor::Ptr LMDBStore::create_cursor(ReadTransaction::SharedPtr tx, const std::string& dbName) { Database::SharedPtr db = get_database(dbName); diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp index a65a2324f805..ffcf8d8cdc33 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp @@ -47,6 +47,7 @@ class LMDBStore : public LMDBStoreBase { void put(std::vector& data); void get(KeysVector& keys, OptionalValuesVector& values, const std::string& name); + void has(const KeyOptionalValuesVector& entries, std::vector& results, const std::string& name); Cursor::Ptr create_cursor(ReadTransaction::SharedPtr tx, const std::string& dbName); diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp index a46e28eee454..75c677adc14b 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp @@ -1372,3 +1372,132 @@ TEST_F(LMDBStoreTest, can_read_data_from_multiple_threads) } } } + +TEST_F(LMDBStoreTest, has_returns_false_for_missing_key) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name); + + KeyOptionalValuesVector entries = { { get_key(0), std::nullopt } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 1UL); + EXPECT_FALSE(results[0]); +} + +TEST_F(LMDBStoreTest, has_returns_true_for_existing_key) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name); + write_test_data({ name }, 3, 1, *store); + + KeyOptionalValuesVector entries = { { get_key(0), std::nullopt }, { get_key(1), std::nullopt } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 2UL); + EXPECT_TRUE(results[0]); + EXPECT_TRUE(results[1]); +} + +TEST_F(LMDBStoreTest, has_returns_true_when_value_exists) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 2, 3, *store); + + // Check that key 0 has value (0, 0) + ValuesVector requested = { get_value(0, 0) }; + KeyOptionalValuesVector entries = { { get_key(0), requested } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 1UL); + EXPECT_TRUE(results[0]); +} + +TEST_F(LMDBStoreTest, has_returns_false_when_value_missing) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 2, 3, *store); + + // Check for a value that doesn't exist under key 0 + ValuesVector requested = { get_value(99, 99) }; + KeyOptionalValuesVector entries = { { get_key(0), requested } }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 1UL); + EXPECT_FALSE(results[0]); +} + +TEST_F(LMDBStoreTest, has_checks_all_requested_values) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 1, 3, *store); + + // All values present + ValuesVector all_present = { get_value(0, 0), get_value(0, 1), get_value(0, 2) }; + KeyOptionalValuesVector entries_all = { { get_key(0), all_present } }; + std::vector results_all; + store->has(entries_all, results_all, name); + EXPECT_TRUE(results_all[0]); + + // One value missing + ValuesVector one_missing = { get_value(0, 0), get_value(99, 99) }; + KeyOptionalValuesVector entries_missing = { { get_key(0), one_missing } }; + std::vector results_missing; + store->has(entries_missing, results_missing, name); + EXPECT_FALSE(results_missing[0]); +} + +TEST_F(LMDBStoreTest, has_handles_mixed_entries) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name, true); + write_test_data({ name }, 3, 2, *store); + + KeyOptionalValuesVector entries = { + { get_key(0), std::nullopt }, // key exists, no value check -> true + { get_key(99), std::nullopt }, // key missing -> false + { get_key(1), ValuesVector{ get_value(1, 0) } }, // key exists, value present -> true + { get_key(2), ValuesVector{ get_value(99, 99) } }, // key exists, value missing -> false + }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 4UL); + EXPECT_TRUE(results[0]); + EXPECT_FALSE(results[1]); + EXPECT_TRUE(results[2]); + EXPECT_FALSE(results[3]); +} + +TEST_F(LMDBStoreTest, has_deduplicates_keys) +{ + LMDBStore::Ptr store = create_store(); + const std::string name = "Test Database"; + store->open_database(name); + write_test_data({ name }, 2, 1, *store); + + // Same key appearing twice with different value checks + KeyOptionalValuesVector entries = { + { get_key(0), std::nullopt }, + { get_key(0), ValuesVector{ get_value(0, 0) } }, + }; + std::vector results; + store->has(entries, results, name); + + EXPECT_EQ(results.size(), 2UL); + EXPECT_TRUE(results[0]); + EXPECT_TRUE(results[1]); +} diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp index b98dcb892409..94b3ca562283 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp @@ -3,10 +3,8 @@ #include "barretenberg/lmdblib/types.hpp" #include "barretenberg/nodejs_module/lmdb_store/lmdb_store_message.hpp" #include "napi.h" -#include #include #include -#include #include #include #include @@ -117,52 +115,9 @@ GetResponse LMDBStoreWrapper::get(const GetRequest& req) HasResponse LMDBStoreWrapper::has(const HasRequest& req) { - auto string_cmp = [](const std::vector& a, const std::vector& b) { - return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); - }; - verify_store(); - std::set key_set(string_cmp); - for (const auto& entry : req.entries) { - key_set.insert(entry.first); - } - - lmdblib::KeysVector keys(key_set.begin(), key_set.end()); - lmdblib::OptionalValuesVector vals; - _store->get(keys, vals, req.db); - std::vector exists; - - for (const auto& entry : req.entries) { - const auto& key = entry.first; - const auto& requested_values = entry.second; - - const auto& key_it = std::find(keys.begin(), keys.end(), key); - if (key_it == keys.end()) { - // this shouldn't happen. It means we missed a key when we created the key_set - exists.push_back(false); - continue; - } - - // should be fine to convert this to an index in the array? - const auto& values = vals[static_cast(key_it - keys.begin())]; - - if (!values.has_value()) { - exists.push_back(false); - continue; - } - - // client just wanted to know if the key exists - if (!requested_values.has_value()) { - exists.push_back(true); - continue; - } - - exists.push_back(std::all_of(requested_values->begin(), requested_values->end(), [&](const auto& val) { - return std::find(values->begin(), values->end(), val) != values->begin(); - })); - } - + _store->has(req.entries, exists, req.db); return { exists }; } From e81be61ff45711642ef32f92bb829eca93a63fec Mon Sep 17 00:00:00 2001 From: thunkar Date: Tue, 24 Mar 2026 12:52:40 +0100 Subject: [PATCH 19/57] wip --- yarn-project/wallet-sdk/package.json | 4 + yarn-project/wallet-sdk/src/crypto.ts | 102 ++++++ .../handlers/iframe_connection_handler.ts | 330 +++++++++++++++++ .../wallet-sdk/src/iframe/handlers/index.ts | 7 + .../src/iframe/provider/iframe_discovery.ts | 176 +++++++++ .../src/iframe/provider/iframe_provider.ts | 334 ++++++++++++++++++ .../src/iframe/provider/iframe_wallet.ts | 242 +++++++++++++ .../wallet-sdk/src/iframe/provider/index.ts | 3 + yarn-project/wallet-sdk/src/manager/types.ts | 3 +- .../wallet-sdk/src/manager/wallet_manager.ts | 67 +++- yarn-project/wallet-sdk/src/types.ts | 8 + 11 files changed, 1261 insertions(+), 15 deletions(-) create mode 100644 yarn-project/wallet-sdk/src/iframe/handlers/iframe_connection_handler.ts create mode 100644 yarn-project/wallet-sdk/src/iframe/handlers/index.ts create mode 100644 yarn-project/wallet-sdk/src/iframe/provider/iframe_discovery.ts create mode 100644 yarn-project/wallet-sdk/src/iframe/provider/iframe_provider.ts create mode 100644 yarn-project/wallet-sdk/src/iframe/provider/iframe_wallet.ts create mode 100644 yarn-project/wallet-sdk/src/iframe/provider/index.ts diff --git a/yarn-project/wallet-sdk/package.json b/yarn-project/wallet-sdk/package.json index b48c7af514f4..1c1f2927d540 100644 --- a/yarn-project/wallet-sdk/package.json +++ b/yarn-project/wallet-sdk/package.json @@ -7,6 +7,8 @@ "./base-wallet": "./dest/base-wallet/index.js", "./extension/handlers": "./dest/extension/handlers/index.js", "./extension/provider": "./dest/extension/provider/index.js", + "./iframe/handlers": "./dest/iframe/handlers/index.js", + "./iframe/provider": "./dest/iframe/provider/index.js", "./crypto": "./dest/crypto.js", "./types": "./dest/types.js", "./manager": "./dest/manager/index.js" @@ -16,6 +18,8 @@ "./src/base-wallet/index.ts", "./src/extension/handlers/index.ts", "./src/extension/provider/index.ts", + "./src/iframe/handlers/index.ts", + "./src/iframe/provider/index.ts", "./src/crypto.ts", "./src/types.ts", "./src/manager/index.ts" diff --git a/yarn-project/wallet-sdk/src/crypto.ts b/yarn-project/wallet-sdk/src/crypto.ts index 976628b50618..f1739d93e9e7 100644 --- a/yarn-project/wallet-sdk/src/crypto.ts +++ b/yarn-project/wallet-sdk/src/crypto.ts @@ -497,3 +497,105 @@ export function hashToEmoji(hash: string, count: number = DEFAULT_EMOJI_GRID_SIZ } return emojis.join(''); } + +// ─── Passphrase-based encryption (PBKDF2 + AES-256-GCM) ─────────────────── + +/** Default PBKDF2 iteration count. High to compensate for short PINs (~1-2s on modern hardware). */ +const DEFAULT_PBKDF2_ITERATIONS = 2_000_000; +const PBKDF2_SALT_BYTES = 16; +const PBKDF2_IV_BYTES = 12; + +/** + * Derives an AES-256-GCM key from a passphrase using PBKDF2-SHA256. + * + * @param passphrase - The user-provided passphrase or PIN + * @param salt - Random salt bytes + * @param iterations - PBKDF2 iteration count (default: 2,000,000) + * @returns An AES-256-GCM CryptoKey + */ +export async function deriveKeyFromPassphrase( + passphrase: string, + salt: Uint8Array, + iterations: number = DEFAULT_PBKDF2_ITERATIONS, +): Promise { + const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, [ + 'deriveKey', + ]); + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'], + ); +} + +/** + * Encrypts arbitrary bytes with a passphrase using PBKDF2 + AES-256-GCM. + * + * Output layout: `[salt (16)] [iv (12)] [ciphertext (...)]` + * + * @param plaintext - Data to encrypt + * @param passphrase - User passphrase or PIN + * @param iterations - PBKDF2 iteration count (default: 2,000,000) + * @returns A Uint8Array containing salt + iv + ciphertext + */ +export async function encryptWithPassphrase( + plaintext: Uint8Array, + passphrase: string, + iterations: number = DEFAULT_PBKDF2_ITERATIONS, +): Promise { + const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_BYTES)); + const iv = crypto.getRandomValues(new Uint8Array(PBKDF2_IV_BYTES)); + const key = await deriveKeyFromPassphrase(passphrase, salt, iterations); + const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext)); + const result = new Uint8Array(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES + ciphertext.length); + result.set(salt, 0); + result.set(iv, PBKDF2_SALT_BYTES); + result.set(ciphertext, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES); + return result; +} + +/** + * Decrypts data produced by {@link encryptWithPassphrase}. + * + * @param data - The encrypted blob (salt + iv + ciphertext) + * @param passphrase - The passphrase used during encryption + * @param iterations - PBKDF2 iteration count (must match encryption) + * @returns The decrypted plaintext bytes + * @throws On wrong passphrase (AES-GCM auth tag mismatch) + */ +export async function decryptWithPassphrase( + data: Uint8Array, + passphrase: string, + iterations: number = DEFAULT_PBKDF2_ITERATIONS, +): Promise { + const salt = data.slice(0, PBKDF2_SALT_BYTES); + const iv = data.slice(PBKDF2_SALT_BYTES, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES); + const ciphertext = data.slice(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES); + const key = await deriveKeyFromPassphrase(passphrase, salt, iterations); + return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext)); +} + +/** + * Converts a Uint8Array to a base64 string (browser-safe, no atob dependency issues). + */ +export function uint8ToBase64(bytes: Uint8Array): string { + let binary = ''; + for (const b of bytes) { + binary += String.fromCharCode(b); + } + return btoa(binary); +} + +/** + * Converts a base64 string to a Uint8Array. + */ +export function base64ToUint8(b64: string): Uint8Array { + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} diff --git a/yarn-project/wallet-sdk/src/iframe/handlers/iframe_connection_handler.ts b/yarn-project/wallet-sdk/src/iframe/handlers/iframe_connection_handler.ts new file mode 100644 index 000000000000..e185836875a7 --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/handlers/iframe_connection_handler.ts @@ -0,0 +1,330 @@ +/** + * IframeConnectionHandler — wallet-side of the cross-origin iframe protocol. + * + * This mirrors {@link BackgroundConnectionHandler} from `@aztec/wallet-sdk/extension/handlers` + * but uses `window.postMessage` instead of browser.runtime messaging. + * + * Message flow (wallet receives): + * parent → DISCOVERY → show approval UI → send DISCOVERY_RESPONSE + * parent → KEY_EXCHANGE_REQUEST → ECDH → send KEY_EXCHANGE_RESPONSE + * parent → SECURE_MESSAGE → decrypt → Wallet → encrypt → SECURE_RESPONSE + * parent → DISCONNECT → terminate session + * + * The wallet announces itself by posting WALLET_READY as soon as the handler starts, + * so the dApp knows it can send a discovery request. + */ +import type { ChainInfo } from '@aztec/aztec.js/account'; +import { createLogger } from '@aztec/aztec.js/log'; +import type { Wallet } from '@aztec/aztec.js/wallet'; +import { WalletSchema } from '@aztec/aztec.js/wallet'; +import { jsonStringify } from '@aztec/foundation/json-rpc'; +import { schemaHasMethod } from '@aztec/foundation/schemas'; + +import { + type EncryptedPayload, + decrypt, + deriveSessionKeys, + encrypt, + exportPublicKey, + generateKeyPair, + importPublicKey, +} from '../../crypto.js'; +import { type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js'; + +/** + * A pending discovery request from a dApp (before user approval). + */ +export interface PendingSession { + /** Unique request identifier */ + requestId: string; + /** Application identifier */ + appId: string; + /** Origin URL of the requesting page */ + origin: string; + /** Approval status */ + status: 'pending' | 'approved'; +} + +/** + * An active session (after key exchange). + */ +export interface ActiveSession { + /** Session identifier (same as the discovery requestId) */ + sessionId: string; + /** AES-256-GCM shared key for this session */ + sharedKey: CryptoKey; + /** Verification hash for emoji display */ + verificationHash: string; + /** Origin URL of the connected dApp */ + origin: string; + /** Application identifier */ + appId: string; +} + +/** + * Configuration for the iframe connection handler. + */ +export interface IframeConnectionConfig { + /** Unique wallet identifier */ + walletId: string; + /** Display name for the wallet */ + walletName: string; + /** Wallet version string */ + walletVersion: string; + /** Optional wallet icon URL */ + walletIcon?: string; + /** Origins allowed to connect. If empty or undefined, all origins are allowed (dev mode). */ + allowedOrigins?: string[]; +} + +/** + * Event callbacks for the iframe connection handler. + */ +export interface IframeConnectionCallbacks { + /** Called when a new discovery request arrives — wallet can show approval UI */ + onPendingDiscovery?: (session: PendingSession) => void; + /** Called when a session is established (key exchange complete) */ + onSessionEstablished?: (session: ActiveSession) => void; + /** Called when a session is terminated */ + onSessionTerminated?: (sessionId: string) => void; + /** Called when a key exchange completes — show verificationHash as emojis to the user */ + onVerificationHash?: (verificationHash: string) => void; + /** + * Resolves the Wallet instance to use for a given dApp and chain. + * Called when an encrypted message arrives and needs to be dispatched. + */ + getWallet: (appId: string, chainInfo: ChainInfo) => Promise; +} + +/** + * Handles the wallet side of the cross-origin iframe protocol. + * + * Manages the full lifecycle: discovery, ECDH key exchange, encrypted message + * dispatch to a {@link Wallet} instance, and session termination. + * + * @example + * ```typescript + * const handler = new IframeConnectionHandler( + * { walletId: 'my-wallet', walletName: 'My Wallet', walletVersion: '1.0.0' }, + * { + * onPendingDiscovery: (session) => showApprovalUI(session), + * getWallet: (appId, chainInfo) => createWalletForApp(appId, chainInfo), + * }, + * ); + * handler.start(); + * ``` + */ +export class IframeConnectionHandler { + private pendingSessions = new Map(); + private activeSessions = new Map(); + private log = createLogger('wallet:iframe-handler'); + + constructor( + private config: IframeConnectionConfig, + private callbacks: IframeConnectionCallbacks, + ) {} + + /** Start listening for messages and announce WALLET_READY. */ + start(): void { + window.addEventListener('message', this.handleMessage); + this.postToParent({ type: WalletMessageType.WALLET_READY }); + this.log.info('IframeConnectionHandler started, posted WALLET_READY'); + } + + /** Stop listening for messages. */ + stop(): void { + window.removeEventListener('message', this.handleMessage); + } + + // ─── Approval API (called by wallet UI) ──────────────────────────────────── + + /** Approve a pending discovery request and send wallet info to the dApp. */ + approveDiscovery(requestId: string): void { + const pending = this.pendingSessions.get(requestId); + if (!pending || pending.status !== 'pending') { + return; + } + + pending.status = 'approved'; + this.postToOrigin(pending.origin, { + type: WalletMessageType.DISCOVERY_RESPONSE, + requestId, + walletInfo: { + id: this.config.walletId, + name: this.config.walletName, + version: this.config.walletVersion, + icon: this.config.walletIcon, + }, + }); + this.log.info(`Discovery approved for requestId=${requestId}`); + } + + /** Reject a pending discovery request. */ + rejectDiscovery(requestId: string): void { + this.pendingSessions.delete(requestId); + } + + /** Terminate an active session and notify the dApp. */ + terminateSession(sessionId: string): void { + const session = this.activeSessions.get(sessionId); + if (session) { + this.postToOrigin(session.origin, { + type: WalletMessageType.SESSION_DISCONNECTED, + sessionId, + }); + this.activeSessions.delete(sessionId); + this.callbacks.onSessionTerminated?.(sessionId); + } + } + + /** Returns all sessions that are awaiting user approval. */ + getPendingSessions(): PendingSession[] { + return Array.from(this.pendingSessions.values()).filter(s => s.status === 'pending'); + } + + // ─── Message handler ──────────────────────────────────────────────────────── + + private handleMessage = async (event: MessageEvent): Promise => { + if (this.config.allowedOrigins && this.config.allowedOrigins.length > 0) { + if (!this.config.allowedOrigins.includes(event.origin)) { + return; + } + } + + const msg = event.data; + if (!msg || typeof msg !== 'object' || !msg.type) { + return; + } + + switch (msg.type) { + case WalletMessageType.DISCOVERY: + this.handleDiscoveryRequest(msg, event.origin); + break; + case WalletMessageType.KEY_EXCHANGE_REQUEST: + await this.handleKeyExchangeRequest(msg, event.origin); + break; + case WalletMessageType.SECURE_MESSAGE: + await this.handleSecureMessage(msg); + break; + case WalletMessageType.DISCONNECT: + this.terminateSession(msg.sessionId); + break; + } + }; + + private handleDiscoveryRequest(msg: Record, origin: string): void { + const { requestId, appId } = msg as { requestId: string; appId: string }; + const pending: PendingSession = { requestId, appId, origin, status: 'pending' }; + this.pendingSessions.set(requestId, pending); + this.log.info(`Discovery request from appId=${appId} origin=${origin}`); + this.callbacks.onPendingDiscovery?.(pending); + } + + private async handleKeyExchangeRequest(msg: Record, origin: string): Promise { + const { requestId, publicKey: appPublicKeyRaw } = msg as { + requestId: string; + publicKey: { kty: string; crv: string; x: string; y: string }; + }; + const pending = this.pendingSessions.get(requestId); + if (!pending || pending.status !== 'approved') { + this.log.warn(`Key exchange for unknown/unapproved requestId=${requestId}`); + return; + } + + try { + const keyPair = await generateKeyPair(); + const walletPublicKey = await exportPublicKey(keyPair.publicKey); + const appPublicKey = await importPublicKey(appPublicKeyRaw); + const sessionKeys = await deriveSessionKeys(keyPair, appPublicKey, false); + + const session: ActiveSession = { + sessionId: requestId, + sharedKey: sessionKeys.encryptionKey, + verificationHash: sessionKeys.verificationHash, + origin: pending.origin, + appId: pending.appId, + }; + + this.activeSessions.set(requestId, session); + this.pendingSessions.delete(requestId); + + this.postToOrigin(origin, { + type: WalletMessageType.KEY_EXCHANGE_RESPONSE, + requestId, + publicKey: walletPublicKey, + verificationHash: sessionKeys.verificationHash, + }); + + this.callbacks.onVerificationHash?.(sessionKeys.verificationHash); + this.callbacks.onSessionEstablished?.(session); + this.log.info(`Key exchange complete, sessionId=${requestId}`); + } catch (err) { + this.log.error(`Key exchange failed: ${err}`); + } + } + + private async handleSecureMessage(msg: Record): Promise { + const { sessionId, encrypted } = msg as { sessionId: string; encrypted: EncryptedPayload }; + const session = this.activeSessions.get(sessionId); + if (!session) { + return; + } + + let walletMessage: WalletMessage; + try { + walletMessage = await decrypt(session.sharedKey, encrypted); + } catch { + this.log.warn(`Decryption failed for sessionId=${sessionId}`); + return; + } + + const { messageId, type, args, chainInfo, appId } = walletMessage; + + let result: unknown; + let error: string | undefined; + + try { + const wallet = await this.callbacks.getWallet(appId, chainInfo); + + if (!schemaHasMethod(WalletSchema, type)) { + throw new Error(`Unknown wallet method: ${type}`); + } + result = await (wallet as Record Promise>)[type](...args); + } catch (err: unknown) { + error = err instanceof Error ? err.message : String(err); + this.log.error(`Error handling ${type}: ${error}`); + } + + const response: WalletResponse = { + messageId, + walletId: this.config.walletId, + result, + error, + }; + + try { + const encryptedResponse = await encrypt(session.sharedKey, jsonStringify(response)); + this.postToOrigin(session.origin, { + type: WalletMessageType.SECURE_RESPONSE, + sessionId, + encrypted: encryptedResponse, + }); + } catch (err) { + this.log.error(`Encryption of response failed: ${err}`); + } + } + + // ─── Transport helpers ────────────────────────────────────────────────────── + + private postToParent(msg: object): void { + if (window.parent !== window) { + window.parent.postMessage(msg, '*'); + } + } + + private postToOrigin(origin: string, msg: object): void { + if (window.parent !== window) { + window.parent.postMessage(msg, origin); + } + } +} diff --git a/yarn-project/wallet-sdk/src/iframe/handlers/index.ts b/yarn-project/wallet-sdk/src/iframe/handlers/index.ts new file mode 100644 index 000000000000..8207ad9d58ca --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/handlers/index.ts @@ -0,0 +1,7 @@ +export { + IframeConnectionHandler, + type IframeConnectionConfig, + type IframeConnectionCallbacks, + type PendingSession, + type ActiveSession, +} from './iframe_connection_handler.js'; diff --git a/yarn-project/wallet-sdk/src/iframe/provider/iframe_discovery.ts b/yarn-project/wallet-sdk/src/iframe/provider/iframe_discovery.ts new file mode 100644 index 000000000000..f13defa5ad96 --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/provider/iframe_discovery.ts @@ -0,0 +1,176 @@ +/** + * Web wallet discovery — creates {@link IframeWalletProvider} instances from a list of URLs. + * + * For each configured URL we probe the wallet by loading a tiny invisible iframe, + * waiting for WALLET_READY, then sending a DISCOVERY request. On a successful + * DISCOVERY_RESPONSE we emit an IframeWalletProvider to the caller. + * + * This is intentionally lightweight (no key exchange yet) — key exchange happens + * later when the user selects the wallet and calls `provider.establishSecureChannel()`. + */ +import type { ChainInfo } from '@aztec/aztec.js/account'; +import { promiseWithResolvers } from '@aztec/foundation/promise'; + +import type { DiscoverySession, WalletProvider } from '../../manager/types.js'; +import { WalletMessageType } from '../../types.js'; +import { IframeWalletProvider } from './iframe_provider.js'; + +const PROBE_TIMEOUT_MS = 10_000; + +/** + * Probes a list of web wallet URLs and returns a {@link DiscoverySession} compatible + * with WalletManager's `getAvailableWallets()` interface. + * + * Discovered {@link IframeWalletProvider} instances are yielded asynchronously as each + * wallet responds to the probe. + * + * @param walletUrls - URLs of web wallets to probe + * @param chainInfo - Network information to pass during discovery + * @returns A cancellable discovery session + */ +export function discoverWebWallets(walletUrls: string[], chainInfo: ChainInfo): DiscoverySession { + const { promise: donePromise, resolve: resolveDone } = promiseWithResolvers(); + + let cancelled = false; + const pendingProviders: WalletProvider[] = []; + let pendingResolve: ((result: IteratorResult) => void) | null = null; + let completed = false; + + function emit(provider: WalletProvider) { + if (pendingResolve) { + const resolve = pendingResolve; + pendingResolve = null; + resolve({ value: provider, done: false }); + } else { + pendingProviders.push(provider); + } + } + + function markComplete() { + completed = true; + resolveDone(); + if (pendingResolve) { + const resolve = pendingResolve; + pendingResolve = null; + resolve({ value: undefined as unknown as WalletProvider, done: true }); + } + } + + // Probe all URLs in parallel + const probes = walletUrls.map(url => + probeWallet(url, chainInfo, PROBE_TIMEOUT_MS).then( + provider => { + if (!cancelled && provider) { + emit(provider); + } + }, + () => { + // ignore probe errors + }, + ), + ); + + void Promise.all(probes).then(() => { + if (!cancelled) { + markComplete(); + } + }); + + const wallets: AsyncIterable = { + [Symbol.asyncIterator]() { + return { + async next(): Promise> { + if (completed && pendingProviders.length === 0) { + return { value: undefined as unknown as WalletProvider, done: true }; + } + if (pendingProviders.length > 0) { + return { value: pendingProviders.shift()!, done: false }; + } + return new Promise(resolve => { + pendingResolve = resolve; + }); + }, + async return() { + markComplete(); + return { value: undefined as unknown as WalletProvider, done: true }; + }, + }; + }, + }; + + return { + wallets, + done: donePromise, + cancel: () => { + cancelled = true; + markComplete(); + }, + }; +} + +/** + * Probes a single web wallet URL. + * Creates a temporary hidden iframe, waits for WALLET_READY, sends DISCOVERY_REQUEST. + * Returns an IframeWalletProvider on success, null on timeout/failure. + * @internal + */ +async function probeWallet( + walletUrl: string, + chainInfo: ChainInfo, + timeoutMs: number, +): Promise { + const walletOrigin = new URL(walletUrl).origin; + const iframe = document.createElement('iframe'); + iframe.src = walletUrl; + iframe.style.cssText = 'display:none;width:0;height:0;border:none;position:absolute;top:-9999px;'; + iframe.allow = 'storage-access; cross-origin-isolated'; + document.body.appendChild(iframe); + + return new Promise(resolve => { + let timer: ReturnType; + + const cleanup = () => { + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + window.removeEventListener('message', handler); + clearTimeout(timer); + }; + + timer = setTimeout(() => { + cleanup(); + resolve(null); + }, timeoutMs); + + let step: 'waiting-ready' | 'waiting-discovery' = 'waiting-ready'; + const requestId = globalThis.crypto.randomUUID(); + + function handler(event: MessageEvent) { + if (event.origin !== walletOrigin) { + return; + } + const msg = event.data; + if (!msg || typeof msg !== 'object') { + return; + } + + if (step === 'waiting-ready' && msg.type === WalletMessageType.WALLET_READY) { + step = 'waiting-discovery'; + iframe.contentWindow?.postMessage( + { type: WalletMessageType.DISCOVERY, requestId, appId: 'discovery-probe' }, + walletOrigin, + ); + } else if ( + step === 'waiting-discovery' && + msg.type === WalletMessageType.DISCOVERY_RESPONSE && + msg.requestId === requestId + ) { + const info = msg.walletInfo as { id: string; name: string; version: string; icon?: string }; + cleanup(); + resolve(new IframeWalletProvider(info.id, info.name, info.icon, walletUrl, chainInfo)); + } + } + + window.addEventListener('message', handler); + }); +} diff --git a/yarn-project/wallet-sdk/src/iframe/provider/iframe_provider.ts b/yarn-project/wallet-sdk/src/iframe/provider/iframe_provider.ts new file mode 100644 index 000000000000..ef2e91fd1917 --- /dev/null +++ b/yarn-project/wallet-sdk/src/iframe/provider/iframe_provider.ts @@ -0,0 +1,334 @@ +/** + * IframeWalletProvider — implements {@link WalletProvider} for web wallets loaded in iframes. + * + * Flow (mirrors ExtensionProvider): + * 1. Creates an `