diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index a33bfa8ca7b9..5a01f5290f9b 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -107,7 +107,7 @@ describe('Archiver', () => { let blobSinkClient: MockProxy; let epochCache: MockProxy; let archiverStore: ArchiverDataStore; - let l1Constants: L1RollupConstants & { l1StartBlockHash: Buffer32 }; + let l1Constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr }; let now: number; let mockRollupRead: MockProxy; @@ -168,6 +168,7 @@ describe('Archiver', () => { slotDuration: 24, ethereumSlotDuration: 12, proofSubmissionEpochs: 1, + genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT), }; archiver = new Archiver( diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 3e293ef93deb..75bf6ce9f2ff 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -150,7 +150,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem private readonly blobSinkClient: BlobSinkClientInterface, private readonly epochCache: EpochCache, private readonly instrumentation: ArchiverInstrumentation, - private readonly l1constants: L1RollupConstants & { l1StartBlockHash: Buffer32 }, + private readonly l1constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr }, private readonly log: Logger = createLogger('archiver'), ) { super(); @@ -184,10 +184,11 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem const rollup = new RollupContract(publicClient, config.l1Contracts.rollupAddress); - const [l1StartBlock, l1GenesisTime, proofSubmissionEpochs] = await Promise.all([ + const [l1StartBlock, l1GenesisTime, proofSubmissionEpochs, genesisArchiveRoot] = await Promise.all([ rollup.getL1StartBlock(), rollup.getL1GenesisTime(), rollup.getProofSubmissionEpochs(), + rollup.getGenesisArchiveTreeRoot(), ] as const); const l1StartBlockHash = await publicClient @@ -204,6 +205,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem slotDuration, ethereumSlotDuration, proofSubmissionEpochs: Number(proofSubmissionEpochs), + genesisArchiveRoot: Fr.fromHexString(genesisArchiveRoot), }; const opts = merge({ pollingIntervalMs: 10_000, batchSize: 100 }, mapArchiverConfig(config)); @@ -977,6 +979,10 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem return Promise.resolve(this.l1constants); } + public getGenesisValues(): Promise<{ genesisArchiveRoot: Fr }> { + return Promise.resolve({ genesisArchiveRoot: this.l1constants.genesisArchiveRoot }); + } + public getRollupAddress(): Promise { return Promise.resolve(this.l1Addresses.rollupAddress); } @@ -1097,6 +1103,22 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem return limitWithProven === 0 ? [] : await this.store.getPublishedBlocks(from, limitWithProven); } + public getPublishedBlockByHash(blockHash: Fr): Promise { + return this.store.getPublishedBlockByHash(blockHash); + } + + public getPublishedBlockByArchive(archive: Fr): Promise { + return this.store.getPublishedBlockByArchive(archive); + } + + public getBlockHeaderByHash(blockHash: Fr): Promise { + return this.store.getBlockHeaderByHash(blockHash); + } + + public getBlockHeaderByArchive(archive: Fr): Promise { + return this.store.getBlockHeaderByArchive(archive); + } + /** * Gets an l2 block. * @param number - The block number to return. @@ -1592,9 +1614,21 @@ export class ArchiverStoreHelper getPublishedBlock(number: number): Promise { return this.store.getPublishedBlock(number); } + getPublishedBlockByHash(blockHash: Fr): Promise { + return this.store.getPublishedBlockByHash(blockHash); + } + getPublishedBlockByArchive(archive: Fr): Promise { + return this.store.getPublishedBlockByArchive(archive); + } getBlockHeaders(from: number, limit: number): Promise { return this.store.getBlockHeaders(from, limit); } + getBlockHeaderByHash(blockHash: Fr): Promise { + return this.store.getBlockHeaderByHash(blockHash); + } + getBlockHeaderByArchive(archive: Fr): Promise { + return this.store.getBlockHeaderByArchive(archive); + } getTxEffect(txHash: TxHash): Promise { return this.store.getTxEffect(txHash); } diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index 5bedb316eabb..9a0c058be995 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -61,6 +61,18 @@ export interface ArchiverDataStore { */ getPublishedBlock(number: number): Promise; + /** + * Returns the block for the given hash, or undefined if not exists. + * @param blockHash - The block hash to return. + */ + getPublishedBlockByHash(blockHash: Fr): Promise; + + /** + * Returns the block for the given archive root, or undefined if not exists. + * @param archive - The archive root to return. + */ + getPublishedBlockByArchive(archive: Fr): Promise; + /** * Gets up to `limit` amount of published L2 blocks starting from `from`. * @param from - Number of the first block to return (inclusive). @@ -77,6 +89,18 @@ export interface ArchiverDataStore { */ getBlockHeaders(from: number, limit: number): Promise; + /** + * Returns the block header for the given hash, or undefined if not exists. + * @param blockHash - The block hash to return. + */ + getBlockHeaderByHash(blockHash: Fr): Promise; + + /** + * Returns the block header for the given archive root, or undefined if not exists. + * @param archive - The archive root to return. + */ + getBlockHeaderByArchive(archive: Fr): Promise; + /** * Gets a tx effect. * @param txHash - The hash of the tx corresponding to the tx effect. diff --git a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts index 29369d6a1bce..a542949ad2bb 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -143,6 +143,28 @@ export function describeArchiverDataStore( await store.addBlocks(blocks); await expect(store.unwindBlocks(5, 1)).rejects.toThrow(/can only unwind blocks from the tip/i); }); + + it('unwound blocks and headers cannot be retrieved by hash or archive', async () => { + await store.addBlocks(blocks); + const lastBlock = blocks[blocks.length - 1]; + const blockHash = await lastBlock.block.hash(); + const archive = lastBlock.block.archive.root; + + // Verify block and header exist before unwinding + expect(await store.getPublishedBlockByHash(blockHash)).toBeDefined(); + expect(await store.getPublishedBlockByArchive(archive)).toBeDefined(); + expect(await store.getBlockHeaderByHash(blockHash)).toBeDefined(); + expect(await store.getBlockHeaderByArchive(archive)).toBeDefined(); + + // Unwind the block + await store.unwindBlocks(lastBlock.block.number, 1); + + // Verify neither block nor header can be retrieved after unwinding + expect(await store.getPublishedBlockByHash(blockHash)).toBeUndefined(); + expect(await store.getPublishedBlockByArchive(archive)).toBeUndefined(); + expect(await store.getBlockHeaderByHash(blockHash)).toBeUndefined(); + expect(await store.getBlockHeaderByArchive(archive)).toBeUndefined(); + }); }); describe('getBlocks', () => { @@ -180,6 +202,86 @@ export function describeArchiverDataStore( }); }); + describe('getPublishedBlockByHash', () => { + beforeEach(async () => { + await store.addBlocks(blocks); + }); + + it('retrieves a block by its hash', async () => { + const expectedBlock = blocks[5]; + const blockHash = await expectedBlock.block.hash(); + const retrievedBlock = await store.getPublishedBlockByHash(blockHash); + + expect(retrievedBlock).toBeDefined(); + expectBlocksEqual([retrievedBlock!], [expectedBlock]); + }); + + it('returns undefined for non-existent block hash', async () => { + const nonExistentHash = Fr.random(); + await expect(store.getPublishedBlockByHash(nonExistentHash)).resolves.toBeUndefined(); + }); + }); + + describe('getPublishedBlockByArchive', () => { + beforeEach(async () => { + await store.addBlocks(blocks); + }); + + it('retrieves a block by its archive root', async () => { + const expectedBlock = blocks[3]; + const archive = expectedBlock.block.archive.root; + const retrievedBlock = await store.getPublishedBlockByArchive(archive); + + expect(retrievedBlock).toBeDefined(); + expectBlocksEqual([retrievedBlock!], [expectedBlock]); + }); + + it('returns undefined for non-existent archive root', async () => { + const nonExistentArchive = Fr.random(); + await expect(store.getPublishedBlockByArchive(nonExistentArchive)).resolves.toBeUndefined(); + }); + }); + + describe('getBlockHeaderByHash', () => { + beforeEach(async () => { + await store.addBlocks(blocks); + }); + + it('retrieves a block header by its hash', async () => { + const expectedBlock = blocks[7]; + const blockHash = await expectedBlock.block.hash(); + const retrievedHeader = await store.getBlockHeaderByHash(blockHash); + + expect(retrievedHeader).toBeDefined(); + expect(retrievedHeader!.equals(expectedBlock.block.header)).toBe(true); + }); + + it('returns undefined for non-existent block hash', async () => { + const nonExistentHash = Fr.random(); + await expect(store.getBlockHeaderByHash(nonExistentHash)).resolves.toBeUndefined(); + }); + }); + + describe('getBlockHeaderByArchive', () => { + beforeEach(async () => { + await store.addBlocks(blocks); + }); + + it('retrieves a block header by its archive root', async () => { + const expectedBlock = blocks[2]; + const archive = expectedBlock.block.archive.root; + const retrievedHeader = await store.getBlockHeaderByArchive(archive); + + expect(retrievedHeader).toBeDefined(); + expect(retrievedHeader!.equals(expectedBlock.block.header)).toBe(true); + }); + + it('returns undefined for non-existent archive root', async () => { + const nonExistentArchive = Fr.random(); + await expect(store.getBlockHeaderByArchive(nonExistentArchive)).resolves.toBeUndefined(); + }); + }); + describe('getSyncedL2BlockNumber', () => { it('returns the block number before INITIAL_L2_BLOCK_NUM if no blocks have been added', async () => { await expect(store.getSynchedL2BlockNumber()).resolves.toEqual(INITIAL_L2_BLOCK_NUM - 1); diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts index 0cf29fd02e07..b5b7d270685d 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts @@ -66,6 +66,12 @@ export class BlockStore { /** Index mapping a contract's address (as a string) to its location in a block */ #contractIndex: AztecAsyncMap; + /** Index mapping block hash to block number */ + #blockHashIndex: AztecAsyncMap; + + /** Index mapping block archive to block number */ + #blockArchiveIndex: AztecAsyncMap; + #log = createLogger('archiver:block_store'); constructor(private db: AztecAsyncKVStore) { @@ -73,6 +79,8 @@ export class BlockStore { this.#blockTxs = db.openMap('archiver_block_txs'); this.#txEffects = db.openMap('archiver_tx_effects'); this.#contractIndex = db.openMap('archiver_contract_index'); + this.#blockHashIndex = db.openMap('archiver_block_hash_index'); + this.#blockArchiveIndex = db.openMap('archiver_block_archive_index'); this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block'); this.#lastProvenL2Block = db.openSingleton('archiver_last_proven_l2_block'); this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status'); @@ -132,6 +140,10 @@ export class BlockStore { blockHash.toString(), Buffer.concat(block.block.body.txEffects.map(tx => tx.txHash.toBuffer())), ); + + // Update indices for block hash and archive + await this.#blockHashIndex.set(blockHash.toString(), block.block.number); + await this.#blockArchiveIndex.set(block.block.archive.root.toString(), block.block.number); } await this.#lastSynchedL1Block.set(blocks[blocks.length - 1].l1.blockNumber); @@ -170,6 +182,11 @@ export class BlockStore { await Promise.all(block.block.body.txEffects.map(tx => this.#txEffects.delete(tx.txHash.toString()))); const blockHash = (await block.block.hash()).toString(); await this.#blockTxs.delete(blockHash); + + // Clean up indices + await this.#blockHashIndex.delete(blockHash); + await this.#blockArchiveIndex.delete(block.block.archive.root.toString()); + this.#log.debug(`Unwound block ${blockNumber} ${blockHash}`); } @@ -205,6 +222,66 @@ export class BlockStore { return this.getBlockFromBlockStorage(blockNumber, blockStorage); } + /** + * Gets an L2 block by its hash. + * @param blockHash - The hash of the block to return. + * @returns The requested L2 block. + */ + async getBlockByHash(blockHash: L2BlockHash): Promise { + const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString()); + if (blockNumber === undefined) { + return undefined; + } + return this.getBlock(blockNumber); + } + + /** + * Gets an L2 block by its archive root. + * @param archive - The archive root of the block to return. + * @returns The requested L2 block. + */ + async getBlockByArchive(archive: Fr): Promise { + const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString()); + if (blockNumber === undefined) { + return undefined; + } + return this.getBlock(blockNumber); + } + + /** + * Gets a block header by its hash. + * @param blockHash - The hash of the block to return. + * @returns The requested block header. + */ + async getBlockHeaderByHash(blockHash: L2BlockHash): Promise { + const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString()); + if (blockNumber === undefined) { + return undefined; + } + const blockStorage = await this.#blocks.getAsync(blockNumber); + if (!blockStorage || !blockStorage.header) { + return undefined; + } + return BlockHeader.fromBuffer(blockStorage.header); + } + + /** + * Gets a block header by its archive root. + * @param archive - The archive root of the block to return. + * @returns The requested block header. + */ + async getBlockHeaderByArchive(archive: Fr): Promise { + const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString()); + if (blockNumber === undefined) { + return undefined; + } + const blockStorage = await this.#blocks.getAsync(blockNumber); + if (!blockStorage || !blockStorage.header) { + return undefined; + } + return BlockHeader.fromBuffer(blockStorage.header); + } + /** * Gets the headers for a sequence of L2 blocks. * @param start - Number of the first block to return (inclusive). diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts index c1e01a28976d..39200bb0cce9 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts @@ -5,7 +5,7 @@ import { createLogger } from '@aztec/foundation/log'; import type { AztecAsyncKVStore, CustomRange, StoreSize } from '@aztec/kv-store'; import { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { L2Block, ValidateBlockResult } from '@aztec/stdlib/block'; +import { type L2Block, L2BlockHash, type ValidateBlockResult } from '@aztec/stdlib/block'; import type { ContractClassPublic, ContractDataSource, @@ -204,6 +204,14 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc return this.#blockStore.getBlock(number); } + getPublishedBlockByHash(blockHash: Fr): Promise { + return this.#blockStore.getBlockByHash(L2BlockHash.fromField(blockHash)); + } + + getPublishedBlockByArchive(archive: Fr): Promise { + return this.#blockStore.getBlockByArchive(archive); + } + /** * Gets up to `limit` amount of L2 blocks starting from `from`. * @@ -226,6 +234,14 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc return toArray(this.#blockStore.getBlockHeaders(start, limit)); } + getBlockHeaderByHash(blockHash: Fr): Promise { + return this.#blockStore.getBlockHeaderByHash(L2BlockHash.fromField(blockHash)); + } + + getBlockHeaderByArchive(archive: Fr): Promise { + return this.#blockStore.getBlockHeaderByArchive(archive); + } + /** * Gets a tx effect. * @param txHash - The hash of the tx corresponding to the tx effect. diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index 14b089ff5f07..8ce9776754c3 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -1,7 +1,8 @@ +import { GENESIS_ARCHIVE_ROOT } from '@aztec/constants'; import { DefaultL1ContractsConfig } from '@aztec/ethereum'; import { Buffer32 } from '@aztec/foundation/buffer'; import { EthAddress } from '@aztec/foundation/eth-address'; -import type { Fr } from '@aztec/foundation/fields'; +import { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import type { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -126,6 +127,57 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { ); } + public async getPublishedBlockByHash(blockHash: Fr): Promise { + for (const block of this.l2Blocks) { + const hash = await block.hash(); + if (hash.equals(blockHash)) { + return PublishedL2Block.fromFields({ + block, + l1: { + blockNumber: BigInt(block.number), + blockHash: Buffer32.random().toString(), + timestamp: BigInt(block.number), + }, + attestations: [], + }); + } + } + return undefined; + } + + public getPublishedBlockByArchive(archive: Fr): Promise { + const block = this.l2Blocks.find(b => b.archive.root.equals(archive)); + if (!block) { + return Promise.resolve(undefined); + } + return Promise.resolve( + PublishedL2Block.fromFields({ + block, + l1: { + blockNumber: BigInt(block.number), + blockHash: Buffer32.random().toString(), + timestamp: BigInt(block.number), + }, + attestations: [], + }), + ); + } + + public async getBlockHeaderByHash(blockHash: Fr): Promise { + for (const block of this.l2Blocks) { + const hash = await block.hash(); + if (hash.equals(blockHash)) { + return block.header; + } + } + return undefined; + } + + public getBlockHeaderByArchive(archive: Fr): Promise { + const block = this.l2Blocks.find(b => b.archive.root.equals(archive)); + return Promise.resolve(block?.header); + } + getBlockHeader(number: number | 'latest'): Promise { return Promise.resolve(this.l2Blocks.at(typeof number === 'number' ? number - 1 : -1)?.header); } @@ -231,6 +283,10 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(EmptyL1RollupConstants); } + getGenesisValues(): Promise<{ genesisArchiveRoot: Fr }> { + return Promise.resolve({ genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT) }); + } + getL1Timestamp(): Promise { throw new Error('Method not implemented.'); } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index d5f1d848c4c4..808e31d4758d 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -548,6 +548,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return await this.blockSource.getBlock(blockNumber); } + /** + * Get a block specified by its hash. + * @param blockHash - The block hash being requested. + * @returns The requested block. + */ + public async getBlockByHash(blockHash: Fr): Promise { + const publishedBlock = await this.blockSource.getPublishedBlockByHash(blockHash); + return publishedBlock?.block; + } + + /** + * Get a block specified by its archive root. + * @param archive - The archive root being requested. + * @returns The requested block. + */ + public async getBlockByArchive(archive: Fr): Promise { + const publishedBlock = await this.blockSource.getPublishedBlockByArchive(archive); + return publishedBlock?.block; + } + /** * Method to request blocks. Will attempt to return all requested blocks but will return only those available. * @param from - The start of the range of blocks to return. @@ -1056,6 +1076,24 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { : this.blockSource.getBlockHeader(blockNumber); } + /** + * Get a block header specified by its hash. + * @param blockHash - The block hash being requested. + * @returns The requested block header. + */ + public async getBlockHeaderByHash(blockHash: Fr): Promise { + return await this.blockSource.getBlockHeaderByHash(blockHash); + } + + /** + * Get a block header specified by its archive root. + * @param archive - The archive root being requested. + * @returns The requested block header. + */ + public async getBlockHeaderByArchive(archive: Fr): Promise { + return await this.blockSource.getBlockHeaderByArchive(archive); + } + /** * Simulates the public part of a transaction with the current state. * @param tx - The transaction to simulate. 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 fc81c01c8ac8..cd939f940eca 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 @@ -135,7 +135,7 @@ describe('e2e_multi_validator_node', () => { const payload = ConsensusPayload.fromBlock(block.block); const attestations = block.attestations .filter(a => !a.signature.isEmpty()) - .map(a => new BlockAttestation(block.block.number, payload, a.signature, Signature.empty())); + .map(a => new BlockAttestation(payload, a.signature, Signature.empty())); expect(attestations.length).toBeGreaterThanOrEqual((COMMITTEE_SIZE * 2) / 3 + 1); @@ -192,7 +192,7 @@ describe('e2e_multi_validator_node', () => { const payload = ConsensusPayload.fromBlock(block.block); const attestations = block.attestations .filter(a => !a.signature.isEmpty()) - .map(a => new BlockAttestation(block.block.number, payload, a.signature, Signature.empty())); + .map(a => new BlockAttestation(payload, a.signature, Signature.empty())); expect(attestations.length).toBeGreaterThanOrEqual((COMMITTEE_SIZE * 2) / 3 + 1); diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts index 76ce7ce5caa7..aadbaff493cb 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts @@ -174,7 +174,7 @@ describe('e2e_p2p_network', () => { const payload = ConsensusPayload.fromBlock(block.block); const attestations = block.attestations .filter(a => !a.signature.isEmpty()) - .map(a => new BlockAttestation(blockNumber, payload, a.signature, Signature.empty())); + .map(a => new BlockAttestation(payload, a.signature, Signature.empty())); const signers = await Promise.all(attestations.map(att => att.getSender()!.toString())); t.logger.info(`Attestation signers`, { signers }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts index 969d31fed5e0..f9e2e136ba5f 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network_no_cheat.test.ts @@ -226,7 +226,7 @@ describe('e2e_p2p_network', () => { const payload = ConsensusPayload.fromBlock(block.block); const attestations = block.attestations .filter(a => !a.signature.isEmpty()) - .map(a => new BlockAttestation(blockNumber, payload, a.signature, Signature.empty())); + .map(a => new BlockAttestation(payload, a.signature, Signature.empty())); const signers = await Promise.all(attestations.map(att => att.getSender()!.toString())); t.logger.info(`Attestation signers`, { signers }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts index 27ae28b33310..1e7ffd2bbece 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/preferred_gossip_network.test.ts @@ -358,7 +358,7 @@ describe('e2e_p2p_preferred_network', () => { const payload = ConsensusPayload.fromBlock(block.block); const attestations = block.attestations .filter(a => !a.signature.isEmpty()) - .map(a => new BlockAttestation(blockNumber, payload, a.signature, Signature.empty())); + .map(a => new BlockAttestation(payload, a.signature, Signature.empty())); const signers = await Promise.all(attestations.map(att => att.getSender()!.toString())); t.logger.info(`Attestation signers`, { signers }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts index 7e6f5e712186..17c3dfa29796 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts @@ -1,11 +1,13 @@ import type { AztecNodeService } from '@aztec/aztec-node'; import { Fr, type SentTx, Tx, sleep } from '@aztec/aztec.js'; import { times } from '@aztec/foundation/collection'; +import { unfreeze } from '@aztec/foundation/types'; +import type { LibP2PService, P2PClient } from '@aztec/p2p'; import type { BlockBuilder } from '@aztec/sequencer-client'; import type { PublicTxResult, PublicTxSimulator } from '@aztec/simulator/server'; import { BlockProposal, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p'; import { ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError } from '@aztec/stdlib/validators'; -import type { ValidatorClient } from '@aztec/validator-client'; +import type { ValidatorClient, ValidatorKeyStore } from '@aztec/validator-client'; import { describe, it, jest } from '@jest/globals'; import fs from 'fs'; @@ -121,25 +123,30 @@ describe('e2e_p2p_reex', () => { // Make sure the nodes submit faulty proposals, in this case a faulty proposal is one where we remove one of the transactions // Such that the calculated archive will be different! const interceptBroadcastProposal = (node: AztecNodeService) => { - jest.spyOn((node as any).p2pClient, 'broadcastProposal').mockImplementation(async (...args: unknown[]) => { + const p2pClient = (node as any).p2pClient as P2PClient; + jest.spyOn(p2pClient, 'broadcastProposal').mockImplementation(async (...args: unknown[]) => { // We remove one of the transactions, therefore the block root will be different! const proposal = args[0] as BlockProposal; + const proposerAddress = proposal.getSender(); const txHashes = proposal.txHashes; - // We need to mutate the proposal, so we cast to any - (proposal as any).txHashes = txHashes.slice(0, txHashes.length - 1); + // Mutate txhashes to remove the last one + unfreeze(proposal).txHashes = txHashes.slice(0, txHashes.length - 1); // We sign over the proposal using the node's signing key - // Abusing javascript to access the nodes signing key - const signer = (node as any).sequencer.sequencer.validatorClient.validationService.keyStore; + const signer = (node as any).sequencer.sequencer.validatorClient.validationService + .keyStore as ValidatorKeyStore; const newProposal = new BlockProposal( - proposal.blockNumber, proposal.payload, - await signer.signMessage(getHashedSignaturePayload(proposal.payload, SignatureDomainSeparator.blockProposal)), + await signer.signMessageWithAddress( + proposerAddress!, + getHashedSignaturePayload(proposal.payload, SignatureDomainSeparator.blockProposal), + ), proposal.txHashes, ); - return (node as any).p2pClient.p2pService.propagate(newProposal); + const p2pService = (p2pClient as any).p2pService as LibP2PService; + return p2pService.propagate(newProposal); }); }; diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index b7a24787ce22..e85af917bbe0 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -123,7 +123,13 @@ export class P2PClient const constants = this.txCollection.getConstants(); const nextSlotTimestampSeconds = Number(getTimestampForSlot(block.slotNumber.toBigInt() + 1n, constants)); const deadline = new Date(nextSlotTimestampSeconds * 1000); - await this.txProvider.getTxsForBlockProposal(block, { pinnedPeer: sender, deadline }); + const parentBlock = await this.l2BlockSource.getBlockHeaderByArchive(block.payload.header.lastArchiveRoot); + if (!parentBlock) { + this.log.debug(`Cannot collect txs for proposal as parent block not found`); + return; + } + const blockNumber = parentBlock.getBlockNumber() + 1; + await this.txProvider.getTxsForBlockProposal(block, blockNumber, { pinnedPeer: sender, deadline }); return undefined; }); @@ -365,7 +371,6 @@ export class P2PClient } @trackSpan('p2pClient.broadcastProposal', async proposal => ({ - [Attributes.BLOCK_NUMBER]: proposal.blockNumber, [Attributes.SLOT_NUMBER]: proposal.slotNumber.toNumber(), [Attributes.BLOCK_ARCHIVE]: proposal.archive.toString(), [Attributes.P2P_ID]: (await proposal.p2pMessageIdentifier()).toString(), diff --git a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts index d9eec7678b04..171bcc162749 100644 --- a/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts +++ b/yarn-project/p2p/src/mem_pools/attestation_pool/attestation_pool_test_suite.ts @@ -41,7 +41,6 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo }; const mockBlockProposal = (signer: Secp256k1Signer, slotNumber: number, archive: Fr = Fr.random()): BlockProposal => { - const blockNumber = 1; const header = makeHeader(1, 2, slotNumber); const payload = new ConsensusPayload(header.toPropose(), archive, header.state); @@ -50,7 +49,7 @@ export function describeAttestationPool(getAttestationPool: () => AttestationPoo const txHashes = [TxHash.random(), TxHash.random()]; // Mock tx hashes - return new BlockProposalClass(blockNumber, payload, signature, txHashes); + return new BlockProposalClass(payload, signature, txHashes); }; // We compare buffers as the objects can have cached values attached to them which are not serialised diff --git a/yarn-project/p2p/src/mem_pools/attestation_pool/mocks.ts b/yarn-project/p2p/src/mem_pools/attestation_pool/mocks.ts index e343a370bcac..855cc6ec4f8a 100644 --- a/yarn-project/p2p/src/mem_pools/attestation_pool/mocks.ts +++ b/yarn-project/p2p/src/mem_pools/attestation_pool/mocks.ts @@ -41,5 +41,5 @@ export const mockAttestation = ( const proposalHash = getHashedSignaturePayloadEthSignedMessage(payload, SignatureDomainSeparator.blockProposal); const proposerSignature = signer.sign(proposalHash); - return new BlockAttestation(header.globalVariables.blockNumber, payload, attestationSignature, proposerSignature); + return new BlockAttestation(payload, attestationSignature, proposerSignature); }; diff --git a/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts b/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts index 4fd556ebabc7..f9582c1e745b 100644 --- a/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts +++ b/yarn-project/p2p/src/msg_validators/attestation_validator/attestation_validator.ts @@ -46,6 +46,10 @@ export class AttestationValidator implements P2PValidator { this.logger.warn(`No proposer defined for slot ${slotNumberBigInt}`); return PeerErrorSeverity.HighToleranceError; } + if (!proposer) { + this.logger.warn(`Invalid proposer signature in attestation for slot ${slotNumberBigInt}`); + return PeerErrorSeverity.LowToleranceError; + } if (!proposer.equals(expectedProposer)) { this.logger.warn( `Proposer signature mismatch in attestation. ` + diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index 4bf88cb81eb7..c2d4544938c9 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -745,12 +745,11 @@ export class LibP2PService extends return; } this.logger.debug( - `Received attestation for block ${attestation.blockNumber} slot ${attestation.slotNumber.toNumber()} from external peer ${source.toString()}`, + `Received attestation for slot ${attestation.slotNumber.toNumber()} from external peer ${source.toString()}`, { p2pMessageIdentifier: await attestation.p2pMessageIdentifier(), slot: attestation.slotNumber.toNumber(), archive: attestation.archive.toString(), - block: attestation.blockNumber, source: source.toString(), }, ); @@ -783,7 +782,6 @@ export class LibP2PService extends // REVIEW: callback pattern https://github.com/AztecProtocol/aztec-packages/issues/7963 @trackSpan('Libp2pService.processValidBlockProposal', async block => ({ - [Attributes.BLOCK_NUMBER]: block.blockNumber, [Attributes.SLOT_NUMBER]: block.slotNumber.toNumber(), [Attributes.BLOCK_ARCHIVE]: block.archive.toString(), [Attributes.P2P_ID]: await block.p2pMessageIdentifier().then(i => i.toString()), @@ -791,16 +789,12 @@ export class LibP2PService extends private async processValidBlockProposal(block: BlockProposal, sender: PeerId) { const slot = block.slotNumber.toBigInt(); const previousSlot = slot - 1n; - this.logger.verbose( - `Received block ${block.blockNumber} for slot ${slot} from external peer ${sender.toString()}.`, - { - p2pMessageIdentifier: await block.p2pMessageIdentifier(), - slot: block.slotNumber.toNumber(), - archive: block.archive.toString(), - block: block.blockNumber, - source: sender.toString(), - }, - ); + this.logger.verbose(`Received block proposal for slot ${slot} from external peer ${sender.toString()}.`, { + p2pMessageIdentifier: await block.p2pMessageIdentifier(), + slot: block.slotNumber.toNumber(), + archive: block.archive.toString(), + source: sender.toString(), + }); const attestationsForPreviousSlot = await this.mempools.attestationPool?.getAttestationsForSlot(previousSlot); if (attestationsForPreviousSlot !== undefined) { this.logger.verbose(`Received ${attestationsForPreviousSlot.length} attestations for slot ${previousSlot}`); @@ -815,15 +809,11 @@ export class LibP2PService extends // The attestation can be undefined if no handler is registered / the validator deems the block invalid if (attestations?.length) { for (const attestation of attestations) { - this.logger.verbose( - `Broadcasting attestation for block ${attestation.blockNumber} slot ${attestation.slotNumber.toNumber()}`, - { - p2pMessageIdentifier: await attestation.p2pMessageIdentifier(), - slot: attestation.slotNumber.toNumber(), - archive: attestation.archive.toString(), - block: attestation.blockNumber, - }, - ); + this.logger.verbose(`Broadcasting attestation for slot ${attestation.slotNumber.toNumber()}`, { + p2pMessageIdentifier: await attestation.p2pMessageIdentifier(), + slot: attestation.slotNumber.toNumber(), + archive: attestation.archive.toString(), + }); await this.broadcastAttestation(attestation); } } @@ -834,7 +824,6 @@ export class LibP2PService extends * @param attestation - The attestation to broadcast. */ @trackSpan('Libp2pService.broadcastAttestation', async attestation => ({ - [Attributes.BLOCK_NUMBER]: attestation.blockNumber, [Attributes.SLOT_NUMBER]: attestation.payload.header.slotNumber.toNumber(), [Attributes.BLOCK_ARCHIVE]: attestation.archive.toString(), [Attributes.P2P_ID]: await attestation.p2pMessageIdentifier().then(i => i.toString()), @@ -1080,7 +1069,6 @@ export class LibP2PService extends * @returns True if the attestation is valid, false otherwise. */ @trackSpan('Libp2pService.validateAttestation', async (_, attestation) => ({ - [Attributes.BLOCK_NUMBER]: attestation.blockNumber, [Attributes.SLOT_NUMBER]: attestation.payload.header.slotNumber.toNumber(), [Attributes.BLOCK_ARCHIVE]: attestation.archive.toString(), [Attributes.P2P_ID]: await attestation.p2pMessageIdentifier().then(i => i.toString()), diff --git a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts index 75ecc34f0cba..70d175b7fd3d 100644 --- a/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/fast_tx_collection.ts @@ -55,7 +55,9 @@ export class FastTxCollection { } const blockInfo: L2BlockInfo = - input.type === 'proposal' ? input.blockProposal.toBlockInfo() : input.block.toBlockInfo(); + input.type === 'proposal' + ? { ...input.blockProposal.toBlockInfo(), blockNumber: input.blockNumber } + : { ...input.block.toBlockInfo() }; // This promise is used to await for the collection to finish during the main collectFast method. // It gets resolved in `foundTxs` when all txs have been collected, or rejected if the request is aborted or hits the deadline. diff --git a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts index 72257a22e5a4..800f42601554 100644 --- a/yarn-project/p2p/src/services/tx_collection/tx_collection.ts +++ b/yarn-project/p2p/src/services/tx_collection/tx_collection.ts @@ -25,7 +25,7 @@ export type MissingTxInfo = { blockNumber: number; deadline: Date; readyForReqRe export type FastCollectionRequestInput = | { type: 'block'; block: L2Block } - | { type: 'proposal'; blockProposal: BlockProposal }; + | { type: 'proposal'; blockProposal: BlockProposal; blockNumber: number }; export type FastCollectionRequest = FastCollectionRequestInput & { missingTxHashes: Set; @@ -152,10 +152,11 @@ export class TxCollection { /** Collects the set of txs for the given block proposal as fast as possible */ public collectFastForProposal( blockProposal: BlockProposal, + blockNumber: number, txHashes: TxHash[] | string[], opts: { deadline: Date; pinnedPeer?: PeerId }, ) { - return this.collectFastFor({ type: 'proposal', blockProposal }, txHashes, opts); + return this.collectFastFor({ type: 'proposal', blockProposal, blockNumber }, txHashes, opts); } /** Collects the set of txs for the given mined block as fast as possible */ diff --git a/yarn-project/p2p/src/services/tx_provider.test.ts b/yarn-project/p2p/src/services/tx_provider.test.ts index 041b3fe2705f..84cd874a2f74 100644 --- a/yarn-project/p2p/src/services/tx_provider.test.ts +++ b/yarn-project/p2p/src/services/tx_provider.test.ts @@ -36,7 +36,7 @@ describe('TxProvider', () => { const buildProposal = (txs: Tx[], txHashes: TxHash[]) => { const payload = new ConsensusPayload(ProposedBlockHeader.empty(), Fr.random(), StateReference.empty()); - return new BlockProposal(1, payload, Signature.empty(), txHashes, txs); + return new BlockProposal(payload, Signature.empty(), txHashes, txs); }; const setupTxPools = (txsInPool: number, txsOnP2P: number, txs: Tx[]) => { @@ -74,6 +74,8 @@ describe('TxProvider', () => { .map(({ value }) => value); }; + const blockNumber = 1; + beforeEach(() => { txPools.clear(); additionalP2PTxs.length = 0; @@ -116,7 +118,7 @@ describe('TxProvider', () => { const txs = shuffleTxs(original); const hashes = await Promise.all(txs.map(tx => tx.getTxHash())); const proposal = buildProposal([], hashes); - const results = await txProvider.getTxsForBlockProposal(proposal, opts); + const results = await txProvider.getTxsForBlockProposal(proposal, blockNumber, opts); const expected: TxResults = { txs, missingTxs: [] }; await checkResults(results, expected); expect(txPools.size).toEqual(10); @@ -131,7 +133,7 @@ describe('TxProvider', () => { const hashes = await Promise.all(txs.map(tx => tx.getTxHash())); const proposal = buildProposal([], hashes); - const results = await txProvider.getTxsForBlockProposal(proposal, opts); + const results = await txProvider.getTxsForBlockProposal(proposal, blockNumber, opts); const expected: TxResults = { txs: txs.slice(0, 5), missingTxs: originalHashes.slice(5) }; await checkResults(results, expected); expect(txPools.size).toEqual(5); @@ -145,7 +147,7 @@ describe('TxProvider', () => { const txs = original; const hashes = await Promise.all(txs.map(tx => tx.getTxHash())); const proposal = buildProposal([], hashes); - const results = await txProvider.getTxsForBlockProposal(proposal, opts); + const results = await txProvider.getTxsForBlockProposal(proposal, blockNumber, opts); const expected: TxResults = { txs: txs.slice(0, 6), missingTxs: originalHashes.slice(6) }; await checkResults(results, expected); expect(txPools.size).toEqual(6); @@ -159,7 +161,7 @@ describe('TxProvider', () => { const txs = shuffleTxs([...original]); const hashes = await Promise.all(txs.map(tx => tx.getTxHash())); const proposal = buildProposal(original.slice(6), hashes); - const results = await txProvider.getTxsForBlockProposal(proposal, opts); + const results = await txProvider.getTxsForBlockProposal(proposal, blockNumber, opts); const expected: TxResults = { txs, missingTxs: [] }; await checkResults(results, expected); // all txs should be in the pool @@ -186,7 +188,7 @@ describe('TxProvider', () => { ).map(method => jest.spyOn(txProvider.instrumentation, method)); // Check result is correct - const results = await txProvider.getTxsForBlockProposal(proposal, opts); + const results = await txProvider.getTxsForBlockProposal(proposal, blockNumber, opts); const expected: TxResults = { txs: txs.slice(0, 8), missingTxs: txs.slice(8).map(t => t.txHash) }; await checkResults(results, expected); expect(txPools.size).toEqual(8); @@ -212,7 +214,7 @@ describe('TxProvider', () => { const txs = original; const hashes = await Promise.all(txs.map(tx => tx.getTxHash())); const proposal = buildProposal(txs.slice(4, 8), hashes); - const results = await txProvider.getTxsForBlockProposal(proposal, opts); + const results = await txProvider.getTxsForBlockProposal(proposal, blockNumber, opts); const expected: TxResults = { txs: txs.slice(0, 8), missingTxs: originalHashes.slice(8) }; await checkResults(results, expected); // all txs should be in the pool @@ -231,7 +233,7 @@ describe('TxProvider', () => { // Add additional txs and these should not be added to the pool and not in the results const proposal = buildProposal(txs.slice(4, 8).concat(additional), hashes); - const results = await txProvider.getTxsForBlockProposal(proposal, opts); + const results = await txProvider.getTxsForBlockProposal(proposal, blockNumber, opts); const expected: TxResults = { txs: txs.slice(0, 8), missingTxs: originalHashes.slice(8) }; await checkResults(results, expected); // all txs should be in the pool diff --git a/yarn-project/p2p/src/services/tx_provider.ts b/yarn-project/p2p/src/services/tx_provider.ts index 0e2459ab4699..ef640405d049 100644 --- a/yarn-project/p2p/src/services/tx_provider.ts +++ b/yarn-project/p2p/src/services/tx_provider.ts @@ -55,11 +55,12 @@ export class TxProvider implements ITxProvider { /** Gathers txs from the tx pool, proposal body, remote rpc nodes, and reqresp. */ public getTxsForBlockProposal( blockProposal: BlockProposal, + blockNumber: number, opts: { pinnedPeer: PeerId | undefined; deadline: Date }, ): Promise<{ txs: Tx[]; missingTxs: TxHash[] }> { return this.getOrderedTxsFromAllSources( - { type: 'proposal', blockProposal }, - blockProposal.toBlockInfo(), + { type: 'proposal', blockProposal, blockNumber }, + { ...blockProposal.toBlockInfo(), blockNumber }, blockProposal.txHashes, { ...opts, pinnedPeer: opts.pinnedPeer }, ); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index c04ae006c55c..f8710479eb19 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -93,12 +93,7 @@ describe('sequencer', () => { const getAttestations = () => { const consensusPayload = ConsensusPayload.fromBlock(block); - const attestation = new BlockAttestation( - block.header.globalVariables.blockNumber, - consensusPayload, - mockedSig, - mockedSig, - ); + const attestation = new BlockAttestation(consensusPayload, mockedSig, mockedSig); (attestation as any).sender = committee[0]; return [attestation]; }; @@ -106,7 +101,7 @@ describe('sequencer', () => { const createBlockProposal = () => { const consensusPayload = ConsensusPayload.fromBlock(block); const txHashes = block.body.txEffects.map(tx => tx.txHash); - return new BlockProposal(block.header.globalVariables.blockNumber, consensusPayload, mockedSig, txHashes); + return new BlockProposal(consensusPayload, mockedSig, txHashes); }; const processTxs = async (txs: Tx[]) => { diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 2946abd3836b..a66466bd174f 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -1,4 +1,5 @@ import type { EthAddress } from '@aztec/foundation/eth-address'; +import type { Fr } from '@aztec/foundation/fields'; import type { TypedEventEmitter } from '@aztec/foundation/types'; import { z } from 'zod'; @@ -66,6 +67,34 @@ export interface L2BlockSource { /** Equivalent to getBlocks but includes publish data. */ getPublishedBlocks(from: number, limit: number, proven?: boolean): Promise; + /** + * Gets a published block by its hash. + * @param blockHash - The block hash to retrieve. + * @returns The requested published block (or undefined if not found). + */ + getPublishedBlockByHash(blockHash: Fr): Promise; + + /** + * Gets a published block by its archive root. + * @param archive - The archive root to retrieve. + * @returns The requested published block (or undefined if not found). + */ + getPublishedBlockByArchive(archive: Fr): Promise; + + /** + * Gets a block header by its hash. + * @param blockHash - The block hash to retrieve. + * @returns The requested block header (or undefined if not found). + */ + getBlockHeaderByHash(blockHash: Fr): Promise; + + /** + * Gets a block header by its archive root. + * @param archive - The archive root to retrieve. + * @returns The requested block header (or undefined if not found). + */ + getBlockHeaderByArchive(archive: Fr): Promise; + /** * Gets a tx effect. * @param txHash - The hash of the tx corresponding to the tx effect. @@ -120,6 +149,9 @@ export interface L2BlockSource { */ getL1Constants(): Promise; + /** Returns values for the genesis block */ + getGenesisValues(): Promise<{ genesisArchiveRoot: Fr }>; + /** Latest synced L1 timestamp. */ getL1Timestamp(): Promise; diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index d06a339164c0..9fecaeb83fbe 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -91,6 +91,16 @@ describe('ArchiverApiSchema', () => { expect(result).toBeInstanceOf(BlockHeader); }); + it('getBlockHeaderByArchive', async () => { + const result = await context.client.getBlockHeaderByArchive(Fr.random()); + expect(result).toBeInstanceOf(BlockHeader); + }); + + it('getBlockHeaderByHash', async () => { + const result = await context.client.getBlockHeaderByHash(Fr.random()); + expect(result).toBeInstanceOf(BlockHeader); + }); + it('getBlocks', async () => { const result = await context.client.getBlocks(1, 1); expect(result).toEqual([expect.any(L2Block)]); @@ -104,6 +114,22 @@ describe('ArchiverApiSchema', () => { expect(response[0].l1).toBeDefined(); }); + it('getPublishedBlockByArchive', async () => { + const result = await context.client.getPublishedBlockByArchive(Fr.random()); + expect(result).toBeDefined(); + expect(result!.block.constructor.name).toEqual('L2Block'); + expect(result!.attestations[0]).toBeInstanceOf(CommitteeAttestation); + expect(result!.l1).toBeDefined(); + }); + + it('getPublishedBlockByHash', async () => { + const result = await context.client.getPublishedBlockByHash(Fr.random()); + expect(result).toBeDefined(); + expect(result!.block.constructor.name).toEqual('L2Block'); + expect(result!.attestations[0]).toBeInstanceOf(CommitteeAttestation); + expect(result!.l1).toBeDefined(); + }); + it('getTxEffect', async () => { const result = await context.client.getTxEffect(TxHash.fromBuffer(Buffer.alloc(32, 1))); expect(result!.data).toBeInstanceOf(TxEffect); @@ -256,11 +282,19 @@ describe('ArchiverApiSchema', () => { const result = await context.client.isPendingChainInvalid(); expect(result).toBe(false); }); + + it('getGenesisValues', async () => { + const result = await context.client.getGenesisValues(); + expect(result).toEqual({ genesisArchiveRoot: expect.any(Fr) }); + }); }); class MockArchiver implements ArchiverApi { constructor(private artifact: ContractArtifact) {} + getGenesisValues(): Promise<{ genesisArchiveRoot: Fr }> { + return Promise.resolve({ genesisArchiveRoot: Fr.random() }); + } isPendingChainInvalid(): Promise { return Promise.resolve(false); } @@ -300,6 +334,26 @@ class MockArchiver implements ArchiverApi { }), ]; } + async getPublishedBlockByHash(_blockHash: Fr): Promise { + return PublishedL2Block.fromFields({ + block: await L2Block.random(1), + attestations: [CommitteeAttestation.random()], + l1: { blockHash: `0x`, blockNumber: 1n, timestamp: 0n }, + }); + } + async getPublishedBlockByArchive(_archive: Fr): Promise { + return PublishedL2Block.fromFields({ + block: await L2Block.random(1), + attestations: [CommitteeAttestation.random()], + l1: { blockHash: `0x`, blockNumber: 1n, timestamp: 0n }, + }); + } + getBlockHeaderByHash(_blockHash: Fr): Promise { + return Promise.resolve(BlockHeader.empty()); + } + getBlockHeaderByArchive(_archive: Fr): Promise { + return Promise.resolve(BlockHeader.empty()); + } async getTxEffect(_txHash: TxHash): Promise { expect(_txHash).toBeInstanceOf(TxHash); return { diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index fa36b76d60c0..e5ca51c73d36 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -83,6 +83,10 @@ export const ArchiverApiSchema: ApiSchemaFor = { .function() .args(schemas.Integer, schemas.Integer, optional(z.boolean())) .returns(z.array(PublishedL2Block.schema)), + getPublishedBlockByHash: z.function().args(schemas.Fr).returns(PublishedL2Block.schema.optional()), + getPublishedBlockByArchive: z.function().args(schemas.Fr).returns(PublishedL2Block.schema.optional()), + getBlockHeaderByHash: z.function().args(schemas.Fr).returns(BlockHeader.schema.optional()), + getBlockHeaderByArchive: z.function().args(schemas.Fr).returns(BlockHeader.schema.optional()), getTxEffect: z.function().args(TxHash.schema).returns(indexedTxSchema().optional()), getSettledTxReceipt: z.function().args(TxHash.schema).returns(TxReceipt.schema.optional()), getL2SlotNumber: z.function().args().returns(schemas.BigInt), @@ -110,6 +114,10 @@ export const ArchiverApiSchema: ApiSchemaFor = { getL1ToL2MessageIndex: z.function().args(schemas.Fr).returns(schemas.BigInt.optional()), getDebugFunctionName: z.function().args(schemas.AztecAddress, schemas.FunctionSelector).returns(optional(z.string())), getL1Constants: z.function().args().returns(L1RollupConstantsSchema), + getGenesisValues: z + .function() + .args() + .returns(z.object({ genesisArchiveRoot: schemas.Fr })), getL1Timestamp: z.function().args().returns(schemas.BigInt), syncImmediate: z.function().args().returns(z.void()), isPendingChainInvalid: z.function().args().returns(z.boolean()), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 17ed537238e8..eeba21760f49 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -174,6 +174,26 @@ describe('AztecNodeApiSchema', () => { expect(response).toBeInstanceOf(L2Block); }); + it('getBlockByHash', async () => { + const response = await context.client.getBlockByHash(Fr.random()); + expect(response).toBeInstanceOf(L2Block); + }); + + it('getBlockByArchive', async () => { + const response = await context.client.getBlockByArchive(Fr.random()); + expect(response).toBeInstanceOf(L2Block); + }); + + it('getBlockHeaderByHash', async () => { + const response = await context.client.getBlockHeaderByHash(Fr.random()); + expect(response).toBeInstanceOf(BlockHeader); + }); + + it('getBlockHeaderByArchive', async () => { + const response = await context.client.getBlockHeaderByArchive(Fr.random()); + expect(response).toBeInstanceOf(BlockHeader); + }); + it('getCurrentBaseFees', async () => { const response = await context.client.getCurrentBaseFees(); expect(response).toEqual(GasFees.empty()); @@ -585,6 +605,18 @@ class MockAztecNode implements AztecNode { getBlock(number: number): Promise { return Promise.resolve(L2Block.random(number)); } + getBlockByHash(_blockHash: Fr): Promise { + return Promise.resolve(L2Block.random(1)); + } + getBlockByArchive(_archive: Fr): Promise { + return Promise.resolve(L2Block.random(1)); + } + getBlockHeaderByHash(_blockHash: Fr): Promise { + return Promise.resolve(BlockHeader.empty()); + } + getBlockHeaderByArchive(_archive: Fr): Promise { + return Promise.resolve(BlockHeader.empty()); + } getCurrentBaseFees(): Promise { return Promise.resolve(GasFees.empty()); } diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 11b5b2bee02f..4a1fbd94cf01 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -224,6 +224,20 @@ export interface AztecNode */ getBlock(number: L2BlockNumber): Promise; + /** + * Get a block specified by its hash. + * @param blockHash - The block hash being requested. + * @returns The requested block. + */ + getBlockByHash(blockHash: Fr): Promise; + + /** + * Get a block specified by its archive root. + * @param archive - The archive root being requested. + * @returns The requested block. + */ + getBlockByArchive(archive: Fr): Promise; + /** * Method to fetch the latest block number synchronized by the node. * @returns The block number. @@ -399,6 +413,20 @@ export interface AztecNode */ getBlockHeader(blockNumber?: L2BlockNumber): Promise; + /** + * Get a block header specified by its hash. + * @param blockHash - The block hash being requested. + * @returns The requested block header. + */ + getBlockHeaderByHash(blockHash: Fr): Promise; + + /** + * Get a block header specified by its archive root. + * @param archive - The archive root being requested. + * @returns The requested block header. + */ + getBlockHeaderByArchive(archive: Fr): Promise; + /** Returns stats for validators if enabled. */ getValidatorsStats(): Promise; @@ -516,6 +544,10 @@ export const AztecNodeApiSchema: ApiSchemaFor = { getBlock: z.function().args(L2BlockNumberSchema).returns(L2Block.schema.optional()), + getBlockByHash: z.function().args(schemas.Fr).returns(L2Block.schema.optional()), + + getBlockByArchive: z.function().args(schemas.Fr).returns(L2Block.schema.optional()), + getBlockNumber: z.function().returns(z.number()), getProvenBlockNumber: z.function().returns(z.number()), @@ -589,6 +621,10 @@ export const AztecNodeApiSchema: ApiSchemaFor = { getBlockHeader: z.function().args(optional(L2BlockNumberSchema)).returns(BlockHeader.schema.optional()), + getBlockHeaderByHash: z.function().args(schemas.Fr).returns(BlockHeader.schema.optional()), + + getBlockHeaderByArchive: z.function().args(schemas.Fr).returns(BlockHeader.schema.optional()), + getValidatorsStats: z.function().returns(ValidatorsStatsSchema), getValidatorStats: z diff --git a/yarn-project/stdlib/src/interfaces/tx_provider.ts b/yarn-project/stdlib/src/interfaces/tx_provider.ts index 1cbde3a3bb1f..4113b69cae5f 100644 --- a/yarn-project/stdlib/src/interfaces/tx_provider.ts +++ b/yarn-project/stdlib/src/interfaces/tx_provider.ts @@ -9,6 +9,7 @@ export interface ITxProvider { getTxsForBlockProposal( blockProposal: BlockProposal, + blockNumber: number, opts: { pinnedPeer: PeerId | undefined; deadline: Date }, ): Promise<{ txs: Tx[]; missingTxs: TxHash[] }>; diff --git a/yarn-project/stdlib/src/p2p/block_attestation.ts b/yarn-project/stdlib/src/p2p/block_attestation.ts index 715ee9b87c34..3080b1cc91ac 100644 --- a/yarn-project/stdlib/src/p2p/block_attestation.ts +++ b/yarn-project/stdlib/src/p2p/block_attestation.ts @@ -7,8 +7,7 @@ import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import { z } from 'zod'; -import { type ZodFor, schemas } from '../schemas/index.js'; -import type { UInt32 } from '../types/index.js'; +import type { ZodFor } from '../schemas/index.js'; import { ConsensusPayload } from './consensus_payload.js'; import { Gossipable } from './gossipable.js'; import { SignatureDomainSeparator, getHashedSignaturePayloadEthSignedMessage } from './signature_utils.js'; @@ -33,9 +32,6 @@ export class BlockAttestation extends Gossipable { private proposer: EthAddress | undefined; constructor( - /** The block number of the attestation. */ - public readonly blockNumber: UInt32, - /** The payload of the message, and what the signature is over */ public readonly payload: ConsensusPayload, @@ -51,12 +47,11 @@ export class BlockAttestation extends Gossipable { static get schema(): ZodFor { return z .object({ - blockNumber: schemas.UInt32, payload: ConsensusPayload.schema, signature: Signature.schema, proposerSignature: Signature.schema, }) - .transform(obj => new BlockAttestation(obj.blockNumber, obj.payload, obj.signature, obj.proposerSignature)); + .transform(obj => new BlockAttestation(obj.payload, obj.signature, obj.proposerSignature)); } override generateP2PMessageIdentifier(): Promise { @@ -90,12 +85,12 @@ export class BlockAttestation extends Gossipable { * Lazily evaluate and cache the proposer of the block * @returns The proposer of the block */ - getProposer(): EthAddress { + getProposer(): EthAddress | undefined { if (!this.proposer) { // Recover the proposer from the proposal signature const hashed = getHashedSignaturePayloadEthSignedMessage(this.payload, SignatureDomainSeparator.blockProposal); // Cache the proposer for later use - this.proposer = tryRecoverAddress(hashed, this.proposerSignature)!; + this.proposer = tryRecoverAddress(hashed, this.proposerSignature); } return this.proposer; @@ -106,13 +101,12 @@ export class BlockAttestation extends Gossipable { } toBuffer(): Buffer { - return serializeToBuffer([this.blockNumber, this.payload, this.signature, this.proposerSignature]); + return serializeToBuffer([this.payload, this.signature, this.proposerSignature]); } static fromBuffer(buf: Buffer | BufferReader): BlockAttestation { const reader = BufferReader.asReader(buf); return new BlockAttestation( - reader.readNumber(), reader.readObject(ConsensusPayload), reader.readObject(Signature), reader.readObject(Signature), @@ -120,25 +114,19 @@ export class BlockAttestation extends Gossipable { } static empty(): BlockAttestation { - return new BlockAttestation(0, ConsensusPayload.empty(), Signature.empty(), Signature.empty()); + return new BlockAttestation(ConsensusPayload.empty(), Signature.empty(), Signature.empty()); } static random(): BlockAttestation { - return new BlockAttestation( - Math.floor(Math.random() * 1000) + 1, - ConsensusPayload.random(), - Signature.random(), - Signature.random(), - ); + return new BlockAttestation(ConsensusPayload.random(), Signature.random(), Signature.random()); } getSize(): number { - return 4 /* blockNumber */ + this.payload.getSize() + this.signature.getSize() + this.proposerSignature.getSize(); + return this.payload.getSize() + this.signature.getSize() + this.proposerSignature.getSize(); } toInspect() { return { - blockNumber: this.blockNumber, payload: this.payload.toInspect(), signature: this.signature.toString(), proposerSignature: this.proposerSignature.toString(), diff --git a/yarn-project/stdlib/src/p2p/block_proposal.test.ts b/yarn-project/stdlib/src/p2p/block_proposal.test.ts index 062f105787e5..2ef091b25697 100644 --- a/yarn-project/stdlib/src/p2p/block_proposal.test.ts +++ b/yarn-project/stdlib/src/p2p/block_proposal.test.ts @@ -11,17 +11,16 @@ import { ConsensusPayload } from './consensus_payload.js'; class BackwardsCompatibleBlockProposal extends BlockProposal { constructor(payload: ConsensusPayload, signature: Signature) { - super(1, payload, signature, [], undefined); + super(payload, signature, [], undefined); } oldToBuffer(): Buffer { - return serializeToBuffer([this.blockNumber, this.payload, this.signature, 0, []]); + return serializeToBuffer([this.payload, this.signature, 0, []]); } static oldFromBuffer(buf: Buffer | BufferReader): BlockProposal { const reader = BufferReader.asReader(buf); return new BlockProposal( - reader.readNumber(), reader.readObject(ConsensusPayload), reader.readObject(Signature), reader.readArray(0, TxHash), diff --git a/yarn-project/stdlib/src/p2p/block_proposal.ts b/yarn-project/stdlib/src/p2p/block_proposal.ts index 394d64f8c2e3..c619695cbe6b 100644 --- a/yarn-project/stdlib/src/p2p/block_proposal.ts +++ b/yarn-project/stdlib/src/p2p/block_proposal.ts @@ -8,7 +8,6 @@ import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; import type { L2BlockInfo } from '../block/l2_block_info.js'; import { TxHash } from '../tx/index.js'; import { Tx } from '../tx/tx.js'; -import type { UInt32 } from '../types/index.js'; import { ConsensusPayload } from './consensus_payload.js'; import { Gossipable } from './gossipable.js'; import { @@ -40,9 +39,6 @@ export class BlockProposal extends Gossipable { private sender: EthAddress | undefined; constructor( - /** The number of the block */ - public readonly blockNumber: UInt32, - /** The payload of the message, and what the signature is over */ public readonly payload: ConsensusPayload, @@ -71,9 +67,8 @@ export class BlockProposal extends Gossipable { return this.payload.header.slotNumber; } - toBlockInfo(): L2BlockInfo { + toBlockInfo(): Omit { return { - blockNumber: this.blockNumber, slotNumber: this.slotNumber.toNumber(), lastArchive: this.payload.header.lastArchiveRoot, timestamp: this.payload.header.timestamp, @@ -83,7 +78,6 @@ export class BlockProposal extends Gossipable { } static async createProposalFromSigner( - blockNumber: UInt32, payload: ConsensusPayload, txHashes: TxHash[], // Note(md): Provided separately to tx hashes such that this function can be optional @@ -93,7 +87,7 @@ export class BlockProposal extends Gossipable { const hashed = getHashedSignaturePayload(payload, SignatureDomainSeparator.blockProposal); const sig = await payloadSigner(hashed); - return new BlockProposal(blockNumber, payload, sig, txHashes, txs); + return new BlockProposal(payload, sig, txHashes, txs); } /**Get Sender @@ -115,7 +109,7 @@ export class BlockProposal extends Gossipable { } toBuffer(): Buffer { - const buffer: any[] = [this.blockNumber, this.payload, this.signature, this.txHashes.length, this.txHashes]; + const buffer: any[] = [this.payload, this.signature, this.txHashes.length, this.txHashes]; if (this.txs) { buffer.push(this.txs.length); buffer.push(this.txs); @@ -126,22 +120,20 @@ export class BlockProposal extends Gossipable { static fromBuffer(buf: Buffer | BufferReader): BlockProposal { const reader = BufferReader.asReader(buf); - const blockNumber = reader.readNumber(); const payload = reader.readObject(ConsensusPayload); const sig = reader.readObject(Signature); const txHashes = reader.readArray(reader.readNumber(), TxHash); if (!reader.isEmpty()) { const txs = reader.readArray(reader.readNumber(), Tx); - return new BlockProposal(blockNumber, payload, sig, txHashes, txs); + return new BlockProposal(payload, sig, txHashes, txs); } - return new BlockProposal(blockNumber, payload, sig, txHashes); + return new BlockProposal(payload, sig, txHashes); } getSize(): number { return ( - 4 /* blockNumber */ + this.payload.getSize() + this.signature.getSize() + 4 /* txHashes.length */ + diff --git a/yarn-project/stdlib/src/tests/mocks.ts b/yarn-project/stdlib/src/tests/mocks.ts index 793d57697eed..14acf75e23a5 100644 --- a/yarn-project/stdlib/src/tests/mocks.ts +++ b/yarn-project/stdlib/src/tests/mocks.ts @@ -288,12 +288,9 @@ export const makeAndSignCommitteeAttestationsAndSigners = ( }; export const makeBlockProposal = (options?: MakeConsensusPayloadOptions): BlockProposal => { - const { blockNumber, payload, signature } = makeAndSignConsensusPayload( - SignatureDomainSeparator.blockProposal, - options, - ); + const { payload, signature } = makeAndSignConsensusPayload(SignatureDomainSeparator.blockProposal, options); const txHashes = options?.txHashes ?? [0, 1, 2, 3, 4, 5].map(() => TxHash.random()); - return new BlockProposal(blockNumber, payload, signature, txHashes, options?.txs ?? []); + return new BlockProposal(payload, signature, txHashes, options?.txs ?? []); }; // TODO(https://github.com/AztecProtocol/aztec-packages/issues/8028) @@ -321,7 +318,7 @@ export const makeBlockAttestation = (options?: MakeConsensusPayloadOptions): Blo const proposalHash = getHashedSignaturePayloadEthSignedMessage(payload, SignatureDomainSeparator.blockProposal); const proposerSignature = proposerSigner.sign(proposalHash); - return new BlockAttestation(header.globalVariables.blockNumber, payload, attestationSignature, proposerSignature); + return new BlockAttestation(payload, attestationSignature, proposerSignature); }; export const makeBlockAttestationFromBlock = ( @@ -349,7 +346,7 @@ export const makeBlockAttestationFromBlock = ( const proposalSignerToUse = proposerSigner ?? Secp256k1Signer.random(); const proposerSignature = proposalSignerToUse.sign(proposalHash); - return new BlockAttestation(header.globalVariables.blockNumber, payload, attestationSignature, proposerSignature); + return new BlockAttestation(payload, attestationSignature, proposerSignature); }; export async function randomPublishedL2Block( diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index 4f2ca9523537..1430f32ab99d 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -1,5 +1,7 @@ import { ArchiverStoreHelper, KVArchiverDataStore, type PublishedL2Block } from '@aztec/archiver'; +import { GENESIS_ARCHIVE_ROOT } from '@aztec/constants'; import type { EthAddress } from '@aztec/foundation/eth-address'; +import { Fr } from '@aztec/foundation/fields'; import type { AztecAsyncKVStore } from '@aztec/kv-store'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2Block, L2BlockSource, L2Tips, ValidateBlockResult } from '@aztec/stdlib/block'; @@ -115,6 +117,10 @@ export class TXEArchiver extends ArchiverStoreHelper implements L2BlockSource { throw new Error('TXE Archiver does not implement "getL2Constants"'); } + public getGenesisValues(): Promise<{ genesisArchiveRoot: Fr }> { + return Promise.resolve({ genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT) }); + } + public syncImmediate(): Promise { throw new Error('TXE Archiver does not implement "syncImmediate"'); } diff --git a/yarn-project/validator-client/src/block_proposal_handler.ts b/yarn-project/validator-client/src/block_proposal_handler.ts index 358846609517..51090fa2f800 100644 --- a/yarn-project/validator-client/src/block_proposal_handler.ts +++ b/yarn-project/validator-client/src/block_proposal_handler.ts @@ -1,4 +1,5 @@ import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; +import { TimeoutError } from '@aztec/foundation/error'; import { Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; import { retryUntil } from '@aztec/foundation/retry'; @@ -7,12 +8,12 @@ import type { P2P, PeerId } from '@aztec/p2p'; import { TxProvider } from '@aztec/p2p'; import { BlockProposalValidator } from '@aztec/p2p/msg_validators'; import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers'; -import type { L2BlockSource } from '@aztec/stdlib/block'; +import type { L2Block, L2BlockSource } from '@aztec/stdlib/block'; import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; import type { IFullNodeBlockBuilder, ValidatorClientFullConfig } from '@aztec/stdlib/interfaces/server'; import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { type BlockProposal, ConsensusPayload } from '@aztec/stdlib/p2p'; -import { type FailedTx, GlobalVariables, type Tx } from '@aztec/stdlib/tx'; +import { BlockHeader, type FailedTx, GlobalVariables, type Tx } from '@aztec/stdlib/tx'; import { ReExFailedTxsError, ReExStateMismatchError, @@ -26,7 +27,7 @@ import type { ValidatorMetrics } from './metrics.js'; export type BlockProposalValidationFailureReason = | 'invalid_proposal' | 'parent_block_not_found' - | 'parent_block_does_not_match' + | 'parent_block_wrong_slot' | 'in_hash_mismatch' | 'block_number_already_exists' | 'txs_not_available' @@ -35,16 +36,27 @@ export type BlockProposalValidationFailureReason = | 'timeout' | 'unknown_error'; -export interface BlockProposalValidationResult { - isValid: boolean; - reason?: BlockProposalValidationFailureReason; - reexecutionResult?: { - block: any; - failedTxs: FailedTx[]; - reexecutionTimeMs: number; - totalManaUsed: number; - }; -} +type ReexecuteTransactionsResult = { + block: L2Block; + failedTxs: FailedTx[]; + reexecutionTimeMs: number; + totalManaUsed: number; +}; + +export type BlockProposalValidationSuccessResult = { + isValid: true; + blockNumber: number; + reexecutionResult?: ReexecuteTransactionsResult; +}; + +export type BlockProposalValidationFailureResult = { + isValid: false; + reason: BlockProposalValidationFailureReason; + blockNumber?: number; + reexecutionResult?: ReexecuteTransactionsResult; +}; + +export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult; export class BlockProposalHandler { public readonly tracer: Tracer; @@ -68,16 +80,16 @@ export class BlockProposalHandler { const handler = async (proposal: BlockProposal, proposalSender: PeerId) => { try { const result = await this.handleBlockProposal(proposal, proposalSender, true); - if (result.isValid && result.reexecutionResult) { + if (result.isValid) { this.log.info(`Non-validator reexecution completed for slot ${proposal.slotNumber.toBigInt()}`, { - blockNumber: proposal.blockNumber, - reexecutionTimeMs: result.reexecutionResult.reexecutionTimeMs, - totalManaUsed: result.reexecutionResult.totalManaUsed, - numTxs: result.reexecutionResult.block?.body?.txEffects?.length ?? 0, + blockNumber: result.blockNumber, + reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs, + totalManaUsed: result.reexecutionResult?.totalManaUsed, + numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0, }); } else { this.log.warn(`Non-validator reexecution failed for slot ${proposal.slotNumber.toBigInt()}`, { - blockNumber: proposal.blockNumber, + blockNumber: result.blockNumber, reason: result.reason, }); } @@ -97,8 +109,8 @@ export class BlockProposalHandler { shouldReexecute: boolean, ): Promise { const slotNumber = proposal.slotNumber.toBigInt(); - const blockNumber = proposal.blockNumber; const proposer = proposal.getSender(); + const config = this.blockBuilder.getConfig(); // Reject proposals with invalid signatures if (!proposer) { @@ -120,52 +132,40 @@ export class BlockProposalHandler { return { isValid: false, reason: 'invalid_proposal' }; } - // Collect txs from the proposal. We start doing this as early as possible, - // and we do it even if we don't plan to re-execute the txs, so that we have them - // if another node needs them. - const config = this.blockBuilder.getConfig(); - const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, { - pinnedPeer: proposalSender, - deadline: this.getReexecutionDeadline(proposal, config), - }); - // Check that the parent proposal is a block we know, otherwise reexecution would fail - if (blockNumber > INITIAL_L2_BLOCK_NUM) { - const deadline = this.getReexecutionDeadline(proposal, config); - const currentTime = this.dateProvider.now(); - const timeoutDurationMs = deadline.getTime() - currentTime; - const parentBlock = - timeoutDurationMs <= 0 - ? undefined - : await retryUntil( - async () => { - const block = await this.blockSource.getBlock(blockNumber - 1); - if (block) { - return block; - } - await this.blockSource.syncImmediate(); - return await this.blockSource.getBlock(blockNumber - 1); - }, - 'Force Archiver Sync', - timeoutDurationMs / 1000, - 0.5, - ); + const parentBlockHeader = await this.getParentBlock(proposal); + if (parentBlockHeader === undefined) { + this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo); + return { isValid: false, reason: 'parent_block_not_found' }; + } - if (parentBlock === undefined) { - this.log.warn(`Parent block for ${blockNumber} not found, skipping processing`, proposalInfo); - return { isValid: false, reason: 'parent_block_not_found' }; - } + // Check that the parent block's slot is less than the proposal's slot (should not happen, but we check anyway) + if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() >= slotNumber) { + this.log.warn(`Parent block slot is greater than or equal to proposal slot, skipping processing`, { + parentBlockSlot: parentBlockHeader.getSlot().toString(), + proposalSlot: slotNumber.toString(), + ...proposalInfo, + }); + return { isValid: false, reason: 'parent_block_wrong_slot' }; + } - if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) { - this.log.warn(`Parent block archive root for proposal does not match, skipping processing`, { - proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(), - parentBlockArchiveRoot: parentBlock.archive.root.toString(), - ...proposalInfo, - }); - return { isValid: false, reason: 'parent_block_does_not_match' }; - } + // Compute the block number based on the parent block + const blockNumber = parentBlockHeader === 'genesis' ? INITIAL_L2_BLOCK_NUM : parentBlockHeader.getBlockNumber() + 1; + + // Check that this block number does not exist already + const existingBlock = await this.blockSource.getBlockHeader(blockNumber); + if (existingBlock) { + this.log.warn(`Block number ${blockNumber} already exists, skipping processing`, proposalInfo); + return { isValid: false, blockNumber, reason: 'block_number_already_exists' }; } + // Collect txs from the proposal. We start doing this as early as possible, + // and we do it even if we don't plan to re-execute the txs, so that we have them if another node needs them. + const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, blockNumber, { + pinnedPeer: proposalSender, + deadline: this.getReexecutionDeadline(slotNumber, config), + }); + // Check that I have the same set of l1ToL2Messages as the proposal const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber); const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages); @@ -176,20 +176,13 @@ export class BlockProposalHandler { computedInHash: computedInHash.toString(), ...proposalInfo, }); - return { isValid: false, reason: 'in_hash_mismatch' }; - } - - // Check that this block number does not exist already - const existingBlock = await this.blockSource.getBlockHeader(blockNumber); - if (existingBlock) { - this.log.warn(`Block number ${blockNumber} already exists, skipping processing`, proposalInfo); - return { isValid: false, reason: 'block_number_already_exists' }; + return { isValid: false, blockNumber, reason: 'in_hash_mismatch' }; } // Check that all of the transactions in the proposal are available if (missingTxs.length > 0) { this.log.warn(`Missing ${missingTxs.length} txs to process proposal`, { ...proposalInfo, missingTxs }); - return { isValid: false, reason: 'txs_not_available' }; + return { isValid: false, blockNumber, reason: 'txs_not_available' }; } // Try re-executing the transactions in the proposal if needed @@ -197,23 +190,57 @@ export class BlockProposalHandler { if (shouldReexecute) { try { this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo); - reexecutionResult = await this.reexecuteTransactions(proposal, txs, l1ToL2Messages); + reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages); } catch (error) { this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo); const reason = this.getReexecuteFailureReason(error); - return { isValid: false, reason, reexecutionResult }; + return { isValid: false, blockNumber, reason, reexecutionResult }; } } this.log.info(`Successfully processed proposal for slot ${slotNumber}`, proposalInfo); - return { isValid: true, reexecutionResult }; + return { isValid: true, blockNumber, reexecutionResult }; } - private getReexecutionDeadline( - proposal: BlockProposal, - config: { l1GenesisTime: bigint; slotDuration: number }, - ): Date { - const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config)); + private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockHeader | undefined> { + const parentArchive = proposal.payload.header.lastArchiveRoot; + const slot = proposal.slotNumber.toBigInt(); + const config = this.blockBuilder.getConfig(); + const { genesisArchiveRoot } = await this.blockSource.getGenesisValues(); + + if (parentArchive.equals(genesisArchiveRoot)) { + return 'genesis'; + } + + const deadline = this.getReexecutionDeadline(slot, config); + const currentTime = this.dateProvider.now(); + const timeoutDurationMs = deadline.getTime() - currentTime; + + try { + return ( + (await this.blockSource.getBlockHeaderByArchive(parentArchive)) ?? + (timeoutDurationMs <= 0 + ? undefined + : await retryUntil( + () => + this.blockSource.syncImmediate().then(() => this.blockSource.getBlockHeaderByArchive(parentArchive)), + 'force archiver sync', + timeoutDurationMs / 1000, + 0.5, + )) + ); + } catch (err) { + if (err instanceof TimeoutError) { + this.log.debug(`Timed out getting parent block by archive root`, { parentArchive }); + } else { + this.log.error('Error getting parent block by archive root', err, { parentArchive }); + } + return undefined; + } + } + + private getReexecutionDeadline(slot: bigint, config: { l1GenesisTime: bigint; slotDuration: number }): Date { + const nextSlotTimestampSeconds = Number(getTimestampForSlot(slot + 1n, config)); const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs; return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing); } @@ -225,21 +252,17 @@ export class BlockProposalHandler { return 'failed_txs'; } else if (err instanceof ReExTimeoutError) { return 'timeout'; - } else if (err instanceof Error) { + } else { return 'unknown_error'; } } async reexecuteTransactions( proposal: BlockProposal, + blockNumber: number, txs: Tx[], l1ToL2Messages: Fr[], - ): Promise<{ - block: any; - failedTxs: FailedTx[]; - reexecutionTimeMs: number; - totalManaUsed: number; - }> { + ): Promise { const { header } = proposal.payload; const { txHashes } = proposal; @@ -260,14 +283,14 @@ export class BlockProposalHandler { coinbase: proposal.payload.header.coinbase, // set arbitrarily by the proposer feeRecipient: proposal.payload.header.feeRecipient, // set arbitrarily by the proposer gasFees: proposal.payload.header.gasFees, // validated by the rollup contract - blockNumber: proposal.blockNumber, // checked blockNumber-1 exists in archiver but blockNumber doesnt + blockNumber, // computed from the parent block and checked it does not exist in archiver timestamp: header.timestamp, // checked in the rollup contract against the slot number chainId: new Fr(config.l1ChainId), version: new Fr(config.rollupVersion), }); const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, globalVariables, { - deadline: this.getReexecutionDeadline(proposal, config), + deadline: this.getReexecutionDeadline(proposal.payload.header.slotNumber.toBigInt(), config), }); const numFailedTxs = failedTxs.length; diff --git a/yarn-project/validator-client/src/duties/validation_service.test.ts b/yarn-project/validator-client/src/duties/validation_service.test.ts index f28d36be5e6b..99f0e1d34217 100644 --- a/yarn-project/validator-client/src/duties/validation_service.test.ts +++ b/yarn-project/validator-client/src/duties/validation_service.test.ts @@ -25,20 +25,11 @@ describe('ValidationService', () => { it('creates a proposal with txs appended', async () => { const txs = await Promise.all([Tx.random(), Tx.random()]); const { - blockNumber, payload: { header, archive, stateReference }, } = makeBlockProposal({ txs }); - const proposal = await service.createBlockProposal( - blockNumber, - header, - archive, - stateReference, - txs, - addresses[0], - { - publishFullTxs: true, - }, - ); + const proposal = await service.createBlockProposal(header, archive, stateReference, txs, addresses[0], { + publishFullTxs: true, + }); expect(proposal.getSender()).toEqual(store.getAddress(0)); expect(proposal.txs).toBeDefined(); expect(proposal.txs).toBe(txs); @@ -47,20 +38,11 @@ describe('ValidationService', () => { it('creates a proposal without txs appended', async () => { const txs = await Promise.all([Tx.random(), Tx.random()]); const { - blockNumber, payload: { header, archive, stateReference }, } = makeBlockProposal({ txs }); - const proposal = await service.createBlockProposal( - blockNumber, - header, - archive, - stateReference, - txs, - addresses[0], - { - publishFullTxs: false, - }, - ); + const proposal = await service.createBlockProposal(header, archive, stateReference, txs, addresses[0], { + publishFullTxs: false, + }); expect(proposal.getSender()).toEqual(addresses[0]); expect(proposal.txs).toBeUndefined(); }); diff --git a/yarn-project/validator-client/src/duties/validation_service.ts b/yarn-project/validator-client/src/duties/validation_service.ts index 7af5e978387e..345895bc6b02 100644 --- a/yarn-project/validator-client/src/duties/validation_service.ts +++ b/yarn-project/validator-client/src/duties/validation_service.ts @@ -21,7 +21,6 @@ export class ValidationService { /** * Create a block proposal with the given header, archive, and transactions * - * @param blockNumber - The block number this proposal is for * @param header - The block header * @param archive - The archive of the current block * @param txs - TxHash[] ordered list of transactions @@ -29,7 +28,6 @@ export class ValidationService { * @returns A block proposal signing the above information (not the current implementation!!!) */ async createBlockProposal( - blockNumber: number, header: ProposedBlockHeader, archive: Fr, stateReference: StateReference, @@ -49,7 +47,6 @@ export class ValidationService { const txHashes = await Promise.all(txs.map(tx => tx.getTxHash())); return BlockProposal.createProposalFromSigner( - blockNumber, new ConsensusPayload(header, archive, stateReference), txHashes, options.publishFullTxs ? txs : undefined, @@ -74,7 +71,7 @@ export class ValidationService { const signatures = await Promise.all( attestors.map(attestor => this.keyStore.signMessageWithAddress(attestor, buf)), ); - return signatures.map(sig => new BlockAttestation(proposal.blockNumber, proposal.payload, sig, proposal.signature)); + return signatures.map(sig => new BlockAttestation(proposal.payload, sig, proposal.signature)); } async signAttestationsAndSigners( diff --git a/yarn-project/validator-client/src/validator.test.ts b/yarn-project/validator-client/src/validator.test.ts index 50ed2a6f2b3f..ef197da1624f 100644 --- a/yarn-project/validator-client/src/validator.test.ts +++ b/yarn-project/validator-client/src/validator.test.ts @@ -1,3 +1,4 @@ +import { GENESIS_ARCHIVE_ROOT } from '@aztec/constants'; import type { EpochCache } from '@aztec/epoch-cache'; import { Buffer32 } from '@aztec/foundation/buffer'; import { times } from '@aztec/foundation/collection'; @@ -220,6 +221,7 @@ describe('ValidatorClient', () => { describe('attestToProposal', () => { let proposal: BlockProposal; + let blockNumber: number; let sender: PeerId; let blockBuildResult: BuildBlockResult; @@ -234,6 +236,7 @@ describe('ValidatorClient', () => { const emptyInHash = await computeInHashFromL1ToL2Messages([]); const contentCommitment = new ContentCommitment(Fr.random(), emptyInHash, Fr.random()); const blockHeader = makeHeader(1, 100, 100, { contentCommitment }); + blockNumber = blockHeader.getBlockNumber(); proposal = makeBlockProposal({ header: blockHeader }); // Set the current time to the start of the slot of the proposal const genesisTime = 1n; @@ -261,9 +264,13 @@ describe('ValidatorClient', () => { }); epochCache.filterInCommittee.mockResolvedValue([EthAddress.fromString(validatorAccounts[0].address)]); - blockSource.getBlock.mockResolvedValue({ - archive: new AppendOnlyTreeSnapshot(proposal.payload.header.lastArchiveRoot, proposal.blockNumber), - } as L2Block); + // Return parent block when requested + blockSource.getBlockHeaderByArchive.mockResolvedValue({ + getBlockNumber: () => blockNumber - 1, + getSlot: () => blockHeader.getSlot() - 1n, + } as BlockHeader); + + blockSource.getGenesisValues.mockResolvedValue({ genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT) }); blockSource.syncImmediate.mockImplementation(() => Promise.resolve()); blockBuildResult = { @@ -277,7 +284,7 @@ describe('ValidatorClient', () => { block: { header: blockHeader.clone(), body: { txEffects: times(proposal.txHashes.length, () => ({})) }, - archive: new AppendOnlyTreeSnapshot(proposal.archive, proposal.blockNumber), + archive: new AppendOnlyTreeSnapshot(proposal.archive, blockNumber), } as L2Block, }; }); @@ -291,11 +298,11 @@ describe('ValidatorClient', () => { it('should wait for previous block to sync', async () => { epochCache.filterInCommittee.mockResolvedValue([EthAddress.fromString(validatorAccounts[0].address)]); - blockSource.getBlock.mockResolvedValueOnce(undefined); - blockSource.getBlock.mockResolvedValueOnce(undefined); - blockSource.getBlock.mockResolvedValueOnce(undefined); + blockSource.getBlockHeaderByArchive.mockResolvedValueOnce(undefined); + blockSource.getBlockHeaderByArchive.mockResolvedValueOnce(undefined); + blockSource.getBlockHeaderByArchive.mockResolvedValueOnce(undefined); const attestations = await validatorClient.attestToProposal(proposal, sender); - expect(blockSource.getBlock).toHaveBeenCalledTimes(4); + expect(blockSource.getBlockHeaderByArchive).toHaveBeenCalledTimes(4); expect(attestations).toBeDefined(); expect(attestations?.length).toBe(1); }); @@ -344,7 +351,7 @@ describe('ValidatorClient', () => { blockSource.getBlockHeader.mockResolvedValue({} as BlockHeader); const attestations = await validatorClient.attestToProposal(proposal, sender); expect(attestations).toBeUndefined(); - expect(blockSource.getBlockHeader).toHaveBeenCalledWith(proposal.blockNumber); + expect(blockSource.getBlockHeader).toHaveBeenCalledWith(blockNumber); }); it('should not emit WANT_TO_SLASH_EVENT if slashing is disabled', async () => { @@ -365,6 +372,7 @@ describe('ValidatorClient', () => { expect(txProvider.getTxsForBlockProposal).toHaveBeenCalledWith( proposal, + blockNumber, expect.objectContaining({ pinnedPeer: sender }), ); }); @@ -377,6 +385,7 @@ describe('ValidatorClient', () => { expect(txProvider.getTxsForBlockProposal).toHaveBeenCalledWith( proposal, + blockNumber, expect.objectContaining({ pinnedPeer: sender }), ); }); diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index 800b0d345232..3fa68e02fdb5 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -183,8 +183,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) } // Proxy method for backwards compatibility with tests - public reExecuteTransactions(proposal: BlockProposal, txs: any[], l1ToL2Messages: Fr[]): Promise { - return this.blockProposalHandler.reexecuteTransactions(proposal, txs, l1ToL2Messages); + public reExecuteTransactions( + proposal: BlockProposal, + blockNumber: number, + txs: any[], + l1ToL2Messages: Fr[], + ): Promise { + return this.blockProposalHandler.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages); } public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) { @@ -268,7 +273,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) const incFailedAttestation = (reason: string) => this.metrics.incFailedAttestations(1, reason, partOfCommittee); const proposalInfo = { ...proposal.toBlockInfo(), proposer: proposer.toString() }; - this.log.info(`Received proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, { + this.log.info(`Received proposal for slot ${slotNumber}`, { ...proposalInfo, txHashes: proposal.txHashes.map(t => t.toString()), }); @@ -310,7 +315,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) } // Provided all of the above checks pass, we can attest to the proposal - this.log.info(`Attesting to proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, proposalInfo); + this.log.info(`Attesting to proposal for block at slot ${slotNumber}`, proposalInfo); this.metrics.incAttestations(inCommittee.length); // If the above function does not throw an error, then we can attest to the proposal @@ -359,7 +364,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) } const newProposal = await this.validationService.createBlockProposal( - blockNumber, header, archive, stateReference,