Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,23 @@ import { type CheckpointTopTreeData, TopTreeOrchestrator } from '../orchestrator
// before the (two-input) block root, whereas one- or two-tx blocks feed the block root directly, so a
// dedicated three-tx scenario is what regenerates the tx-merge sample. The samples for the variants
// that thread a start sponge from a previous block are taken from mid-checkpoint blocks, so those
// scenarios need a per-block message distribution. Every scenario also produces the root rollup.
// scenarios need a per-block message distribution, and they declare which block they mean: blocks are
// proven concurrently, so the order the inputs were captured in does not identify them. Every scenario
// also produces the root rollup.
const describeOrSkip = isGenerateTestDataEnabled() ? describe : describe.skip;

describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
let context: TestContext;
let log: Logger;

/**
* How many L1-to-L2 messages a block inherits from earlier blocks of its checkpoint (its start sponge) and how
* many its own bundle inserts. A scenario that runs a block-root circuit once per block identifies the run it
* means by this shape, which is a property of the block itself, rather than by the order the inputs happened to
* be captured in.
*/
type MessageShape = { inherited: number; bundle: number };

interface Scenario {
numCheckpoints: number;
numBlocksPerCheckpoint: number;
Expand All @@ -55,6 +65,8 @@ describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
l1ToL2MessagesPerBlock?: Fr[][];
/** Circuits whose sample inputs this scenario is responsible for regenerating. */
dump: CircuitName[];
/** The block whose inputs each dumped block-root sample must be taken from. */
sampleFrom?: Partial<Record<CircuitName, MessageShape>>;
}

// `makeCheckpoint` puts the scenario's whole message list into the first block, so the most a
Expand All @@ -75,6 +87,9 @@ describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
numTxsPerBlock: 1,
numL1ToL2Messages: withMessages,
dump: ['rollup-block-root-single-tx', 'rollup-block-merge', 'rollup-checkpoint-root'],
// All the messages go to the first block, so the single-tx sample is the block that carries the bundle; the
// two blocks after it insert nothing and only inherit the sponge.
sampleFrom: { 'rollup-block-root-single-tx': { inherited: 0, bundle: withMessages } },
},
// Messages split across both blocks so the block-root sample is taken from a mid-checkpoint block with a
// non-empty bundle, exercising the per-block sponge continuity asserts in the circuit.
Expand All @@ -85,6 +100,9 @@ describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
numL1ToL2Messages: 0, // Overridden by l1ToL2MessagesPerBlock.
l1ToL2MessagesPerBlock: [times(2, i => new Fr(0xb00 + i)), times(3, i => new Fr(0xc00 + i))],
dump: ['rollup-block-root'],
// The second block: it inherits the two messages the first block inserted and inserts three of its own, so
// its start sponge is a continuation rather than the initial empty one.
sampleFrom: { 'rollup-block-root': { inherited: 2, bundle: 3 } },
},
// Three txs in a block force a tx-merge to pair the base proofs down to the two the block root
// takes; one- or two-tx blocks feed the block root directly and never exercise tx-merge.
Expand All @@ -104,6 +122,7 @@ describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
numL1ToL2Messages: 0, // Overridden by l1ToL2MessagesPerBlock.
l1ToL2MessagesPerBlock: [times(2, i => new Fr(0x900 + i)), times(3, i => new Fr(0xa00 + i))],
dump: ['rollup-block-root-no-txs'],
sampleFrom: { 'rollup-block-root-no-txs': { inherited: 2, bundle: 3 } },
},
// The checkpoint-merge only appears with three checkpoints. Independently-built checkpoints do
// not carry the inbox message state forward, so this scenario runs with no L1-to-L2 messages and
Expand All @@ -117,6 +136,51 @@ describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
},
];

