Skip to content

Commit e122458

Browse files
authored
fix: Remove block number from p2p proposals and attestations (#17886)
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. Fixes A-128
2 parents 4efdd0a + de6b4d4 commit e122458

39 files changed

+754
-239
lines changed

yarn-project/archiver/src/archiver/archiver.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ describe('Archiver', () => {
106106
let blobSinkClient: MockProxy<BlobSinkClientInterface>;
107107
let epochCache: MockProxy<EpochCache>;
108108
let archiverStore: ArchiverDataStore;
109-
let l1Constants: L1RollupConstants & { l1StartBlockHash: Buffer32 };
109+
let l1Constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr };
110110
let now: number;
111111

112112
let mockRollupRead: MockProxy<MockRollupContractRead>;
@@ -167,6 +167,7 @@ describe('Archiver', () => {
167167
slotDuration: 24,
168168
ethereumSlotDuration: 12,
169169
proofSubmissionEpochs: 1,
170+
genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT),
170171
};
171172

172173
archiver = new Archiver(

yarn-project/archiver/src/archiver/archiver.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
150150
private readonly blobSinkClient: BlobSinkClientInterface,
151151
private readonly epochCache: EpochCache,
152152
private readonly instrumentation: ArchiverInstrumentation,
153-
private readonly l1constants: L1RollupConstants & { l1StartBlockHash: Buffer32 },
153+
private readonly l1constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr },
154154
private readonly log: Logger = createLogger('archiver'),
155155
) {
156156
super();
@@ -184,10 +184,11 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
184184

185185
const rollup = new RollupContract(publicClient, config.l1Contracts.rollupAddress);
186186

187-
const [l1StartBlock, l1GenesisTime, proofSubmissionEpochs] = await Promise.all([
187+
const [l1StartBlock, l1GenesisTime, proofSubmissionEpochs, genesisArchiveRoot] = await Promise.all([
188188
rollup.getL1StartBlock(),
189189
rollup.getL1GenesisTime(),
190190
rollup.getProofSubmissionEpochs(),
191+
rollup.getGenesisArchiveTreeRoot(),
191192
] as const);
192193

193194
const l1StartBlockHash = await publicClient
@@ -204,6 +205,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
204205
slotDuration,
205206
ethereumSlotDuration,
206207
proofSubmissionEpochs: Number(proofSubmissionEpochs),
208+
genesisArchiveRoot: Fr.fromHexString(genesisArchiveRoot),
207209
};
208210

209211
const opts = merge({ pollingIntervalMs: 10_000, batchSize: 100 }, mapArchiverConfig(config));
@@ -977,6 +979,10 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
977979
return Promise.resolve(this.l1constants);
978980
}
979981

982+
public getGenesisValues(): Promise<{ genesisArchiveRoot: Fr }> {
983+
return Promise.resolve({ genesisArchiveRoot: this.l1constants.genesisArchiveRoot });
984+
}
985+
980986
public getRollupAddress(): Promise<EthAddress> {
981987
return Promise.resolve(this.l1Addresses.rollupAddress);
982988
}
@@ -1097,6 +1103,22 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
10971103
return limitWithProven === 0 ? [] : await this.store.getPublishedBlocks(from, limitWithProven);
10981104
}
10991105

1106+
public getPublishedBlockByHash(blockHash: Fr): Promise<PublishedL2Block | undefined> {
1107+
return this.store.getPublishedBlockByHash(blockHash);
1108+
}
1109+
1110+
public getPublishedBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined> {
1111+
return this.store.getPublishedBlockByArchive(archive);
1112+
}
1113+
1114+
public getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined> {
1115+
return this.store.getBlockHeaderByHash(blockHash);
1116+
}
1117+
1118+
public getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
1119+
return this.store.getBlockHeaderByArchive(archive);
1120+
}
1121+
11001122
/**
11011123
* Gets an l2 block.
11021124
* @param number - The block number to return.
@@ -1592,9 +1614,21 @@ export class ArchiverStoreHelper
15921614
getPublishedBlock(number: number): Promise<PublishedL2Block | undefined> {
15931615
return this.store.getPublishedBlock(number);
15941616
}
1617+
getPublishedBlockByHash(blockHash: Fr): Promise<PublishedL2Block | undefined> {
1618+
return this.store.getPublishedBlockByHash(blockHash);
1619+
}
1620+
getPublishedBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined> {
1621+
return this.store.getPublishedBlockByArchive(archive);
1622+
}
15951623
getBlockHeaders(from: number, limit: number): Promise<BlockHeader[]> {
15961624
return this.store.getBlockHeaders(from, limit);
15971625
}
1626+
getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined> {
1627+
return this.store.getBlockHeaderByHash(blockHash);
1628+
}
1629+
getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
1630+
return this.store.getBlockHeaderByArchive(archive);
1631+
}
15981632
getTxEffect(txHash: TxHash): Promise<IndexedTxEffect | undefined> {
15991633
return this.store.getTxEffect(txHash);
16001634
}

yarn-project/archiver/src/archiver/archiver_store.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ export interface ArchiverDataStore {
6161
*/
6262
getPublishedBlock(number: number): Promise<PublishedL2Block | undefined>;
6363

