Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions yarn-project/archiver/src/archiver-misc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,13 @@ describe('Archiver misc', () => {
describe('isPruneDueAtSlot', () => {
/**
* Builds a fake L2Tips. `pending` is the L1-confirmed pending checkpoint (= `tips.checkpointed`
* in production). `proposedCheckpoint` is set to `pending + 1` to catch any implementation that
* accidentally reads the local-optimistic proposed checkpoint instead of the L1-confirmed one.
* in production).
*/
function makeTips(pending: CheckpointNumber, proven: CheckpointNumber): L2Tips {
const block = { number: BlockNumber(0), hash: '0x' };
const tip = (n: CheckpointNumber) => ({ block, checkpoint: { number: n, hash: '0x' } });
const proposedAhead = CheckpointNumber(Number(pending) + 1);
return {
proposed: block,
proposedCheckpoint: tip(proposedAhead),
checkpointed: tip(pending),
proven: tip(proven),
finalized: tip(proven),
Expand Down
14 changes: 7 additions & 7 deletions yarn-project/archiver/src/archiver-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2075,11 +2075,13 @@ describe('Archiver Sync', () => {
// Proposed checkpoint should still be set
expect(await archiverStore.blocks.getLastProposedCheckpoint()).toBeDefined();

// Proposed tip should be ahead of the checkpointed tip
// Proposed checkpoint should lead the checkpointed tip
const tips = await archiver.getL2Tips();
expect(tips.proposedCheckpoint.checkpoint.number).toEqual(CheckpointNumber(2));
const proposedCheckpointResult = await archiver.getProposedCheckpoint();
expect(proposedCheckpointResult).toBeDefined();
expect(proposedCheckpointResult!.tip.checkpoint.number).toEqual(CheckpointNumber(2));
expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1));
expect(tips.proposedCheckpoint.block.number).toBeGreaterThan(tips.checkpointed.block.number);
expect(proposedCheckpointResult!.tip.block.number).toBeGreaterThan(tips.checkpointed.block.number);
}, 15_000);

it('prunes blocks and clears stale pending checkpoint when slot ends', async () => {
Expand Down Expand Up @@ -2142,11 +2144,9 @@ describe('Archiver Sync', () => {
expect(await archiver.getBlockNumber()).toEqual(lastBlockInCheckpoint1);
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(1));

// Proposed checkpoint should be cleared, so proposed tip falls back to checkpointed tip
// Proposed checkpoint should be cleared, so no proposed checkpoint leads the checkpointed tip
expect(await archiverStore.blocks.getLastProposedCheckpoint()).toBeUndefined();
const tips = await archiver.getL2Tips();
expect(tips.proposedCheckpoint.checkpoint.number).toEqual(tips.checkpointed.checkpoint.number);
expect(tips.proposedCheckpoint.block.number).toEqual(tips.checkpointed.block.number);
expect(await archiver.getProposedCheckpoint()).toBeUndefined();
}, 15_000);
});

Expand Down
20 changes: 12 additions & 8 deletions yarn-project/archiver/src/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'
import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { Buffer32 } from '@aztec/foundation/buffer';
import { merge, pick } from '@aztec/foundation/collection';
import { merge } from '@aztec/foundation/collection';
import { Fr } from '@aztec/foundation/curves/bn254';
import { EthAddress } from '@aztec/foundation/eth-address';
import { type Logger, createLogger } from '@aztec/foundation/log';
Expand Down Expand Up @@ -435,19 +435,23 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
const tips = await this.getL2Tips();
const now = BigInt(this.dateProvider.nowInSeconds());

// Frontier block covered by a proposed (or, falling back, confirmed) checkpoint. Blocks beyond it
// have no enclosing checkpoint proposal and are the orphan-pruning candidates.
const proposedCheckpointBlockNumber = await this.stores.blocks.getProposedCheckpointL2BlockNumber();

// The proposed tip is a proposed-checkpointed block, so there are no orphan proposed blocks to prune
if (tips.proposedCheckpoint.block.number === tips.proposed.number) {
this.log.trace(
`No orphan proposed blocks to prune: proposed tip ${tips.proposed.number} is checkpointed`,
pick(tips, 'proposed', 'proposedCheckpoint'),
);
if (proposedCheckpointBlockNumber === tips.proposed.number) {
this.log.trace(`No orphan proposed blocks to prune: proposed tip ${tips.proposed.number} is checkpointed`, {
proposed: tips.proposed,
proposedCheckpointBlockNumber,
});
return;
}

// Load the blocks that are candidates for pruning (ie blocks without a proposed checkpoint covering them)
const blocksWithoutProposedCheckpoint = await this.stores.blocks.getBlocksData({
from: BlockNumber(tips.proposedCheckpoint.block.number + 1),
limit: tips.proposed.number - tips.proposedCheckpoint.block.number,
from: BlockNumber(proposedCheckpointBlockNumber + 1),
limit: tips.proposed.number - proposedCheckpointBlockNumber,
});

// Iterate through them in order, the first one with a slot that should have received a proposed checkpoint
Expand Down
5 changes: 5 additions & 0 deletions yarn-project/archiver/src/modules/data_source_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type CheckpointsQuery,
L2Block,
type L2Tips,
type ProposedCheckpoint,
type ProposedCheckpointQuery,
} from '@aztec/stdlib/block';
import {
Expand Down Expand Up @@ -288,6 +289,10 @@ export abstract class ArchiverDataSourceBase
return this.stores.blocks.getProposedCheckpointBySlot(query.slot);
}

public getProposedCheckpoint(): Promise<ProposedCheckpoint | undefined> {
return this.stores.blocks.getProposedCheckpoint();
}

public getTxEffect(txHash: TxHash): Promise<IndexedTxEffect | undefined> {
return this.stores.blocks.getTxEffect(txHash);
}
Expand Down
36 changes: 16 additions & 20 deletions yarn-project/archiver/src/store/block_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
BlockHash,
CommitteeAttestation,
EthAddress,
GENESIS_BLOCK_HEADER_HASH,
L2Block,
type ValidateCheckpointResult,
} from '@aztec/stdlib/block';
Expand All @@ -40,7 +39,6 @@ import {
makeStateForBlock,
} from '../test/mock_structs.js';
import { BlockStore } from './block_store.js';
import { L2TipsCache } from './l2_tips_cache.js';

async function addProposedBlocks(
blockStore: BlockStore,
Expand Down Expand Up @@ -2627,26 +2625,19 @@ describe('BlockStore', () => {
});
});

describe('L2TipsCache proposedCheckpoint', () => {
it('returns proposedCheckpoint equal to checkpointed when no pending exists', async () => {
describe('getProposedCheckpoint', () => {
it('returns undefined when no pending checkpoint leads the checkpointed frontier', async () => {
// Add checkpoint 1 with blocks 1-3
const checkpoint1 = makePublishedCheckpoint(
await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }),
10,
);
await blockStore.addCheckpoints([checkpoint1]);

const l2TipsCache = new L2TipsCache(blockStore, GENESIS_BLOCK_HEADER_HASH);
const tips = await l2TipsCache.getL2Tips();

// proposedCheckpoint should always be defined
expect(tips.proposedCheckpoint).toBeDefined();
// With no proposed checkpoint, it should equal the checkpointed tip
expect(tips.proposedCheckpoint!.block.number).toBe(tips.checkpointed.block.number);
expect(tips.proposedCheckpoint!.checkpoint.number).toBe(tips.checkpointed.checkpoint.number);
expect(await blockStore.getProposedCheckpoint()).toBeUndefined();
});

it('returns proposedCheckpoint ahead of checkpointed when pending is set', async () => {
it('returns the leading proposed checkpoint with a tip derived from its payload', async () => {
// Add checkpoint 1
const checkpoint1 = makePublishedCheckpoint(
await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 }),
Expand All @@ -2663,21 +2654,26 @@ describe('BlockStore', () => {
await blockStore.addProposedBlock(block2, { force: true });

// Set proposed checkpoint
const header = CheckpointHeader.empty();
await blockStore.addProposedCheckpoint({
checkpointNumber: CheckpointNumber(2),
header: CheckpointHeader.empty(),
header,
startBlock: BlockNumber(2),
blockCount: 1,
totalManaUsed: 100n,
feeAssetPriceModifier: 50n,
});

const l2TipsCache = new L2TipsCache(blockStore, GENESIS_BLOCK_HEADER_HASH);
const tips = await l2TipsCache.getL2Tips();

expect(tips.proposedCheckpoint).toBeDefined();
expect(tips.proposedCheckpoint!.block.number).toBeGreaterThan(tips.checkpointed.block.number);
expect(tips.proposedCheckpoint!.checkpoint.number).toBeGreaterThan(tips.checkpointed.checkpoint.number);
const proposedCheckpoint = await blockStore.getProposedCheckpoint();
expect(proposedCheckpoint).toBeDefined();
// Tip is derived from the payload: last block = startBlock + blockCount - 1, checkpoint number
// and header hash from the stored proposed checkpoint.
expect(proposedCheckpoint!.tip.block.number).toBe(BlockNumber(2));
expect(proposedCheckpoint!.tip.block.hash).toBe((await block2.hash()).toString());
expect(proposedCheckpoint!.tip.checkpoint.number).toBe(CheckpointNumber(2));
expect(proposedCheckpoint!.tip.checkpoint.hash).toBe(header.hash().toString());
expect(proposedCheckpoint!.data.checkpointNumber).toBe(CheckpointNumber(2));
expect(proposedCheckpoint!.data.totalManaUsed).toBe(100n);
});
});

Expand Down
70 changes: 44 additions & 26 deletions yarn-project/archiver/src/store/block_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
L2Block,
type L2TipId,
type L2Tips,
type ProposedCheckpoint,
type ValidateCheckpointResult,
deserializeValidateCheckpointResult,
serializeValidateCheckpointResult,
Expand Down Expand Up @@ -864,6 +865,38 @@ export class BlockStore {
return undefined;
}

/**
* Returns the latest proposed checkpoint that leads the checkpointed frontier, paired with its
* derived chain tip, in a single read-only transaction so the tip and payload are a coherent
* snapshot. Returns undefined when no proposed checkpoint exists beyond the latest confirmed
* checkpoint. The tip's block number is `startBlock + blockCount - 1` and its block hash is read
* from the block store; the checkpoint hash is derived from the stored header.
*/
async getProposedCheckpoint(): Promise<ProposedCheckpoint | undefined> {
return await this.db.transactionAsync(async () => {
const [entry] = await toArray(this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }));
if (entry === undefined) {
return undefined;
}
const latestCheckpointNumber = await this.getLatestCheckpointNumber();
if (entry[0] <= latestCheckpointNumber) {
return undefined;
}
const data = this.convertToProposedCheckpointData(entry[1]);
const blockNumber = BlockNumber(data.startBlock + data.blockCount - 1);
const blockStorage = await this.#blocks.getAsync(blockNumber);
if (!blockStorage) {
throw new BlockNotFoundError(blockNumber);
}
const blockHash = BlockHash.fromBuffer(blockStorage.blockHash).toString();
const tip: L2TipId = {
block: { number: blockNumber, hash: blockHash },
checkpoint: { number: data.checkpointNumber, hash: data.header.hash().toString() },
};
return { tip, data };
});
}

/**
* Evicts all pending checkpoints with checkpoint number >= fromNumber.
* Used for divergent-mined-checkpoint cleanup: when L1 mines checkpoint N with a different archive,
Expand Down Expand Up @@ -1167,14 +1200,13 @@ export class BlockStore {
}

/**
* Resolves all five L2 chain tips (proposed, proposedCheckpoint, checkpointed, proven, finalized)
* in a single read-only transaction so the snapshot is internally consistent. Each underlying
* record is read at most once: latest block, latest confirmed checkpoint, and latest pending
* checkpoint are each loaded directly (no separate "find the number, then look up data" hop),
* the proven/finalized checkpoint singletons are read once and their storage entries are
* reused if they coincide with the latest checkpoint, and per-tip block hashes are deduped
* when two tips land on the same block (e.g. finalized == proven, or proposedCheckpoint falls
* back to checkpointed when no pending checkpoint exists).
* Resolves all four L2 chain tips (proposed, checkpointed, proven, finalized) in a single
* read-only transaction so the snapshot is internally consistent. Each underlying record is
* read at most once: latest block and latest confirmed checkpoint are loaded directly (no
* separate "find the number, then look up data" hop), the proven/finalized checkpoint
* singletons are read once and their storage entries are reused if they coincide with the
* latest checkpoint, and per-tip block hashes are deduped when two tips land on the same block
* (e.g. finalized == proven).
*
* The result is guaranteed to satisfy `finalized <= proven <= checkpointed <= proposed` (by
* block number). Genesis is represented by `(INITIAL_L2_BLOCK_NUM - 1)` and the supplied
Expand All @@ -1197,9 +1229,6 @@ export class BlockStore {

// Load latest block and checkpoint entries
const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 }));
const [proposedCheckpointEntry] = await toArray(
this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }),
);
const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 }));
const latestCheckpointNumber = latestCheckpointEntry
? CheckpointNumber(latestCheckpointEntry[0])
Expand Down Expand Up @@ -1285,14 +1314,6 @@ export class BlockStore {
const provenTip = await buildTipFromCheckpoint(provenCheckpoint);
const finalizedTip = await buildTipFromCheckpoint(finalizedCheckpoint);

// Proposed checkpoint falls back to the checkpoint tip if it's not set. And if local storage is
// inconsistent and the proposed checkpoint is behind the checkpointed tip, we patch that and
// report the checkpointed tip as the proposed checkpoint to maintain the invariant.
const proposedCheckpointTip =
proposedCheckpointEntry === undefined || proposedCheckpointEntry[0] <= latestCheckpointNumber
? checkpointedTip
: await buildTipFromCheckpoint(proposedCheckpointEntry[1]);

// A checkpointed block past the latest stored block would mean a checkpoint
// references blocks that aren't in blocks.
if (proposedBlockId.number < checkpointedTip.block.number) {
Expand All @@ -1304,29 +1325,26 @@ export class BlockStore {
// Assert that checkpoint numbers are increasing
if (
finalizedTip.checkpoint.number > provenTip.checkpoint.number ||
provenTip.checkpoint.number > checkpointedTip.checkpoint.number ||
checkpointedTip.checkpoint.number > proposedCheckpointTip.checkpoint.number
provenTip.checkpoint.number > checkpointedTip.checkpoint.number
) {
throw new Error(
`Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number} proposed=${proposedCheckpointTip.checkpoint.number}`,
`Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number}`,
);
}

// Assert block numbers are increasing
if (
finalizedTip.block.number > provenTip.block.number ||
provenTip.block.number > checkpointedTip.block.number ||
checkpointedTip.block.number > proposedCheckpointTip.block.number ||
proposedCheckpointTip.block.number > proposedBlockId.number
checkpointedTip.block.number > proposedBlockId.number
) {
throw new Error(
`Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposedCheckpoint=${proposedCheckpointTip.block.number} proposed=${proposedBlockId.number}`,
`Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposed=${proposedBlockId.number}`,
);
}

return {
proposed: proposedBlockId,
proposedCheckpoint: proposedCheckpointTip,
checkpointed: checkpointedTip,
proven: provenTip,
finalized: finalizedTip,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource {
checkpointed: tip,
proven: tip,
finalized: tip,
proposedCheckpoint: tip,
});
}
}
Loading