/** The message shape of a captured input, or undefined for circuits that take no message bundle. */
const messageShapeOf = (captured: unknown): MessageShape | undefined => {
const { inputs } = (captured ?? {}) as {
inputs?: { message_bundle?: { num_msgs?: string }; start_msg_sponge?: { num_absorbed?: string } };
};
Comment on lines +141 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 messageShapeOf treats captured as the expected object with an as cast. The repo rule says to avoid as Type casts and use type guards. Add a guard before reading inputs; this also keeps malformed captures from reaching BigInt unchecked.

Context Used: yarn-project/CLAUDE.md (source)

Fix in Codex Fix in Claude Code

const bundle = inputs?.message_bundle?.num_msgs;
const inherited = inputs?.start_msg_sponge?.num_absorbed;
return bundle === undefined || inherited === undefined
? undefined
: { inherited: Number(BigInt(inherited)), bundle: Number(BigInt(bundle)) };
};

/**
* Picks the one captured input a scenario means to commit for `circuitName`. A circuit the scenario runs once has
* a single candidate; one it runs per block is identified by the message shape the scenario declared. Nothing
* captured, several runs with no declared shape, or a declared shape matching no run or more than one all throw:
* committing whichever input happened to be captured first would silently pin the wrong block.
*/
const selectSample = (circuitName: CircuitName, wanted: MessageShape | undefined): unknown => {
const captured = getTestData(circuitName) ?? [];
const shapes = () => captured.map(entry => JSON.stringify(messageShapeOf(entry) ?? 'no bundle')).join(', ');
if (captured.length === 0) {
throw new Error(`No test data captured for ${circuitName}; scenario does not exercise it.`);
}
if (wanted === undefined) {
if (captured.length > 1) {
throw new Error(
`${circuitName} ran ${captured.length} times (${shapes()}); declare in sampleFrom which run to commit.`,
);
}
return captured[0];
}
const matching = captured.filter(entry => {
const shape = messageShapeOf(entry);
return shape?.inherited === wanted.inherited && shape.bundle === wanted.bundle;
});
if (matching.length !== 1) {
throw new Error(
`Expected exactly one ${circuitName} run inheriting ${wanted.inherited} messages and inserting ` +
`${wanted.bundle}, found ${matching.length} of ${captured.length} runs (${shapes()}).`,
);
}
return matching[0];
};
Comment on lines +157 to +182

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 selectSample contains the new matching and error behavior, but its only caller is inside the suite skipped unless AZTEC_GENERATE_TEST_DATA=1. No normal CI job enables that flag or tests this helper. Add an always-run unit test for one match, no match, duplicate matches, and missing shapes.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code


beforeEach(async () => {
log = createLogger('prover-client:test:regenerate-rollup-sample-inputs');
context = await TestContext.new(log, { proverCount: 1 });
Expand All @@ -135,6 +199,7 @@ describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
numL1ToL2Messages,
l1ToL2MessagesPerBlock,
dump,
sampleFrom,
}) => {
const makeProcessedTxOpts = (_: unknown, txIndex: number) => ({ privateOnly: txIndex % 2 === 0 });
const checkpoints = await timesAsync(numCheckpoints, () =>
Expand Down Expand Up @@ -203,12 +268,9 @@ describeOrSkip('prover/regenerate-rollup-sample-inputs', () => {
}

for (const circuitName of dump) {
const data = getTestData(circuitName);
if (!data || data.length === 0) {
throw new Error(`No test data captured for ${circuitName}; scenario does not exercise it.`);
}
updateProtocolCircuitSampleInputs(circuitName, TOML.stringify(data[0] as any));
log.info(`Regenerated sample inputs for ${circuitName}`);
const sample = selectSample(circuitName, sampleFrom?.[circuitName]);
updateProtocolCircuitSampleInputs(circuitName, TOML.stringify(sample as any));
log.info(`Regenerated sample inputs for ${circuitName}`, messageShapeOf(sample));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 messageShapeOf returns undefined for circuits with no message bundle, so this call sometimes passes undefined as its second argument. The repo rule requires every log call to pass a structured context object. Wrap the optional value in an object, such as { messageShape: messageShapeOf(sample) }.

Context Used: yarn-project/AGENTS.md (source)

Fix in Codex Fix in Claude Code

}
} finally {
await Promise.all(subTrees.map(s => s.stop()));
Expand Down
Loading