64+
/**
65+
* Returns the block for the given hash, or undefined if not exists.
66+
* @param blockHash - The block hash to return.
67+
*/
68+
getPublishedBlockByHash(blockHash: Fr): Promise<PublishedL2Block | undefined>;
69+
70+
/**
71+
* Returns the block for the given archive root, or undefined if not exists.
72+
* @param archive - The archive root to return.
73+
*/
74+
getPublishedBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined>;
75+
6476
/**
6577
* Gets up to `limit` amount of published L2 blocks starting from `from`.
6678
* @param from - Number of the first block to return (inclusive).
@@ -77,6 +89,18 @@ export interface ArchiverDataStore {
7789
*/
7890
getBlockHeaders(from: number, limit: number): Promise<BlockHeader[]>;
7991

92+
/**
93+
* Returns the block header for the given hash, or undefined if not exists.
94+
* @param blockHash - The block hash to return.
95+
*/
96+
getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined>;
97+
98+
/**
99+
* Returns the block header for the given archive root, or undefined if not exists.
100+
* @param archive - The archive root to return.
101+
*/
102+
getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined>;
103+
80104
/**
81105
* Gets a tx effect.
82106
* @param txHash - The hash of the tx corresponding to the tx effect.

yarn-project/archiver/src/archiver/archiver_store_test_suite.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ export function describeArchiverDataStore(
142142
await store.addBlocks(blocks);
143143
await expect(store.unwindBlocks(5, 1)).rejects.toThrow(/can only unwind blocks from the tip/i);
144144
});
145+
146+
it('unwound blocks and headers cannot be retrieved by hash or archive', async () => {
147+
await store.addBlocks(blocks);
148+
const lastBlock = blocks[blocks.length - 1];
149+
const blockHash = await lastBlock.block.hash();
150+
const archive = lastBlock.block.archive.root;
151+
152+
// Verify block and header exist before unwinding
153+
expect(await store.getPublishedBlockByHash(blockHash)).toBeDefined();
154+
expect(await store.getPublishedBlockByArchive(archive)).toBeDefined();
155+
expect(await store.getBlockHeaderByHash(blockHash)).toBeDefined();
156+
expect(await store.getBlockHeaderByArchive(archive)).toBeDefined();
157+
158+
// Unwind the block
159+
await store.unwindBlocks(lastBlock.block.number, 1);
160+
161+
// Verify neither block nor header can be retrieved after unwinding
162+
expect(await store.getPublishedBlockByHash(blockHash)).toBeUndefined();
163+
expect(await store.getPublishedBlockByArchive(archive)).toBeUndefined();
164+
expect(await store.getBlockHeaderByHash(blockHash)).toBeUndefined();
165+
expect(await store.getBlockHeaderByArchive(archive)).toBeUndefined();
166+
});
145167
});
146168

147169
describe('getBlocks', () => {
@@ -179,6 +201,86 @@ export function describeArchiverDataStore(
179201
});
180202
});
181203

204+
describe('getPublishedBlockByHash', () => {
205+
beforeEach(async () => {
206+
await store.addBlocks(blocks);
207+
});
208+
209+
it('retrieves a block by its hash', async () => {
210+
const expectedBlock = blocks[5];
211+
const blockHash = await expectedBlock.block.hash();
212+
const retrievedBlock = await store.getPublishedBlockByHash(blockHash);
213+
214+
expect(retrievedBlock).toBeDefined();
215+
expectBlocksEqual([retrievedBlock!], [expectedBlock]);
216+
});
217+
218+
it('returns undefined for non-existent block hash', async () => {
219+
const nonExistentHash = Fr.random();
220+
await expect(store.getPublishedBlockByHash(nonExistentHash)).resolves.toBeUndefined();
221+
});
222+
});
223+
224+
describe('getPublishedBlockByArchive', () => {
225+
beforeEach(async () => {
226+
await store.addBlocks(blocks);
227+
});
228+
229+
it('retrieves a block by its archive root', async () => {
230+
const expectedBlock = blocks[3];
231+
const archive = expectedBlock.block.archive.root;
232+
const retrievedBlock = await store.getPublishedBlockByArchive(archive);
233+
234+
expect(retrievedBlock).toBeDefined();
235+
expectBlocksEqual([retrievedBlock!], [expectedBlock]);
236+
});
237+
238+
it('returns undefined for non-existent archive root', async () => {
239+
const nonExistentArchive = Fr.random();
240+
await expect(store.getPublishedBlockByArchive(nonExistentArchive)).resolves.toBeUndefined();
241+
});
242+
});
243+
244+
describe('getBlockHeaderByHash', () => {
245+
beforeEach(async () => {
246+
await store.addBlocks(blocks);
247+
});
248+
249+
it('retrieves a block header by its hash', async () => {
250+
const expectedBlock = blocks[7];
251+
const blockHash = await expectedBlock.block.hash();
252+
const retrievedHeader = await store.getBlockHeaderByHash(blockHash);
253+
254+
expect(retrievedHeader).toBeDefined();
255+
expect(retrievedHeader!.equals(expectedBlock.block.getBlockHeader())).toBe(true);
256+
});
257+
258+
it('returns undefined for non-existent block hash', async () => {
259+
const nonExistentHash = Fr.random();
260+
await expect(store.getBlockHeaderByHash(nonExistentHash)).resolves.toBeUndefined();
261+
});
262+
});
263+
264+
describe('getBlockHeaderByArchive', () => {
265+
beforeEach(async () => {
266+
await store.addBlocks(blocks);
267+
});
268+
269+
it('retrieves a block header by its archive root', async () => {
270+
const expectedBlock = blocks[2];
271+
const archive = expectedBlock.block.archive.root;
272+
const retrievedHeader = await store.getBlockHeaderByArchive(archive);
273+
274+
expect(retrievedHeader).toBeDefined();
275+
expect(retrievedHeader!.equals(expectedBlock.block.getBlockHeader())).toBe(true);
276+
});
277+
278+
it('returns undefined for non-existent archive root', async () => {
279+
const nonExistentArchive = Fr.random();
280+
await expect(store.getBlockHeaderByArchive(nonExistentArchive)).resolves.toBeUndefined();
281+
});
282+
});
283+
182284
describe('getSyncedL2BlockNumber', () => {
183285
it('returns the block number before INITIAL_L2_BLOCK_NUM if no blocks have been added', async () => {
184286
await expect(store.getSynchedL2BlockNumber()).resolves.toEqual(INITIAL_L2_BLOCK_NUM - 1);

yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,21 @@ export class BlockStore {
6666
/** Index mapping a contract's address (as a string) to its location in a block */
6767
#contractIndex: AztecAsyncMap<string, BlockIndexValue>;
6868

