From de6b4d4cb5894f0095f9c2f0f6f5066823ada58a Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 22 Oct 2025 18:44:22 -0300 Subject: [PATCH] fix: Remove block number from p2p proposals and attestations Block number was NOT being signed over, this meant any node could tweak it and cause receiving nodes to consider the proposal incorrect. We fix this by fetching the parent node using the last archive root (which required adding new indices to the archiver) and computing the block number by adding one to it. As for attestations, the block number was not used at all, so we could remove it. --- .../archiver/src/archiver/archiver.test.ts | 3 +- .../archiver/src/archiver/archiver.ts | 38 +++- .../archiver/src/archiver/archiver_store.ts | 24 +++ .../src/archiver/archiver_store_test_suite.ts | 102 +++++++++ .../archiver/kv_archiver_store/block_store.ts | 77 +++++++ .../kv_archiver_store/kv_archiver_store.ts | 18 +- .../archiver/src/test/mock_l2_block_source.ts | 58 +++++- .../aztec-node/src/aztec-node/server.ts | 38 ++++ .../e2e_multi_validator_node.test.ts | 4 +- .../src/e2e_p2p/gossip_network.test.ts | 2 +- .../e2e_p2p/gossip_network_no_cheat.test.ts | 2 +- .../e2e_p2p/preferred_gossip_network.test.ts | 2 +- .../end-to-end/src/e2e_p2p/reex.test.ts | 25 ++- yarn-project/p2p/src/client/p2p_client.ts | 9 +- .../attestation_pool_test_suite.ts | 3 +- .../src/mem_pools/attestation_pool/mocks.ts | 2 +- .../attestation_validator.ts | 4 + .../p2p/src/services/libp2p/libp2p_service.ts | 36 ++-- .../tx_collection/fast_tx_collection.ts | 4 +- .../services/tx_collection/tx_collection.ts | 5 +- .../p2p/src/services/tx_provider.test.ts | 18 +- yarn-project/p2p/src/services/tx_provider.ts | 5 +- .../src/sequencer/sequencer.test.ts | 9 +- .../stdlib/src/block/l2_block_source.ts | 32 +++ .../stdlib/src/interfaces/archiver.test.ts | 54 +++++ .../stdlib/src/interfaces/archiver.ts | 8 + .../stdlib/src/interfaces/aztec-node.test.ts | 32 +++ .../stdlib/src/interfaces/aztec-node.ts | 36 ++++ .../stdlib/src/interfaces/tx_provider.ts | 1 + .../stdlib/src/p2p/block_attestation.ts | 28 +-- .../stdlib/src/p2p/block_proposal.test.ts | 5 +- yarn-project/stdlib/src/p2p/block_proposal.ts | 18 +- yarn-project/stdlib/src/tests/mocks.ts | 11 +- .../txe/src/state_machine/archiver.ts | 6 + .../src/block_proposal_handler.ts | 194 ++++++++++-------- .../src/duties/validation_service.test.ts | 30 +-- .../src/duties/validation_service.ts | 7 +- .../validator-client/src/validator.test.ts | 27 ++- .../validator-client/src/validator.ts | 16 +- 39 files changed, 754 insertions(+), 239 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index d739d655b877..eeb23b746889 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -106,7 +106,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; @@ -167,6 +167,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 093238ae4139..5f841b1c582f 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 c8856cda52d9..6b372619b920 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -142,6 +142,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', () => { @@ -179,6 +201,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.getBlockHeader())).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.getBlockHeader())).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 38b0e9f404d1..071c8c76f1bb 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 L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader(); + } + + /** + * 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 L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader(); + } + /** * 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 2682b7cb39b7..21ec22234483 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.getBlockHeader(); + } + } + return undefined; + } + + public getBlockHeaderByArchive(archive: Fr): Promise { + const block = this.l2Blocks.find(b => b.archive.root.equals(archive)); + return Promise.resolve(block?.getBlockHeader()); + } + getBlockHeader(number: number | 'latest'): Promise { return Promise.resolve(this.l2Blocks.at(typeof number === 'number' ? number - 1 : -1)?.getBlockHeader()); } @@ -232,6 +284,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 e541b45a11c3..640cc28f92af 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -553,6 +553,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. @@ -1061,6 +1081,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 b4f73cab6581..51d102d51c94 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 @@ -134,7 +134,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); @@ -193,7 +193,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 a3dfc52a28ae..c77800b8ee02 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 @@ -175,7 +175,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 39cf1a3eb737..da7e4d735734 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 @@ -229,7 +229,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 c06943335dbc..30c7801ff9bf 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 @@ -359,7 +359,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 be36674e78df..5ad8b0811dd3 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 @@ -4,11 +4,13 @@ import { Fr } from '@aztec/aztec.js/fields'; import { Tx } from '@aztec/aztec.js/tx'; import { times } from '@aztec/foundation/collection'; import { sleep } from '@aztec/foundation/sleep'; +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'; @@ -124,25 +126,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 f4ddc90a6647..051e3ad64301 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 95dc787887b7..f731a746d54f 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 = makeL2BlockHeader(1, 2, slotNumber); const payload = new ConsensusPayload(header.toCheckpointHeader(), 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 1d7c08068308..0e030e32dafc 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 42b4b13948ce..d1a07811ea4e 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -756,12 +756,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(), }, ); @@ -794,7 +793,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()), @@ -802,16 +800,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}`); @@ -826,15 +820,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); } } @@ -845,7 +835,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()), @@ -1140,7 +1129,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 8c15967257af..9a5d61b9b1b7 100644 --- a/yarn-project/p2p/src/services/tx_provider.test.ts +++ b/yarn-project/p2p/src/services/tx_provider.test.ts @@ -37,7 +37,7 @@ describe('TxProvider', () => { const buildProposal = (txs: Tx[], txHashes: TxHash[]) => { const payload = new ConsensusPayload(CheckpointHeader.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[]) => { @@ -75,6 +75,8 @@ describe('TxProvider', () => { .map(({ value }) => value); }; + const blockNumber = 1; + beforeEach(() => { txPools.clear(); additionalP2PTxs.length = 0; @@ -117,7 +119,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); @@ -132,7 +134,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); @@ -146,7 +148,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); @@ -160,7 +162,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 @@ -187,7 +189,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); @@ -213,7 +215,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 @@ -232,7 +234,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 9590431e1b19..b7ebfd50291b 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -98,12 +98,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]; }; @@ -111,7 +106,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 395c432e1b9e..24ce3d2fead7 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 a75de5a14474..1b9203480574 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -175,6 +175,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()); @@ -591,6 +611,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 56e60a77a415..1ebf7f40b910 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -225,6 +225,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. @@ -400,6 +414,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; @@ -523,6 +551,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()), @@ -596,6 +628,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 7c62642c1c8e..328aa0bd9de7 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 { @@ -42,9 +41,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, @@ -73,9 +69,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, @@ -85,7 +80,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 @@ -95,7 +89,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 @@ -117,7 +111,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); @@ -128,22 +122,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 1fdeb3f48daf..12138a4a9c86 100644 --- a/yarn-project/stdlib/src/tests/mocks.ts +++ b/yarn-project/stdlib/src/tests/mocks.ts @@ -270,12 +270,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) @@ -303,7 +300,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 = ( @@ -331,7 +328,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 cf4e33bf55cc..2923b9cab75d 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 ab407fe93304..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,49 +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 = - (await this.blockSource.getBlock(blockNumber - 1)) ?? - (timeoutDurationMs <= 0 - ? undefined - : await retryUntil( - async () => { - 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); @@ -173,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 @@ -194,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); } @@ -222,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; @@ -257,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 3f1655c6c727..f17f37b44ae6 100644 --- a/yarn-project/validator-client/src/duties/validation_service.ts +++ b/yarn-project/validator-client/src/duties/validation_service.ts @@ -28,7 +28,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 @@ -37,7 +36,6 @@ export class ValidationService { * @returns A block proposal signing the above information (not the current implementation!!!) */ async createBlockProposal( - blockNumber: number, header: CheckpointHeader, archive: Fr, stateReference: StateReference, @@ -59,11 +57,10 @@ export class ValidationService { // For testing: corrupt the state reference to trigger state_mismatch validation failure if (options.broadcastInvalidBlockProposal) { unfreeze(stateReference.partial).noteHashTree = AppendOnlyTreeSnapshot.random(); - this.log.warn(`Creating INVALID block proposal for block ${blockNumber} at slot ${header.slotNumber.toBigInt()}`); + this.log.warn(`Creating INVALID block proposal for slot ${header.slotNumber.toBigInt()}`); } return BlockProposal.createProposalFromSigner( - blockNumber, new ConsensusPayload(header, archive, stateReference), txHashes, options.publishFullTxs ? txs : undefined, @@ -88,7 +85,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 1decb5886d56..0cb191dc74cd 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'; @@ -222,6 +223,7 @@ describe('ValidatorClient', () => { describe('attestToProposal', () => { let proposal: BlockProposal; + let blockNumber: number; let sender: PeerId; let blockBuildResult: BuildBlockResult; @@ -236,6 +238,7 @@ describe('ValidatorClient', () => { const emptyInHash = await computeInHashFromL1ToL2Messages([]); const contentCommitment = new ContentCommitment(Fr.random(), emptyInHash, Fr.random()); const blockHeader = makeL2BlockHeader(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; @@ -263,9 +266,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 = { @@ -279,7 +286,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, }; }); @@ -293,11 +300,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); }); @@ -346,7 +353,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 () => { @@ -367,6 +374,7 @@ describe('ValidatorClient', () => { expect(txProvider.getTxsForBlockProposal).toHaveBeenCalledWith( proposal, + blockNumber, expect.objectContaining({ pinnedPeer: sender }), ); }); @@ -379,6 +387,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 3bd21631c3be..eaa3bfb483be 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -184,8 +184,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 partOfCommittee = inCommittee.length > 0; 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()), }); @@ -299,7 +304,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) 'state_mismatch', 'failed_txs', 'in_hash_mismatch', - 'parent_block_does_not_match', + 'parent_block_wrong_slot', ]; if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) { @@ -329,7 +334,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 slot ${slotNumber}`, proposalInfo); this.metrics.incSuccessfulAttestations(inCommittee.length); // If the above function does not throw an error, then we can attest to the proposal @@ -378,7 +383,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) } const newProposal = await this.validationService.createBlockProposal( - blockNumber, header, archive, stateReference,