From 8cf1813e314d43ae9e91197cf9e5570a8230ec8a Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 6 Mar 2026 12:56:49 -0300 Subject: [PATCH 1/2] fix(tx): reject txs with invalid setup when unprotecting When unprotecting txs, we were not running the check for allowed public setup functions, which was skipped in the reqresp entrypoint. This commit now tracks whether public setup is allowed or not for a tx in the tx metadata, and uses it to drop the tx when it becomes unprotected. An alternative approach would have been to store the entire public setup calls in the tx metadata, but this means a smaller memory footprint. --- yarn-project/p2p/src/client/factory.ts | 51 ++++++--- .../src/mem_pools/tx_pool_v2/interfaces.ts | 2 + .../src/mem_pools/tx_pool_v2/tx_metadata.ts | 12 +- .../tx_pool_v2/tx_pool_v2.compat.test.ts | 6 + .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 106 ++++++++++++++++++ .../tx_pool_v2/tx_pool_v2_bench.test.ts | 3 + .../mem_pools/tx_pool_v2/tx_pool_v2_impl.ts | 16 ++- .../tx_validator/factory.test.ts | 3 +- .../msg_validators/tx_validator/factory.ts | 22 +++- .../tx_validator/phases_validator.ts | 30 +++++ 10 files changed, 227 insertions(+), 24 deletions(-) diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index da52e261ceb9..aa154872dada 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -17,7 +17,11 @@ import { AttestationPool, type AttestationPoolApi } from '../mem_pools/attestati import type { MemPools } from '../mem_pools/interface.js'; import type { TxPoolV2 } from '../mem_pools/tx_pool_v2/interfaces.js'; import { AztecKVTxPoolV2 } from '../mem_pools/tx_pool_v2/tx_pool_v2.js'; -import { createTxValidatorForTransactionsEnteringPendingTxPool } from '../msg_validators/index.js'; +import { + createCheckAllowedSetupCalls, + createTxValidatorForTransactionsEnteringPendingTxPool, + getDefaultAllowedSetupFunctions, +} from '../msg_validators/index.js'; import { DummyP2PService } from '../services/dummy_service.js'; import { LibP2PService } from '../services/index.js'; import { createFileStoreTxSources } from '../services/tx_collection/file_store_tx_source.js'; @@ -75,6 +79,33 @@ export async function createP2PClient( const rollupAddress = inputConfig.l1Contracts.rollupAddress.toString().toLowerCase().replace(/^0x/, ''); const txFileStoreBasePath = `aztec-${inputConfig.l1ChainId}-${inputConfig.rollupVersion}-0x${rollupAddress}`; + const allowedInSetup = [ + ...(await getDefaultAllowedSetupFunctions()), + ...(inputConfig.txPublicSetupAllowListExtend ?? []), + ]; + const checkAllowedSetupCalls = createCheckAllowedSetupCalls( + archiver, + allowedInSetup, + () => epochCache.getEpochAndSlotInNextL1Slot().ts, + ); + + const createTxValidator = async () => { + // We accept transactions if they are not expired by the next slot and block number (checked based on the ExpirationTimestamp field) + const currentBlockNumber = await archiver.getBlockNumber(); + const { ts: nextSlotTimestamp } = epochCache.getEpochAndSlotInNextL1Slot(); + const l1Constants = await archiver.getL1Constants(); + return createTxValidatorForTransactionsEnteringPendingTxPool( + worldStateSynchronizer, + nextSlotTimestamp, + BlockNumber(currentBlockNumber + 1), + { + rollupManaLimit: l1Constants.rollupManaLimit, + maxBlockL2Gas: config.validateMaxL2BlockGas, + maxBlockDAGas: config.validateMaxDABlockGas, + }, + ); + }; + const txPool = deps.txPool ?? new AztecKVTxPoolV2( @@ -83,22 +114,8 @@ export async function createP2PClient( { l2BlockSource: archiver, worldStateSynchronizer, - createTxValidator: async () => { - // We accept transactions if they are not expired by the next slot and block number (checked based on the ExpirationTimestamp field) - const currentBlockNumber = await archiver.getBlockNumber(); - const { ts: nextSlotTimestamp } = epochCache.getEpochAndSlotInNextL1Slot(); - const l1Constants = await archiver.getL1Constants(); - return createTxValidatorForTransactionsEnteringPendingTxPool( - worldStateSynchronizer, - nextSlotTimestamp, - BlockNumber(currentBlockNumber + 1), - { - rollupManaLimit: l1Constants.rollupManaLimit, - maxBlockL2Gas: config.validateMaxL2BlockGas, - maxBlockDAGas: config.validateMaxDABlockGas, - }, - ); - }, + checkAllowedSetupCalls, + createTxValidator, }, telemetry, { diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts index a78a70482024..cee420a796b4 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts @@ -72,6 +72,8 @@ export type TxPoolV2Dependencies = { worldStateSynchronizer: WorldStateSynchronizer; /** Factory that creates a validator for re-validating pool transactions using metadata */ createTxValidator: () => Promise>; + /** Checks whether a tx's setup-phase calls are on the allow list. Precomputed at receipt time. */ + checkAllowedSetupCalls: (tx: Tx) => Promise; }; /** diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts index 1d2f434fb2fb..44ad665446c2 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_metadata.ts @@ -67,6 +67,9 @@ export type TxMetaData = { /** Timestamp by which the transaction must be included (for expiration checks) */ readonly expirationTimestamp: bigint; + /** Whether the tx's setup-phase calls pass the allow list check. Computed at receipt time. */ + readonly allowedSetupCalls: boolean; + /** Validator-compatible data, providing the same access patterns as Tx.data */ readonly data: TxMetaValidationData; @@ -84,8 +87,12 @@ export type TxState = 'pending' | 'protected' | 'mined' | 'deleted'; * Builds TxMetaData from a full Tx object. * Extracts all relevant fields for efficient in-memory storage and querying. * Fr values are captured in closures for zero-cost re-validation. + * + * @param allowedSetupCalls - Whether the tx's setup-phase calls pass the allow list. + * For gossip/RPC txs this is always `true` (already validated by PhasesTxValidator). + * For req/resp txs this should be computed by the caller using the phases validator. */ -export async function buildTxMetaData(tx: Tx): Promise { +export async function buildTxMetaData(tx: Tx, allowedSetupCalls: boolean = true): Promise { const txHashObj = tx.getTxHash(); const txHash = txHashObj.toString(); const txHashBigInt = txHashObj.toBigInt(); @@ -112,6 +119,7 @@ export async function buildTxMetaData(tx: Tx): Promise { feeLimit, nullifiers, expirationTimestamp, + allowedSetupCalls, receivedAt: 0, estimatedSizeBytes, data: { @@ -304,6 +312,7 @@ export function stubTxMetaData( nullifiers?: string[]; expirationTimestamp?: bigint; anchorBlockHeaderHash?: string; + allowedSetupCalls?: boolean; } = {}, ): TxMetaData { const txHashBigInt = Fr.fromHexString(txHash).toBigInt(); @@ -320,6 +329,7 @@ export function stubTxMetaData( feeLimit: overrides.feeLimit ?? 100n, nullifiers: overrides.nullifiers ?? [`0x${normalizedTxHash.slice(2)}null1`], expirationTimestamp, + allowedSetupCalls: overrides.allowedSetupCalls ?? true, receivedAt: 0, estimatedSizeBytes: 0, data: stubTxMetaValidationData({ expirationTimestamp }), diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts index 7f0e52312729..397541922b43 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.compat.test.ts @@ -87,6 +87,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool.start(); }); @@ -325,6 +326,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { archivedTxLimit: 2 }, @@ -366,6 +368,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { maxPendingTxCount: 3 }, @@ -422,6 +425,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { maxPendingTxCount: 10 }, @@ -465,6 +469,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { maxPendingTxCount: 10 }, @@ -637,6 +642,7 @@ describe('TxPoolV2 Compatibility Tests', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { maxPendingTxCount: 0 }, diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts index f778f6bb3f14..dd9d9b824d24 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts @@ -27,7 +27,9 @@ import { BlockHeader, GlobalVariables, type Tx, TxEffect, TxHash, type TxValidat import { type MockProxy, mock } from 'jest-mock-extended'; +import { AggregateTxValidator } from '../../msg_validators/tx_validator/aggregate_tx_validator.js'; import { GasLimitsValidator } from '../../msg_validators/tx_validator/gas_validator.js'; +import { AllowedSetupCallsMetaValidator } from '../../msg_validators/tx_validator/phases_validator.js'; import type { TxMetaData } from './tx_metadata.js'; import { AztecKVTxPoolV2 } from './tx_pool_v2.js'; @@ -133,6 +135,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool.start(); @@ -540,6 +543,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(rejectingValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await rejectingPool.start(); }); @@ -655,6 +659,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(new GasLimitsValidator()), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await gasPool.start(); }); @@ -1290,6 +1295,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await poolWithValidator.start(); }); @@ -1998,6 +2004,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await poolWithValidator.start(); }); @@ -2131,6 +2138,85 @@ describe('TxPoolV2', () => { expect(await poolWithValidator.getPendingTxCount()).toBe(2); }); + it('prepareForSlot deletes tx with disallowed setup calls when unprotecting', async () => { + // Create a pool where checkAllowedSetupCalls returns false + const disallowStore = await openTmpStore('p2p-disallow'); + const disallowArchiveStore = await openTmpStore('archive-disallow'); + const setupValidator = new AggregateTxValidator(mockValidator, new AllowedSetupCallsMetaValidator()); + const disallowPool = new AztecKVTxPoolV2(disallowStore, disallowArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(setupValidator), + checkAllowedSetupCalls: () => Promise.resolve(false), + }); + await disallowPool.start(); + + const tx = await mockTx(1); + + // Add as protected - checkAllowedSetupCalls returns false, so metadata.allowedSetupCalls = false + await disallowPool.addProtectedTxs([tx], slot1Header); + expect(await disallowPool.getTxStatus(tx.getTxHash())).toBe('protected'); + + // Unprotect - AllowedSetupCallsMetaValidator should reject since allowedSetupCalls is false + await disallowPool.prepareForSlot(SlotNumber(2)); + + expect(await disallowPool.getTxStatus(tx.getTxHash())).toBe('deleted'); + expect(await disallowPool.getPendingTxCount()).toBe(0); + + await disallowPool.stop(); + await disallowStore.delete(); + await disallowArchiveStore.delete(); + }); + + it('prepareForSlot keeps tx with allowed setup calls when unprotecting', async () => { + const tx = await mockTx(1); + + // poolWithValidator has checkAllowedSetupCalls returning true (default) + await poolWithValidator.addProtectedTxs([tx], slot1Header); + + await poolWithValidator.prepareForSlot(SlotNumber(2)); + + expect(await poolWithValidator.getTxStatus(tx.getTxHash())).toBe('pending'); + expect(await poolWithValidator.getPendingTxCount()).toBe(1); + }); + + it('startup rejects tx with disallowed setup calls after reloading from store', async () => { + // First, create a pool that allows setup calls and add a pending tx + const sharedStore = await openTmpStore('p2p-reload'); + const sharedArchiveStore = await openTmpStore('archive-reload'); + const pool1 = new AztecKVTxPoolV2(sharedStore, sharedArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(mockValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), + }); + await pool1.start(); + + const tx = await mockTx(1); + await pool1.addPendingTxs([tx]); + expect(await pool1.getPendingTxCount()).toBe(1); + await pool1.stop(); + + // Restart the pool with checkAllowedSetupCalls returning false. + // On reload, the tx gets allowedSetupCalls=false and revalidation rejects it. + const setupValidator = new AggregateTxValidator(mockValidator, new AllowedSetupCallsMetaValidator()); + const pool2 = new AztecKVTxPoolV2(sharedStore, sharedArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(setupValidator), + checkAllowedSetupCalls: () => Promise.resolve(false), + }); + await pool2.start(); + + expect(await pool2.getPendingTxCount()).toBe(0); + // Tx was never mined, so it gets hard-deleted (no soft-delete tracking) + expect(await pool2.getTxStatus(tx.getTxHash())).toBeUndefined(); + + await pool2.stop(); + await sharedStore.delete(); + await sharedArchiveStore.delete(); + }); + it('validation runs before nullifier conflict check in prepareForSlot', async () => { const txPending = await mockPublicTx(1, 5); const txProtected = await mockPublicTx(2, 10); @@ -2179,6 +2265,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await poolWithValidator.start(); }); @@ -4391,6 +4478,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -4416,6 +4504,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool2.start(); @@ -4444,6 +4533,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { maxPendingTxCount: 100 }, @@ -4470,6 +4560,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { maxPendingTxCount: 3 }, @@ -4502,6 +4593,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -4519,6 +4611,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool2.start(); @@ -4548,6 +4641,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -4582,6 +4676,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { maxPendingTxCount: 0 }, // No pending txs allowed @@ -4610,6 +4705,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -4636,6 +4732,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(selectiveValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool2.start(); @@ -4661,6 +4758,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -4686,6 +4784,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool2.start(); @@ -4880,6 +4979,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, // telemetry { minTxPoolAgeMs: 2_000 }, @@ -4978,6 +5078,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }, undefined, { minTxPoolAgeMs: 2_000 }, @@ -5214,6 +5315,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(mockValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await poolWithValidator.start(); @@ -5280,6 +5382,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -5314,6 +5417,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool2.start(); @@ -5348,6 +5452,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(throwingValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -5373,6 +5478,7 @@ describe('TxPoolV2', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool2.start(); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts index 8e196bcf3cfc..3baeaf320601 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_bench.test.ts @@ -141,6 +141,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool.start(); const cleanup = async () => { @@ -495,6 +496,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); await pool1.start(); @@ -512,6 +514,7 @@ describe('TxPoolV2: benchmarks', () => { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), }); const startTime = performance.now(); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts index 53e88e0e806e..d6701c372d97 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2_impl.ts @@ -62,6 +62,7 @@ export class TxPoolV2Impl { #l2BlockSource: L2BlockSource; #worldStateSynchronizer: WorldStateSynchronizer; #createTxValidator: TxPoolV2Dependencies['createTxValidator']; + #checkAllowedSetupCalls: TxPoolV2Dependencies['checkAllowedSetupCalls']; // === In-Memory Indices === #indices: TxPoolIndices = new TxPoolIndices(); @@ -93,6 +94,7 @@ export class TxPoolV2Impl { this.#l2BlockSource = deps.l2BlockSource; this.#worldStateSynchronizer = deps.worldStateSynchronizer; this.#createTxValidator = deps.createTxValidator; + this.#checkAllowedSetupCalls = deps.checkAllowedSetupCalls; this.#config = { ...DEFAULT_TX_POOL_V2_CONFIG, ...config }; this.#archive = new TxArchive(archiveStore, this.#config.archivedTxLimit, log); @@ -368,20 +370,25 @@ export class TxPoolV2Impl { async addProtectedTxs(txs: Tx[], block: BlockHeader, opts: { source?: string }): Promise { const slotNumber = block.globalVariables.slotNumber; + // Precompute setup-call allow-list flags outside the store transaction + const allowedFlags = await Promise.all(txs.map(tx => this.#checkAllowedSetupCalls(tx))); + await this.#store.transactionAsync(async () => { - for (const tx of txs) { + for (let i = 0; i < txs.length; i++) { + const tx = txs[i]; const txHash = tx.getTxHash(); const txHashStr = txHash.toString(); const isNew = !this.#indices.has(txHashStr); const minedBlockId = await this.#getMinedBlockId(txHash); if (isNew) { + const meta = await buildTxMetaData(tx, allowedFlags[i]); // New tx - add as mined or protected (callback emitted by #addTx) if (minedBlockId) { - await this.#addTx(tx, { mined: minedBlockId }, opts); + await this.#addTx(tx, { mined: minedBlockId }, opts, meta); this.#indices.setProtection(txHashStr, slotNumber); } else { - await this.#addTx(tx, { protected: slotNumber }, opts); + await this.#addTx(tx, { protected: slotNumber }, opts, meta); } } else { // Existing tx - update protection and mined status @@ -976,7 +983,8 @@ export class TxPoolV2Impl { try { const tx = Tx.fromBuffer(buffer); - const meta = await buildTxMetaData(tx); + const allowedSetupCalls = await this.#checkAllowedSetupCalls(tx); + const meta = await buildTxMetaData(tx, allowedSetupCalls); loaded.push({ tx, meta }); } catch (err) { this.#log.warn(`Failed to deserialize tx ${txHashStr}, deleting`, { err }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts index 7cb2e794566c..92ede7a4afaa 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.test.ts @@ -27,7 +27,7 @@ import { } from './factory.js'; import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; -import { PhasesTxValidator } from './phases_validator.js'; +import { AllowedSetupCallsMetaValidator, PhasesTxValidator } from './phases_validator.js'; import { SizeTxValidator } from './size_validator.js'; import { TimestampTxValidator } from './timestamp_validator.js'; import { TxPermittedValidator } from './tx_permitted_validator.js'; @@ -312,6 +312,7 @@ describe('Validator factory functions', () => { TimestampTxValidator.name, DoubleSpendTxValidator.name, BlockHeaderTxValidator.name, + AllowedSetupCallsMetaValidator.name, ]); }); diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts index 9acd13d88c3d..849f105b46be 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -58,7 +58,7 @@ import { DoubleSpendTxValidator, type NullifierSource } from './double_spend_val import { GasLimitsValidator, GasTxValidator } from './gas_validator.js'; import { MetadataTxValidator } from './metadata_validator.js'; import { NullifierCache } from './nullifier_cache.js'; -import { PhasesTxValidator } from './phases_validator.js'; +import { AllowedSetupCallsMetaValidator, PhasesTxValidator } from './phases_validator.js'; import { SizeTxValidator } from './size_validator.js'; import { TimestampTxValidator } from './timestamp_validator.js'; import { TxPermittedValidator } from './tx_permitted_validator.js'; @@ -436,5 +436,25 @@ export async function createTxValidatorForTransactionsEnteringPendingTxPool( new TimestampTxValidator({ timestamp, blockNumber }, bindings), new DoubleSpendTxValidator(nullifierSource, bindings), new BlockHeaderTxValidator(archiveSource, bindings), + new AllowedSetupCallsMetaValidator(bindings), ); } + +/** + * Creates a function that checks whether a tx's setup-phase calls are on the allow list. + * + * Uses the `PhasesTxValidator` on the full Tx. The result is stored as a boolean + * flag in `TxMetaData.allowedSetupCalls` at receipt time, so the pending pool + * migration validator can check it without needing the full Tx or its dependencies. + */ +export function createCheckAllowedSetupCalls( + contractDataSource: ContractDataSource, + setupAllowList: AllowedElement[], + getTimestamp: () => UInt64, +): (tx: Tx) => Promise { + return async (tx: Tx) => { + const validator = new PhasesTxValidator(contractDataSource, setupAllowList, getTimestamp()); + const result = await validator.validateTx(tx); + return result.result === 'valid'; + }; +} diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts index 9de8f7ef19e2..69d5bd9f0cab 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts @@ -141,3 +141,33 @@ export class PhasesTxValidator implements TxValidator { return TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED; } } + +/** Structural interface for the allowed-setup-calls flag check. */ +export interface HasAllowedSetupCallsData { + txHash: { toString(): string }; + allowedSetupCalls: boolean; +} + +/** + * Validates that a transaction's setup-phase calls were allowed at receipt time. + * + * Checks the precomputed `allowedSetupCalls` flag on TxMetaData. The flag is + * computed by running the PhasesTxValidator on the full Tx when it first enters + * the pool. This lightweight validator is used during pending pool migration to + * reject txs whose setup calls are not on the allow list. + */ +export class AllowedSetupCallsMetaValidator implements TxValidator { + #log: Logger; + + constructor(bindings?: LoggerBindings) { + this.#log = createLogger('sequencer:tx_validator:tx_phases_meta', bindings); + } + + validateTx(tx: T): Promise { + if (!tx.allowedSetupCalls) { + this.#log.verbose(`Rejecting tx ${tx.txHash} because its setup calls are not on the allow list`); + return Promise.resolve({ result: 'invalid', reason: [TX_ERROR_SETUP_FUNCTION_NOT_ALLOWED] }); + } + return Promise.resolve({ result: 'valid' }); + } +} From e393a4684da68aa4c01c6767ac1ecc726e912533 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 10 Mar 2026 21:01:40 +0000 Subject: [PATCH 2/2] test(tx): add tx pool tests for rejecting invalid setup on unprotect Co-Authored-By: Claude Opus 4.6 --- .../src/spartan/block_capacity.test.ts | 16 ++- .../mem_pools/tx_pool_v2/tx_pool_v2.test.ts | 135 ++++++++++++++++-- .../src/mem_pools/tx_pool_v2/tx_pool_v2.ts | 14 +- .../src/msg_validators/tx_validator/README.md | 6 +- 4 files changed, 155 insertions(+), 16 deletions(-) diff --git a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts index 22caa27180ea..308d84285c9a 100644 --- a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts +++ b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts @@ -360,10 +360,10 @@ describe('block capacity benchmark', () => { const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); // Deploy BenchmarkingContract using the first wallet logger.info('Deploying benchmark contract...'); - benchmarkContract = await BenchmarkingContract.deploy(wallets[0]).send({ + ({ contract: benchmarkContract } = await BenchmarkingContract.deploy(wallets[0]).send({ from: accountAddresses[0], fee: { paymentMethod: sponsor }, - }); + })); logger.info('BenchmarkingContract deployed', { address: benchmarkContract.address.toString() }); // Register benchmark contract with all other wallets @@ -392,11 +392,17 @@ describe('block capacity benchmark', () => { const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); // Deploy TokenContract using the first wallet logger.info('Deploying token contract...'); - tokenContract = await TokenContract.deploy(wallets[0], accountAddresses[0], 'USDC', 'USD', 18n).send({ + ({ contract: tokenContract } = await TokenContract.deploy( + wallets[0], + accountAddresses[0], + 'USDC', + 'USD', + 18n, + ).send({ from: accountAddresses[0], fee: { paymentMethod: sponsor }, wait: { timeout: 600 }, - }); + })); logger.info('TokenContract deployed', { address: tokenContract.address.toString() }); // Register token contract with all other wallets @@ -411,7 +417,7 @@ describe('block capacity benchmark', () => { logger.info(`Minting 1e18 tokens to each account...`); const mintTxHashes = []; for (const acc of accountAddresses) { - const txHash = await TokenContract.at(tokenContract.address, wallets[0]) + const { txHash } = await TokenContract.at(tokenContract.address, wallets[0]) .methods.mint_to_public(acc, 10n ** 18n) .send({ from: accountAddresses[0], fee: { paymentMethod: sponsor }, wait: NO_WAIT }); mintTxHashes.push(txHash); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts index dd9d9b824d24..a9ea4034f9b0 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.test.ts @@ -2180,10 +2180,51 @@ describe('TxPoolV2', () => { expect(await poolWithValidator.getPendingTxCount()).toBe(1); }); - it('startup rejects tx with disallowed setup calls after reloading from store', async () => { - // First, create a pool that allows setup calls and add a pending tx - const sharedStore = await openTmpStore('p2p-reload'); - const sharedArchiveStore = await openTmpStore('archive-reload'); + it('prepareForSlot handles mixed allowed/disallowed setup calls on unprotect', async () => { + // Create a pool where checkAllowedSetupCalls returns false for specific txs + const mixedStore = await openTmpStore('p2p-mixed'); + const mixedArchiveStore = await openTmpStore('archive-mixed'); + + const txAllowed = await mockTx(1); + const txDisallowed = await mockTx(2); + const txAlsoAllowed = await mockTx(3); + const disallowedHash = txDisallowed.getTxHash().toString(); + + const setupValidator = new AggregateTxValidator(mockValidator, new AllowedSetupCallsMetaValidator()); + const mixedPool = new AztecKVTxPoolV2(mixedStore, mixedArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(setupValidator), + // Only disallow setup calls for the second tx + checkAllowedSetupCalls: async tx => tx.getTxHash().toString() !== disallowedHash, + }); + await mixedPool.start(); + + // Add all as protected + await mixedPool.addProtectedTxs([txAllowed, txDisallowed, txAlsoAllowed], slot1Header); + expect(await mixedPool.getTxStatus(txAllowed.getTxHash())).toBe('protected'); + expect(await mixedPool.getTxStatus(txDisallowed.getTxHash())).toBe('protected'); + expect(await mixedPool.getTxStatus(txAlsoAllowed.getTxHash())).toBe('protected'); + + // Unprotect all - only txDisallowed should be deleted + await mixedPool.prepareForSlot(SlotNumber(2)); + + expect(await mixedPool.getTxStatus(txAllowed.getTxHash())).toBe('pending'); + expect(await mixedPool.getTxStatus(txDisallowed.getTxHash())).toBe('deleted'); + expect(await mixedPool.getTxStatus(txAlsoAllowed.getTxHash())).toBe('pending'); + expect(await mixedPool.getPendingTxCount()).toBe(2); + + await mixedPool.stop(); + await mixedStore.delete(); + await mixedArchiveStore.delete(); + }); + + it('handlePrunedBlocks deletes tx with disallowed setup calls after reload and un-mining', async () => { + db.findLeafIndices.mockResolvedValue([1n]); // Anchor block valid + + // Step 1: Start a pool that allows setup calls, add and mine a tx + const sharedStore = await openTmpStore('p2p-disallow-prune'); + const sharedArchiveStore = await openTmpStore('archive-disallow-prune'); const pool1 = new AztecKVTxPoolV2(sharedStore, sharedArchiveStore, { l2BlockSource: mockL2BlockSource, worldStateSynchronizer: mockWorldState, @@ -2194,11 +2235,12 @@ describe('TxPoolV2', () => { const tx = await mockTx(1); await pool1.addPendingTxs([tx]); - expect(await pool1.getPendingTxCount()).toBe(1); + await pool1.handleMinedBlock(makeBlock([tx], slot1Header)); + expect(await pool1.getTxStatus(tx.getTxHash())).toBe('mined'); await pool1.stop(); - // Restart the pool with checkAllowedSetupCalls returning false. - // On reload, the tx gets allowedSetupCalls=false and revalidation rejects it. + // Step 2: Restart pool with checkAllowedSetupCalls returning false. + // On reload, tx gets allowedSetupCalls=false in metadata. const setupValidator = new AggregateTxValidator(mockValidator, new AllowedSetupCallsMetaValidator()); const pool2 = new AztecKVTxPoolV2(sharedStore, sharedArchiveStore, { l2BlockSource: mockL2BlockSource, @@ -2206,17 +2248,54 @@ describe('TxPoolV2', () => { createTxValidator: () => Promise.resolve(setupValidator), checkAllowedSetupCalls: () => Promise.resolve(false), }); + // Mock getTxEffect to return the mined tx so it stays mined on reload + mockL2BlockSource.getTxEffect.mockResolvedValue({ + txEffect: TxEffect.empty(), + l2BlockNumber: 1, + l2BlockHash: '0x1', + } as any); await pool2.start(); + expect(await pool2.getTxStatus(tx.getTxHash())).toBe('mined'); + + // Restore original mock + mockL2BlockSource.getTxEffect.mockResolvedValue(undefined); + + // Step 3: Prune - tx gets un-mined and revalidated. + // AllowedSetupCallsMetaValidator rejects it since allowedSetupCalls=false. + await pool2.handlePrunedBlocks(block0Id); + expect(await pool2.getTxStatus(tx.getTxHash())).toBe('deleted'); expect(await pool2.getPendingTxCount()).toBe(0); - // Tx was never mined, so it gets hard-deleted (no soft-delete tracking) - expect(await pool2.getTxStatus(tx.getTxHash())).toBeUndefined(); await pool2.stop(); await sharedStore.delete(); await sharedArchiveStore.delete(); }); + it('pending tx via addPendingTxs has allowedSetupCalls=true regardless of checkAllowedSetupCalls', async () => { + // Create a pool where checkAllowedSetupCalls always returns false + const disallowStore = await openTmpStore('p2p-disallow-pending'); + const disallowArchiveStore = await openTmpStore('archive-disallow-pending'); + const disallowPool = new AztecKVTxPoolV2(disallowStore, disallowArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(mockValidator), + checkAllowedSetupCalls: () => Promise.resolve(false), + }); + await disallowPool.start(); + + const tx = await mockTx(1); + + // Add via addPendingTxs - this does NOT call checkAllowedSetupCalls, + // so allowedSetupCalls defaults to true + await disallowPool.addPendingTxs([tx]); + expect(disallowPool.getPoolReadAccess().getMetadata(tx.getTxHash().toString())?.allowedSetupCalls).toBe(true); + + await disallowPool.stop(); + await disallowStore.delete(); + await disallowArchiveStore.delete(); + }); + it('validation runs before nullifier conflict check in prepareForSlot', async () => { const txPending = await mockPublicTx(1, 5); const txProtected = await mockPublicTx(2, 10); @@ -4800,6 +4879,44 @@ describe('TxPoolV2', () => { await testArchiveStore.delete(); } }); + + it('hydration recomputes allowedSetupCalls from checkAllowedSetupCalls', async () => { + const testStore = await openTmpStore('p2p-hydration-setup-test'); + const testArchiveStore = await openTmpStore('archive-hydration-setup-test'); + + try { + // Add a tx with allowedSetupCalls=true (default for addPendingTxs) + const pool1 = new AztecKVTxPoolV2(testStore, testArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(true), + }); + await pool1.start(); + + const tx = await mockTx(1); + await pool1.addPendingTxs([tx]); + const txHashStr = tx.getTxHash().toString(); + expect(pool1.getPoolReadAccess().getMetadata(txHashStr)?.allowedSetupCalls).toBe(true); + await pool1.stop(); + + // Restart with checkAllowedSetupCalls returning false — metadata should reflect it + const pool2 = new AztecKVTxPoolV2(testStore, testArchiveStore, { + l2BlockSource: mockL2BlockSource, + worldStateSynchronizer: mockWorldState, + createTxValidator: () => Promise.resolve(alwaysValidValidator), + checkAllowedSetupCalls: () => Promise.resolve(false), + }); + await pool2.start(); + + expect(pool2.getPoolReadAccess().getMetadata(txHashStr)?.allowedSetupCalls).toBe(false); + + await pool2.stop(); + } finally { + await testStore.delete(); + await testArchiveStore.delete(); + } + }); }); describe('late arrival scenarios', () => { diff --git a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts index 69b91bd95c52..0902ebf767da 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool_v2/tx_pool_v2.ts @@ -11,7 +11,14 @@ import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-clien import EventEmitter from 'node:events'; import { PoolInstrumentation, PoolName } from '../instrumentation.js'; -import type { AddTxsResult, TxPoolV2, TxPoolV2Config, TxPoolV2Dependencies, TxPoolV2Events } from './interfaces.js'; +import type { + AddTxsResult, + PoolReadAccess, + TxPoolV2, + TxPoolV2Config, + TxPoolV2Dependencies, + TxPoolV2Events, +} from './interfaces.js'; import type { TxState } from './tx_metadata.js'; import { TxPoolV2Impl } from './tx_pool_v2_impl.js'; @@ -165,6 +172,11 @@ export class AztecKVTxPoolV2 extends (EventEmitter as new () => TypedEventEmitte return this.#queue.put(() => Promise.resolve(this.#impl.getLowestPriorityPending(limit))); } + /** Returns read-only access to the pool. Used for testing. */ + getPoolReadAccess(): PoolReadAccess { + return this.#impl.getPoolReadAccess(); + } + // === Configuration === updateConfig(config: Partial): Promise { diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/README.md b/yarn-project/p2p/src/msg_validators/tx_validator/README.md index 91087c1559c5..8e0c6b7de274 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/README.md +++ b/yarn-project/p2p/src/msg_validators/tx_validator/README.md @@ -75,10 +75,12 @@ This validator is invoked on **every** transaction potentially entering the pend - Startup hydration — revalidating persisted non-mined txs on node restart Runs: -- DoubleSpend, BlockHeader, GasLimits, Timestamp +- DoubleSpend, BlockHeader, GasLimits, Timestamp, AllowedSetupCalls Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. +The `AllowedSetupCallsMetaValidator` checks a precomputed boolean flag (`TxMetaData.allowedSetupCalls`) rather than re-running the full `PhasesTxValidator`. This flag is computed by `createCheckAllowedSetupCalls` when the tx first enters the pool (via `addProtectedTxs` or startup hydration), so the pool migration validator can reject txs with disallowed setup calls without needing the full `Tx` object or its dependencies. + ## Individual Validators | Validator | What it checks | Benchmarked verification duration | @@ -92,6 +94,7 @@ Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. | `GasTxValidator` | Gas limits are within bounds (delegates to `GasLimitsValidator`), max fee per gas meets current block fees, and fee payer has sufficient FeeJuice balance | 1.02 ms | | `GasLimitsValidator` | Gas limits are >= fixed minimums and <= AVM max processable L2 gas. Used standalone in pool migration; also called internally by `GasTxValidator` | 3–10 us | | `PhasesTxValidator` | Public function calls in setup phase are on the allow list | 10.12–13.12 us | +| `AllowedSetupCallsMetaValidator` | Checks the precomputed `allowedSetupCalls` flag on `TxMetaData`. Used in pool migration instead of the full `PhasesTxValidator` | — | | `BlockHeaderTxValidator` | Transaction's anchor block hash exists in the archive tree | 98.88 us | | `TxProofValidator` | Client proof verifies correctly | ~250ms | @@ -108,6 +111,7 @@ Operates on `TxMetaData` (pre-built by the pool) rather than full `Tx` objects. | Gas (balance + limits) | Stage 1 | Optional* | — | Yes | — | | GasLimits (standalone) | — | — | — | — | Yes | | Phases | Stage 1 | Yes | — | Yes | — | +| AllowedSetupCalls | — | — | — | — | Yes | | BlockHeader | Stage 1 | Yes | — | Yes | Yes | | Proof | Stage 2 | Optional** | Yes | — | — |