69+
/** Index mapping block hash to block number */
70+
#blockHashIndex: AztecAsyncMap<string, number>;
71+
72+
/** Index mapping block archive to block number */
73+
#blockArchiveIndex: AztecAsyncMap<string, number>;
74+
6975
#log = createLogger('archiver:block_store');
7076

7177
constructor(private db: AztecAsyncKVStore) {
7278
this.#blocks = db.openMap('archiver_blocks');
7379
this.#blockTxs = db.openMap('archiver_block_txs');
7480
this.#txEffects = db.openMap('archiver_tx_effects');
7581
this.#contractIndex = db.openMap('archiver_contract_index');
82+
this.#blockHashIndex = db.openMap('archiver_block_hash_index');
83+
this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
7684
this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
7785
this.#lastProvenL2Block = db.openSingleton('archiver_last_proven_l2_block');
7886
this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
@@ -132,6 +140,10 @@ export class BlockStore {
132140
blockHash.toString(),
133141
Buffer.concat(block.block.body.txEffects.map(tx => tx.txHash.toBuffer())),
134142
);
143+
144+
// Update indices for block hash and archive
145+
await this.#blockHashIndex.set(blockHash.toString(), block.block.number);
146+
await this.#blockArchiveIndex.set(block.block.archive.root.toString(), block.block.number);
135147
}
136148

137149
await this.#lastSynchedL1Block.set(blocks[blocks.length - 1].l1.blockNumber);
@@ -170,6 +182,11 @@ export class BlockStore {
170182
await Promise.all(block.block.body.txEffects.map(tx => this.#txEffects.delete(tx.txHash.toString())));
171183
const blockHash = (await block.block.hash()).toString();
172184
await this.#blockTxs.delete(blockHash);
185+
186+
// Clean up indices
187+
await this.#blockHashIndex.delete(blockHash);
188+
await this.#blockArchiveIndex.delete(block.block.archive.root.toString());
189+
173190
this.#log.debug(`Unwound block ${blockNumber} ${blockHash}`);
174191
}
175192

@@ -205,6 +222,66 @@ export class BlockStore {
205222
return this.getBlockFromBlockStorage(blockNumber, blockStorage);
206223
}
207224

225+
/**
226+
* Gets an L2 block by its hash.
227+
* @param blockHash - The hash of the block to return.
228+
* @returns The requested L2 block.
229+
*/
230+
async getBlockByHash(blockHash: L2BlockHash): Promise<PublishedL2Block | undefined> {
231+
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
232+
if (blockNumber === undefined) {
233+
return undefined;
234+
}
235+
return this.getBlock(blockNumber);
236+
}
237+
238+
/**
239+
* Gets an L2 block by its archive root.
240+
* @param archive - The archive root of the block to return.
241+
* @returns The requested L2 block.
242+
*/
243+
async getBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined> {
244+
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
245+
if (blockNumber === undefined) {
246+
return undefined;
247+
}
248+
return this.getBlock(blockNumber);
249+
}
250+
251+
/**
252+
* Gets a block header by its hash.
253+
* @param blockHash - The hash of the block to return.
254+
* @returns The requested block header.
255+
*/
256+
async getBlockHeaderByHash(blockHash: L2BlockHash): Promise<BlockHeader | undefined> {
257+
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
258+
if (blockNumber === undefined) {
259+
return undefined;
260+
}
261+
const blockStorage = await this.#blocks.getAsync(blockNumber);
262+
if (!blockStorage || !blockStorage.header) {
263+
return undefined;
264+
}
265+
return L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
266+
}
267+
268+
/**
269+
* Gets a block header by its archive root.
270+
* @param archive - The archive root of the block to return.
271+
* @returns The requested block header.
272+
*/
273+
async getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
274+
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
275+
if (blockNumber === undefined) {
276+
return undefined;
277+
}
278+
const blockStorage = await this.#blocks.getAsync(blockNumber);
279+
if (!blockStorage || !blockStorage.header) {
280+
return undefined;
281+
}
282+
return L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
283+
}
284+
208285
/**
209286
* Gets the headers for a sequence of L2 blocks.
210287
* @param start - Number of the first block to return (inclusive).

0 commit comments

Comments
 (0)