From b9dd7cf8b0dab0007bfa9bb6f7384d405837d8a7 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Sat, 24 Jan 2026 18:21:50 +0100 Subject: [PATCH 01/12] Fix state carrying issue with block prover --- .../src/prover/block/BlockProvable.ts | 142 ++++++++++++------ .../protocol/src/prover/block/BlockProver.ts | 91 ++++++----- .../contracts/settlement/SettlementBase.ts | 12 +- .../protocol/src/utils/ProvableHashList.ts | 10 +- .../src/protocol/production/flow/BatchFlow.ts | 5 + .../protocol/production/tasks/NewBlockTask.ts | 4 + .../NewBlockProvingParametersSerializer.ts | 12 +- .../production/tracing/BatchTracingService.ts | 26 +++- .../production/tracing/BlockTracingService.ts | 31 +++- .../test/integration/BlockProduction-test.ts | 1 + 10 files changed, 227 insertions(+), 107 deletions(-) diff --git a/packages/protocol/src/prover/block/BlockProvable.ts b/packages/protocol/src/prover/block/BlockProvable.ts index c0396b17..04b506b6 100644 --- a/packages/protocol/src/prover/block/BlockProvable.ts +++ b/packages/protocol/src/prover/block/BlockProvable.ts @@ -1,4 +1,4 @@ -import { Bool, Field, Proof, Provable, Struct } from "o1js"; +import { Bool, Field, Poseidon, Proof, Provable, Struct } from "o1js"; import { CompilableModule, WithZkProgrammable } from "@proto-kit/common"; import { StateTransitionProof } from "../statetransition/StateTransitionProvable"; @@ -52,6 +52,90 @@ export class BlockArgumentsBatch extends Struct({ batch: Provable.Array(BlockArguments, BLOCK_ARGUMENT_BATCH_SIZE), }) {} +const BlockProverStateBaseFields = { + eternalTransactionsHash: Field, + incomingMessagesHash: Field, + stateRoot: Field, + blockHashRoot: Field, + blockNumber: Field, + networkStateHash: Field, +}; + +export class BlockProverPublicInput extends Struct({ + // Tracker of the current block prover state + proverStateRemainder: Field, + ...BlockProverStateBaseFields, +}) { + public equals(input: BlockProverPublicInput): Bool { + const output2 = BlockProverPublicInput.toFields(input); + const output1 = BlockProverPublicInput.toFields(this); + return output1 + .map((value1, index) => value1.equals(output2[index])) + .reduce((a, b) => a.and(b)); + } + + public clone() { + return new BlockProverPublicInput( + BlockProverPublicInput.fromFields(BlockProverPublicInput.toFields(this)) + ); + } +} + +export const BlockProverStateCommitments = { + remainders: { + // Commitment to the list of unprocessed (pending) batches of STs that need to be proven + pendingSTBatchesHash: Field, + witnessedRootsHash: Field, + bundlesHash: Field, + }, + ...BlockProverStateBaseFields, +}; + +export class BlockProverStateInput extends Struct(BlockProverStateCommitments) { + public hash() { + return Poseidon.hash(BlockProverStateInput.toFields(this)); + } + + public static fromPublicInput(input: BlockProverPublicInput) { + return new BlockProverStateInput({ + remainders: { + bundlesHash: Field(0), + pendingSTBatchesHash: Field(0), + witnessedRootsHash: Field(0), + }, + eternalTransactionsHash: input.eternalTransactionsHash, + incomingMessagesHash: input.incomingMessagesHash, + stateRoot: input.stateRoot, + blockHashRoot: input.blockHashRoot, + blockNumber: input.blockNumber, + networkStateHash: input.networkStateHash, + }); + } + + public finalize(condition: Bool) { + condition + .implies( + this.remainders.bundlesHash + .equals(0) + .and(this.remainders.pendingSTBatchesHash.equals(0)) + .and(this.remainders.witnessedRootsHash.equals(0)) + ) + .assertTrue("Remainers not fully removed"); + + return new BlockProverPublicInput({ + proverStateRemainder: Field(0), + eternalTransactionsHash: this.eternalTransactionsHash, + incomingMessagesHash: this.incomingMessagesHash, + stateRoot: this.stateRoot, + blockHashRoot: this.blockHashRoot, + blockNumber: this.blockNumber, + networkStateHash: this.networkStateHash, + }); + } +} + +export class BlockProverPublicOutput extends BlockProverPublicInput {} + export class BlockProverState { /** * The network state which gives access to values such as blockHeight @@ -113,8 +197,8 @@ export class BlockProverState { this.incomingMessages = args.incomingMessages; } - public toCommitments(): BlockProverPublicInput { - return { + public toCommitments(): BlockProverStateInput { + return new BlockProverStateInput({ remainders: { bundlesHash: this.bundleList.commitment, pendingSTBatchesHash: this.pendingSTBatches.commitment, @@ -126,31 +210,31 @@ export class BlockProverState { blockHashRoot: this.blockHashRoot, blockNumber: this.blockNumber, networkStateHash: this.networkState.hash(), - }; + }); } public static blockProverFromCommitments( - publicInput: BlockProverPublicInput, + stateInput: NonMethods, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness ): BlockProverState { return new BlockProverState({ - bundleList: new BundleHashList(publicInput.remainders.bundlesHash), + bundleList: new BundleHashList(stateInput.remainders.bundlesHash), eternalTransactionsList: new TransactionHashList( - publicInput.eternalTransactionsHash + stateInput.eternalTransactionsHash ), incomingMessages: new MinaActionsHashList( - publicInput.incomingMessagesHash + stateInput.incomingMessagesHash ), pendingSTBatches: new AppliedBatchHashList( - publicInput.remainders.pendingSTBatchesHash + stateInput.remainders.pendingSTBatchesHash ), witnessedRoots: new WitnessedRootHashList( - publicInput.remainders.witnessedRootsHash + stateInput.remainders.witnessedRootsHash ), - stateRoot: publicInput.stateRoot, - blockHashRoot: publicInput.blockHashRoot, - blockNumber: publicInput.blockNumber, + stateRoot: stateInput.stateRoot, + blockHashRoot: stateInput.blockHashRoot, + blockNumber: stateInput.blockNumber, networkState, blockWitness, }); @@ -250,37 +334,6 @@ export class BlockProverState { } } -export const BlockProverStateCommitments = { - remainders: { - // Commitment to the list of unprocessed (pending) batches of STs that need to be proven - pendingSTBatchesHash: Field, - witnessedRootsHash: Field, - bundlesHash: Field, - }, - eternalTransactionsHash: Field, - incomingMessagesHash: Field, - stateRoot: Field, - blockHashRoot: Field, - blockNumber: Field, - networkStateHash: Field, -}; - -export class BlockProverPublicInput extends Struct( - BlockProverStateCommitments -) {} - -export class BlockProverPublicOutput extends Struct({ - ...BlockProverStateCommitments, -}) { - public equals(input: BlockProverPublicInput): Bool { - const output2 = BlockProverPublicOutput.toFields(input); - const output1 = BlockProverPublicOutput.toFields(this); - return output1 - .map((value1, index) => value1.equals(output2[index])) - .reduce((a, b) => a.and(b)); - } -} - export type BlockProof = Proof; export interface BlockProvable @@ -288,6 +341,7 @@ export interface BlockProvable CompilableModule { proveBlockBatch: ( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 83485f3b..c821ac0d 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -6,6 +6,7 @@ import { CompileArtifact, CompileRegistry, log, + NonMethods, PlainZkProgram, provableMethod, reduceSequential, @@ -42,13 +43,14 @@ import { import { Bundle } from "../accumulators/BlockHashList"; import { - BlockProvable, + BlockArguments, + BlockArgumentsBatch, BlockProof, + BlockProvable, BlockProverPublicInput, BlockProverPublicOutput, BlockProverState, - BlockArgumentsBatch, - BlockArguments, + BlockProverStateInput, } from "./BlockProvable"; import { BlockHashMerkleTreeWitness, @@ -223,6 +225,7 @@ export class BlockProverProgrammable extends ZkProgrammable< @provableMethod() public async proveBlockBatch( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, @@ -231,29 +234,45 @@ export class BlockProverProgrammable extends ZkProgrammable< deferTransactionProof: Bool, batch: BlockArgumentsBatch ): Promise { - publicInput.networkStateHash.assertEquals( + const hasNoStateRemained = publicInput.proverStateRemainder.equals(0); + + // If the state is supplied as a witness, we check that it is equals the PI's stateHash + stateWitness + .hash() + .equals(publicInput.proverStateRemainder) + .or(hasNoStateRemained) + .assertTrue("Input state witness is invalid"); + + const stateInputs = Provable.if( + hasNoStateRemained, + BlockProverStateInput, + BlockProverStateInput.fromPublicInput(publicInput), + stateWitness + ); + + stateInputs.networkStateHash.assertEquals( networkState.hash(), "Network state not valid" ); + let state = BlockProverState.blockProverFromCommitments( + stateInputs, + networkState, + blockWitness + ); + // Calculate the new block tree hash const blockIndex = blockWitness.calculateIndex(); - blockIndex.assertEquals(publicInput.blockNumber); + blockIndex.assertEquals(stateInputs.blockNumber); blockWitness .calculateRoot(Field(0)) .assertEquals( - publicInput.blockHashRoot, + stateInputs.blockHashRoot, "Supplied block hash witness not matching state root" ); - let state = BlockProverState.blockProverFromCommitments( - publicInput, - networkState, - blockWitness - ); - // Prove blocks iteratively state = await reduceSequential( batch.batch, @@ -333,7 +352,21 @@ export class BlockProverProgrammable extends ZkProgrammable< state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; - return new BlockProverPublicOutput(state.toCommitments()); + const finalizedOutput = state.toCommitments(); + + const deferredOutput = { + ...publicInput, + blockProverStateHashRemainder: finalizedOutput.hash(), + }; + + return new BlockProverPublicOutput( + Provable.if( + verifyTransactionProof, + BlockProverPublicOutput, + finalizedOutput.finalize(verifyTransactionProof), + deferredOutput + ) + ); } private async proveBlock( @@ -456,13 +489,7 @@ export class BlockProverProgrammable extends ZkProgrammable< proof2.verify(); function checkProperty< - Key extends - | "stateRoot" - | "networkStateHash" - | "blockHashRoot" - | "eternalTransactionsHash" - | "incomingMessagesHash" - | "blockNumber", + Key extends keyof NonMethods, >(key: Key) { // Check state publicInput[key].assertEquals( @@ -475,29 +502,12 @@ export class BlockProverProgrammable extends ZkProgrammable< ); } - function checkRemainderProperty< - Key extends "pendingSTBatchesHash" | "witnessedRootsHash" | "bundlesHash", - >(key: Key) { - // Check state - publicInput.remainders[key].assertEquals( - proof1.publicInput.remainders[key], - errors.propertyNotMatchingStep(key, "publicInput.from -> proof1.from") - ); - proof1.publicOutput.remainders[key].assertEquals( - proof2.publicInput.remainders[key], - errors.propertyNotMatchingStep(key, "proof1.to -> proof2.from") - ); - } - checkProperty("stateRoot"); checkProperty("networkStateHash"); checkProperty("blockHashRoot"); checkProperty("eternalTransactionsHash"); checkProperty("incomingMessagesHash"); - - checkRemainderProperty("bundlesHash"); - checkRemainderProperty("pendingSTBatchesHash"); - checkRemainderProperty("witnessedRootsHash"); + checkProperty("proverStateRemainder"); return proof2.publicOutput; } @@ -525,6 +535,7 @@ export class BlockProverProgrammable extends ZkProgrammable< methods: { proveBlockBatch: { privateInputs: [ + BlockProverStateInput, NetworkState, BlockHashMerkleTreeWitness, StateTransitionProofClass, @@ -535,6 +546,7 @@ export class BlockProverProgrammable extends ZkProgrammable< ], async method( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, @@ -546,6 +558,7 @@ export class BlockProverProgrammable extends ZkProgrammable< return { publicOutput: await proveBlockBatch( publicInput, + stateWitness, networkState, blockWitness, stateTransitionProof, @@ -647,6 +660,7 @@ export class BlockProver public proveBlockBatch( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, @@ -657,6 +671,7 @@ export class BlockProver ): Promise { return this.zkProgrammable.proveBlockBatch( publicInput, + stateWitness, networkState, blockWitness, stateTransitionProof, diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts index e37e35a6..050543b5 100644 --- a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -199,17 +199,9 @@ export abstract class SettlementBase ); // Check remainders are zero - blockProof.publicOutput.remainders.bundlesHash.assertEquals( + blockProof.publicOutput.proverStateRemainder.assertEquals( Field(0), - "Bundles list has not been fully proven" - ); - blockProof.publicOutput.remainders.pendingSTBatchesHash.assertEquals( - Field(0), - "Supplied proof is has outstanding STs to be proven" - ); - blockProof.publicOutput.remainders.witnessedRootsHash.assertEquals( - Field(0), - "Supplied proof is has outstanding witnessed roots hashes to be proven" + "Supplied proof is has outstanding block prover state to be proven" ); // Execute onSettlementHooks for additional checks diff --git a/packages/protocol/src/utils/ProvableHashList.ts b/packages/protocol/src/utils/ProvableHashList.ts index b59da667..da96f7a2 100644 --- a/packages/protocol/src/utils/ProvableHashList.ts +++ b/packages/protocol/src/utils/ProvableHashList.ts @@ -79,9 +79,13 @@ export abstract class ProvableHashList { ) { const { from, to } = transition; - condition - .implies(from.equals(this.commitment)) - .assertTrue(`From-commitment for ${message} not matching`); + // Equal to condition -> (from == this.commitment) + from + .mul(condition.toField()) + .assertEquals( + this.commitment.mul(condition.toField()), + `From-commitment for ${message} not matching` + ); this.commitment = Provable.if(condition, to, this.commitment); } diff --git a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts index 82497089..e752f922 100644 --- a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts @@ -48,6 +48,11 @@ export class BatchFlow { b.publicInput.eternalTransactionsHash ) ) + .and( + a.publicOutput.proverStateRemainder.equals( + b.publicInput.proverStateRemainder + ) + ) .toBoolean(); } diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index de5c1501..5b883da5 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -13,6 +13,7 @@ import { TransactionProvable, BlockArguments, BlockArgumentsBatch, + BlockProverStateInput, } from "@proto-kit/protocol"; import { Bool } from "o1js"; import { @@ -37,6 +38,7 @@ export type NewBlockArguments = { export interface NewBlockProverParameters { publicInput: BlockProverPublicInput; + stateWitness: BlockProverStateInput; networkState: NetworkState; blockWitness: BlockHashMerkleTreeWitness; deferSTProof: Bool; @@ -103,6 +105,7 @@ export class NewBlockTask networkState, blockWitness, publicInput, + stateWitness, deferSTProof, deferTransactionProof, blocks, @@ -123,6 +126,7 @@ export class NewBlockTask async () => { await this.blockProver.proveBlockBatch( publicInput, + stateWitness, networkState, blockWitness, input1, diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts index d0f8bc12..3c59f730 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts @@ -2,6 +2,7 @@ import { BlockArguments, BlockHashMerkleTreeWitness, BlockProverPublicInput, + BlockProverStateInput, NetworkState, ReturnType, StateTransitionProof, @@ -28,6 +29,7 @@ interface JsonType { input2: string; params: { publicInput: ReturnType; + stateWitness: ReturnType; networkState: ReturnType; blockWitness: ReturnType; deferSTProof: boolean; @@ -68,6 +70,8 @@ export class NewBlockProvingParametersSerializer params: { publicInput: BlockProverPublicInput.toJSON(input.params.publicInput), + stateWitness: BlockProverStateInput.toJSON(input.params.stateWitness), + networkState: NetworkState.toJSON(input.params.networkState), blockWitness: BlockHashMerkleTreeWitness.toJSON( @@ -102,8 +106,12 @@ export class NewBlockProvingParametersSerializer input2: await this.transactionProofSerializer.fromJSON(jsonObject.input2), params: { - publicInput: BlockProverPublicInput.fromJSON( - jsonObject.params.publicInput + publicInput: new BlockProverPublicInput( + BlockProverPublicInput.fromJSON(jsonObject.params.publicInput) + ), + + stateWitness: new BlockProverStateInput( + BlockProverStateInput.fromJSON(jsonObject.params.stateWitness) ), networkState: new NetworkState( diff --git a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts index f09e2071..5a2dd464 100644 --- a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts @@ -72,6 +72,11 @@ export class BatchTracingService { const batchState = this.createBatchState(blocks[0]); + const publicInput = this.blockTracingService.openBatch( + batchState, + blocks[0] + ); + // Trace blocks const numBlocks = blocks.length; const numBatches = Math.ceil(numBlocks / BLOCK_ARGUMENT_BATCH_SIZE); @@ -80,9 +85,21 @@ export class BatchTracingService { chunk(blocks, BLOCK_ARGUMENT_BATCH_SIZE), async (state, batch, index) => { // Trace batch of blocks fitting in single proof - const batchTrace = this.blockTracingService.openBlock(state, batch[0]); + const partialBlockTrace = this.blockTracingService.openBlock( + state, + batch[0], + publicInput + ); const start = state.blockNumber.toString(); + // PI is taken for first chunk of batch, all others use the stateWitness + // Copy here because we need a fresh instance + const currentPublicInput = publicInput.clone(); + if (index > 0) { + currentPublicInput.proverStateRemainder = + partialBlockTrace.stateWitness.hash(); + } + const [newState, combinedTraces] = await yieldSequential( batch, async (state2, block, jndex) => { @@ -118,10 +135,11 @@ export class BatchTracingService { const blockTrace: BlockTrace = { block: { - ...batchTrace, + ...partialBlockTrace, + publicInput: currentPublicInput, blocks: blockArgumentBatch.concat(dummies), - deferTransactionProof: Bool(numBatches - 1 < index), - deferSTProof: Bool(numBatches - 1 < index), + deferTransactionProof: Bool(numBatches - 1 !== index), + deferSTProof: Bool(numBatches - 1 !== index), }, heights: [start, newState.blockNumber.toString()], }; diff --git a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts index e0e8867c..a326ea89 100644 --- a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts @@ -8,6 +8,7 @@ import { WitnessedRootWitness, BundleHashList, BundlePreimage, + BlockProverStateInput, } from "@proto-kit/protocol"; import { Bool, Field } from "o1js"; import { toStateTransitionsHash } from "@proto-kit/module"; @@ -42,20 +43,38 @@ export class BlockTracingService { public readonly tracer: Tracer ) {} - public openBlock( + public openBatch( state: BlockTracingState, { block: firstBlock, result: firstResult }: BlockWithResult + ) { + return new BlockProverPublicInput({ + stateRoot: state.stateRoot, + blockNumber: firstBlock.height, + blockHashRoot: firstBlock.fromBlockHashRoot, + eternalTransactionsHash: firstBlock.fromEternalTransactionsHash, + incomingMessagesHash: firstBlock.fromMessagesHash, + networkStateHash: firstBlock.networkState.before.hash(), + proverStateRemainder: Field(0), + }); + } + + public openBlock( + state: BlockTracingState, + { block: firstBlock, result: firstResult }: BlockWithResult, + batchInput: BlockProverPublicInput ): Pick< NewBlockProverParameters, - "publicInput" | "networkState" | "blockWitness" + "stateWitness" | "networkState" | "blockWitness" > { - const publicInput: BlockProverPublicInput = new BlockProverPublicInput({ + const stateWitness = new BlockProverStateInput({ stateRoot: state.stateRoot, blockNumber: firstBlock.height, blockHashRoot: firstBlock.fromBlockHashRoot, - eternalTransactionsHash: firstBlock.fromEternalTransactionsHash, - incomingMessagesHash: firstBlock.fromMessagesHash, networkStateHash: firstBlock.networkState.before.hash(), + // The next two are properties that we fast-forward only after tx proofs are verified + // Therefore those don't change over multiple block batches + eternalTransactionsHash: batchInput.eternalTransactionsHash, + incomingMessagesHash: batchInput.incomingMessagesHash, remainders: { witnessedRootsHash: state.witnessedRoots.commitment, pendingSTBatchesHash: state.pendingSTBatches.commitment, @@ -64,7 +83,7 @@ export class BlockTracingService { }); return { - publicInput, + stateWitness, networkState: firstBlock.networkState.before, blockWitness: firstResult.blockHashWitness, }; diff --git a/packages/sequencer/test/integration/BlockProduction-test.ts b/packages/sequencer/test/integration/BlockProduction-test.ts index 06f6c688..fe0bb665 100644 --- a/packages/sequencer/test/integration/BlockProduction-test.ts +++ b/packages/sequencer/test/integration/BlockProduction-test.ts @@ -539,6 +539,7 @@ export function testBlockProduction< [2, 1, 1], [1, 2, 1], [1, 1, 2], + [1, 5, 1], [2, 2, 2], [1, 14, 0], ])( From 4db36dae1007da4ab5f9d5f404b499c46c023201 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Sat, 24 Jan 2026 19:09:18 +0100 Subject: [PATCH 02/12] Fixed issue with empty transaction proofs not triggering batch finalization --- packages/common/src/trees/sparse/RollupMerkleTree.ts | 2 ++ packages/protocol/src/prover/block/BlockProver.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/common/src/trees/sparse/RollupMerkleTree.ts b/packages/common/src/trees/sparse/RollupMerkleTree.ts index c701a091..3cd90301 100644 --- a/packages/common/src/trees/sparse/RollupMerkleTree.ts +++ b/packages/common/src/trees/sparse/RollupMerkleTree.ts @@ -189,6 +189,8 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { return hash; } + // TODO Make sure this implementation is as efficient as it gets. + // Especially compared to doing calculateRoot + witness new witness + check index public calculateRootIncrement( leafIndex: Field, leaf: Field diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index c821ac0d..415ed19a 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -287,9 +287,10 @@ export class BlockProverProgrammable extends ZkProgrammable< ); // Verify Transaction proof if it has at least 1 tx and it isn't deferred - const verifyTransactionProof = deferTransactionProof - .not() - .and(state.bundleList.isEmpty().not()); + const finalizeBlockProof = deferTransactionProof.not(); + const verifyTransactionProof = finalizeBlockProof.and( + state.bundleList.isEmpty().not() + ); transactionProof.verifyIf(verifyTransactionProof); @@ -361,9 +362,9 @@ export class BlockProverProgrammable extends ZkProgrammable< return new BlockProverPublicOutput( Provable.if( - verifyTransactionProof, + finalizeBlockProof, BlockProverPublicOutput, - finalizedOutput.finalize(verifyTransactionProof), + finalizedOutput.finalize(finalizeBlockProof), deferredOutput ) ); From b9904a56eb425d09c162410034ca52a497927846 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Sat, 24 Jan 2026 19:34:31 +0100 Subject: [PATCH 03/12] Fixed wrong public output due to missed renaming --- packages/protocol/src/prover/block/BlockProver.ts | 2 +- packages/sequencer/test/integration/BlockProduction-test.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 415ed19a..cc64531f 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -357,8 +357,8 @@ export class BlockProverProgrammable extends ZkProgrammable< const deferredOutput = { ...publicInput, - blockProverStateHashRemainder: finalizedOutput.hash(), }; + deferredOutput.proverStateRemainder = finalizedOutput.hash(); return new BlockProverPublicOutput( Provable.if( diff --git a/packages/sequencer/test/integration/BlockProduction-test.ts b/packages/sequencer/test/integration/BlockProduction-test.ts index fe0bb665..bb20c9c0 100644 --- a/packages/sequencer/test/integration/BlockProduction-test.ts +++ b/packages/sequencer/test/integration/BlockProduction-test.ts @@ -537,11 +537,10 @@ export function testBlockProduction< it.each([ [2, 1, 1], - [1, 2, 1], - [1, 1, 2], [1, 5, 1], [2, 2, 2], [1, 14, 0], + [1, 6, 5], ])( "should produce multiple blocks with multiple batches with multiple transactions", async (batches, blocksPerBatch, txsPerBlock) => { From ddd651ad0d3fd4f5dec1b10db72576e49b4d7bcc Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Sun, 25 Jan 2026 16:59:38 +0100 Subject: [PATCH 04/12] Fixed bug in block batch sizing --- .../src/protocol/production/tracing/BatchTracingService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts index 5a2dd464..3df22720 100644 --- a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts @@ -117,13 +117,15 @@ export class BatchTracingService { state ); + const [blockArgumentBatch, transactionTraces] = unzip(combinedTraces); + // Fill up with dummies const dummyBlockArgs = BlockArguments.noop( newState, Field(blocks.at(-1)!.result.stateRoot) ); const dummies = range( - blocks.length, + blockArgumentBatch.length, BLOCK_ARGUMENT_BATCH_SIZE ).map(() => ({ args: dummyBlockArgs, @@ -131,8 +133,6 @@ export class BatchTracingService { startingStateBeforeHook: {}, })); - const [blockArgumentBatch, transactionTraces] = unzip(combinedTraces); - const blockTrace: BlockTrace = { block: { ...partialBlockTrace, From c7e81e530103ec21dc68601c8b0dedd65f761d0d Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Sun, 25 Jan 2026 16:55:13 +0100 Subject: [PATCH 05/12] Added sanity check to block task --- .../sequencer/src/protocol/production/tasks/NewBlockTask.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index 5b883da5..427ee0ca 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -14,6 +14,7 @@ import { BlockArguments, BlockArgumentsBatch, BlockProverStateInput, + BLOCK_ARGUMENT_BATCH_SIZE, } from "@proto-kit/protocol"; import { Bool } from "o1js"; import { @@ -111,6 +112,10 @@ export class NewBlockTask blocks, } = parameters; + if (blocks.length !== BLOCK_ARGUMENT_BATCH_SIZE) { + throw new Error("Given block argument length not exactly batch size"); + } + const blockArgumentBatch = new BlockArgumentsBatch({ batch: blocks.map((block) => block.args), }); From eb1fa512e666201d871752b0f5d234df979b5cff Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Mon, 26 Jan 2026 16:31:10 +0100 Subject: [PATCH 06/12] Fixed handling of witnessed root preimage --- .../accumulators/WitnessedRootHashList.ts | 28 ++++++++++++------- .../src/prover/block/BlockProvable.ts | 20 +++++++------ .../protocol/src/prover/block/BlockProver.ts | 12 ++++---- .../production/tasks/StateTransitionTask.ts | 6 +--- .../production/tracing/BlockTracingService.ts | 10 ++----- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts index 7226492d..220ed53a 100644 --- a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts +++ b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts @@ -1,4 +1,4 @@ -import { Bool, Field, Struct } from "o1js"; +import { Bool, Field, Provable, Struct } from "o1js"; import { DefaultProvableHashList } from "../../utils/ProvableHashList"; @@ -13,7 +13,7 @@ export class WitnessedRoot extends Struct({ export class WitnessedRootWitness extends Struct({ witnessedRoot: Field, - preimage: Field, + // preimage: Field, }) {} /** @@ -21,7 +21,10 @@ export class WitnessedRootWitness extends Struct({ */ export class WitnessedRootHashList extends DefaultProvableHashList { - public constructor(commitment: Field = Field(0)) { + public constructor( + commitment: Field = Field(0), + public preimage: Field = Field(0) + ) { super(WitnessedRoot, commitment); } @@ -37,17 +40,13 @@ export class WitnessedRootHashList extends DefaultProvableHashList { - const output = await this.stateTransitionProver.proveBatch( + await this.stateTransitionProver.proveBatch( input.publicInput, input.batch, new MerkleWitnessBatch({ witnesses: input.merkleWitnesses.slice() }), input.batchState ); - log.debug("STTask public io:", { - input: StateTransitionProverPublicInput.toJSON(input.publicInput), - output: StateTransitionProverPublicOutput.toJSON(output), - }); return await this.executionContext .current() diff --git a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts index a326ea89..98c02358 100644 --- a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts @@ -76,9 +76,10 @@ export class BlockTracingService { eternalTransactionsHash: batchInput.eternalTransactionsHash, incomingMessagesHash: batchInput.incomingMessagesHash, remainders: { - witnessedRootsHash: state.witnessedRoots.commitment, pendingSTBatchesHash: state.pendingSTBatches.commitment, bundlesHash: state.bundleList.commitment, + witnessedRootsHash: state.witnessedRoots.commitment, + witnessedRootsPreimage: state.witnessedRoots.preimage, }, }); @@ -173,14 +174,8 @@ export class BlockTracingService { state.incomingMessages = afterState.incomingMessages; state.eternalTransactionsList = afterState.eternalTransactionsList; - const preimage = afterState.witnessedRoots - .getUnconstrainedValues() - .get() - .at(-2)?.preimage; - const afterBlockRootWitness: WitnessedRootWitness = { witnessedRoot: Field(block.result.witnessedRoots[0]), - preimage: preimage ?? Field(0), }; // We create the batch here, because we need the afterBlockRootWitness, @@ -206,7 +201,6 @@ export class BlockTracingService { appliedBatchListState: afterState.pendingSTBatches.commitment, root: afterBlockRootWitness.witnessedRoot, }, - afterBlockRootWitness.preimage, state.pendingSTBatches.commitment.equals(0).not() ); } From 3e5ffc371c886828102b01407505f8273ac6989d Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Mon, 26 Jan 2026 16:45:32 +0100 Subject: [PATCH 07/12] Added regression test for filled + empty blocks --- .../accumulators/WitnessedRootHashList.ts | 7 ---- .../test/integration/BlockProduction-test.ts | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts index 220ed53a..9671c658 100644 --- a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts +++ b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts @@ -13,7 +13,6 @@ export class WitnessedRoot extends Struct({ export class WitnessedRootWitness extends Struct({ witnessedRoot: Field, - // preimage: Field, }) {} /** @@ -37,8 +36,6 @@ export class WitnessedRootHashList extends DefaultProvableHashList { + log.setLevel("INFO"); + + (sequencer.resolve("BlockProducerModule") as BlockProducerModule).config = { + maximumBlockSize: 5, + }; + + const privateKey = PrivateKey.random(); + + for (const i of range(0, 7)) { + await test.addTransaction({ + method: ["Balance", "addBalance"], + privateKey, + args: [PrivateKey.random().toPublicKey(), UInt64.from(100)], + }); + } + + // Produce 6 blocks, 5 txs each into 1 batch + const block = await test.produceBlock(); + + expectDefined(block); + expect(block.transactions).toHaveLength(5); + expect(block.transactions[0].status.toBoolean()).toBe(true); + + await test.produceBlock(); + await test.produceBlock(); + await test.produceBlock(); + await test.produceBlock(); + await test.produceBlock(); + const batch = await test.produceBatch(); + + expectDefined(batch); + + console.log(batch.proof); + + expect(batch.blockHashes).toHaveLength(6); + expect(batch.proof.proof.length).toBeGreaterThan(50); + }, 30000); + it("events - should produce block with the right events", async () => { log.setLevel("TRACE"); From ad8e8996a922aa74a45d1211635c4dea972843d6 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Mon, 26 Jan 2026 17:47:18 +0100 Subject: [PATCH 08/12] Fixed halting issue in block flow --- .../src/prover/block/BlockProvable.ts | 5 ++ .../test/integration/BlockProduction-test.ts | 67 ++++++++++--------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/packages/protocol/src/prover/block/BlockProvable.ts b/packages/protocol/src/prover/block/BlockProvable.ts index eab36dc1..400971ae 100644 --- a/packages/protocol/src/prover/block/BlockProvable.ts +++ b/packages/protocol/src/prover/block/BlockProvable.ts @@ -318,6 +318,11 @@ export class BlockProverState { condition, a.witnessedRoots.commitment, b.witnessedRoots.commitment + ), + Provable.if( + condition, + a.witnessedRoots.preimage, + b.witnessedRoots.preimage ) ), stateRoot: Provable.if(condition, a.stateRoot, b.stateRoot), diff --git a/packages/sequencer/test/integration/BlockProduction-test.ts b/packages/sequencer/test/integration/BlockProduction-test.ts index 57ed164a..328b2a66 100644 --- a/packages/sequencer/test/integration/BlockProduction-test.ts +++ b/packages/sequencer/test/integration/BlockProduction-test.ts @@ -685,44 +685,47 @@ export function testBlockProduction< expect(batch!.proof.proof).toBe(MOCK_PROOF); }, 30000); - it("should produce some filled blocks and some empty blocks", async () => { - log.setLevel("INFO"); - - (sequencer.resolve("BlockProducerModule") as BlockProducerModule).config = { - maximumBlockSize: 5, - }; - - const privateKey = PrivateKey.random(); - - for (const i of range(0, 7)) { - await test.addTransaction({ - method: ["Balance", "addBalance"], - privateKey, - args: [PrivateKey.random().toPublicKey(), UInt64.from(100)], - }); - } + it.each([4, 6, 9])( + "should produce some filled blocks and some empty blocks", + async (numBlocks) => { + log.setLevel("INFO"); + + (sequencer.resolve("BlockProducerModule") as BlockProducerModule).config = + { + maximumBlockSize: 5, + }; + + const privateKey = PrivateKey.random(); + + for (const i of range(0, 7)) { + await test.addTransaction({ + method: ["Balance", "addBalance"], + privateKey, + args: [PrivateKey.random().toPublicKey(), UInt64.from(100)], + }); + } - // Produce 6 blocks, 5 txs each into 1 batch - const block = await test.produceBlock(); + // Produce 6 blocks, 5 txs each into 1 batch + const block = await test.produceBlock(); - expectDefined(block); - expect(block.transactions).toHaveLength(5); - expect(block.transactions[0].status.toBoolean()).toBe(true); + expectDefined(block); + expect(block.transactions).toHaveLength(5); + expect(block.transactions[0].status.toBoolean()).toBe(true); - await test.produceBlock(); - await test.produceBlock(); - await test.produceBlock(); - await test.produceBlock(); - await test.produceBlock(); - const batch = await test.produceBatch(); + await mapSequential( + range(0, numBlocks - 1), + async () => await test.produceBlock() + ); + const batch = await test.produceBatch(); - expectDefined(batch); + expectDefined(batch); - console.log(batch.proof); + console.log(batch.proof); - expect(batch.blockHashes).toHaveLength(6); - expect(batch.proof.proof.length).toBeGreaterThan(50); - }, 30000); + expect(batch.blockHashes).toHaveLength(numBlocks); + }, + 30000 + ); it("events - should produce block with the right events", async () => { log.setLevel("TRACE"); From c434f839a85a3456441c00f7935ad32e2e79909c Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Mon, 26 Jan 2026 17:52:10 +0100 Subject: [PATCH 09/12] Fixed linting --- .../src/protocol/production/tasks/StateTransitionTask.ts | 2 -- packages/sequencer/test/integration/BlockProduction-test.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts index f68b0b52..532a0d5d 100644 --- a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts @@ -9,10 +9,8 @@ import { StateTransitionProvable, StateTransitionProvableBatch, StateTransitionProverPublicInput, - StateTransitionProverPublicOutput, } from "@proto-kit/protocol"; import { - log, ProvableMethodExecutionContext, CompileRegistry, LinkedMerkleTreeWitness, diff --git a/packages/sequencer/test/integration/BlockProduction-test.ts b/packages/sequencer/test/integration/BlockProduction-test.ts index 328b2a66..58f1b0c9 100644 --- a/packages/sequencer/test/integration/BlockProduction-test.ts +++ b/packages/sequencer/test/integration/BlockProduction-test.ts @@ -697,6 +697,7 @@ export function testBlockProduction< const privateKey = PrivateKey.random(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const i of range(0, 7)) { await test.addTransaction({ method: ["Balance", "addBalance"], From c2920e5b0295514fb30d49ed96c7b9b35f3add45 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Tue, 27 Jan 2026 15:06:52 +0100 Subject: [PATCH 10/12] Added proofs/no-proofs variants for blockprover --- .../src/prover/block/BlockProvable.ts | 17 +- .../protocol/src/prover/block/BlockProver.ts | 329 ++++++++++++------ .../src/protocol/production/flow/BatchFlow.ts | 2 + .../protocol/production/tasks/NewBlockTask.ts | 42 ++- 4 files changed, 275 insertions(+), 115 deletions(-) diff --git a/packages/protocol/src/prover/block/BlockProvable.ts b/packages/protocol/src/prover/block/BlockProvable.ts index 400971ae..fc2a1505 100644 --- a/packages/protocol/src/prover/block/BlockProvable.ts +++ b/packages/protocol/src/prover/block/BlockProvable.ts @@ -348,16 +348,25 @@ export type BlockProof = Proof; export interface BlockProvable extends WithZkProgrammable, CompilableModule { - proveBlockBatch: ( + proveBlockBatchNoProofs: ( publicInput: BlockProverPublicInput, stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, - stateTransitionProof: StateTransitionProof, + batch: BlockArgumentsBatch, + finalize: Bool + ) => Promise; + + proveBlockBatchWithProofs: ( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, deferSTProof: Bool, - transactionProof: TransactionProof, deferTransactionProof: Bool, - batch: BlockArgumentsBatch + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ) => Promise; merge: ( diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 740d42e3..4bb709c2 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -64,6 +64,8 @@ const errors = { propertyNotMatching: (propertyName: string) => `${propertyName} not matching`, }; +type Tail = T extends [infer A, ...infer Rest] ? Rest : never; + type BlockHookArgument = T extends "before" ? BeforeBlockHookArguments : AfterBlockHookArguments; @@ -222,70 +224,37 @@ export class BlockProverProgrammable extends ZkProgrammable< }; } - @provableMethod() - public async proveBlockBatch( - publicInput: BlockProverPublicInput, - stateWitness: BlockProverStateInput, - networkState: NetworkState, - blockWitness: BlockHashMerkleTreeWitness, + private verifySTProof( + state: BlockProverState, stateTransitionProof: StateTransitionProof, - deferSTProof: Bool, - transactionProof: TransactionProof, - deferTransactionProof: Bool, - batch: BlockArgumentsBatch - ): Promise { - const hasNoStateRemained = publicInput.proverStateRemainder.equals(0); - - // If the state is supplied as a witness, we check that it is equals the PI's stateHash - stateWitness - .hash() - .equals(publicInput.proverStateRemainder) - .or(hasNoStateRemained) - .assertTrue("Input state witness is invalid"); - - const stateInputs = Provable.if( - hasNoStateRemained, - BlockProverStateInput, - BlockProverStateInput.fromPublicInput(publicInput), - stateWitness - ); - - stateInputs.networkStateHash.assertEquals( - networkState.hash(), - "Network state not valid" - ); - - let state = BlockProverState.blockProverFromCommitments( - stateInputs, - networkState, - blockWitness - ); - - // Calculate the new block tree hash - const blockIndex = blockWitness.calculateIndex(); - - blockIndex.assertEquals(stateInputs.blockNumber); - - blockWitness - .calculateRoot(Field(0)) - .assertEquals( - stateInputs.blockHashRoot, - "Supplied block hash witness not matching state root" - ); - - // Prove blocks iteratively - state = await reduceSequential( - batch.batch, - async (current, block) => { - const result = await this.proveBlock(current.copy(), block); - - this.stateServiceProvider.popCurrentStateService(); + deferSTProof: Bool + ) { + // Verify ST Proof only if STs have been emitted, + // and we don't defer the verification of the STs + // otherwise we can input a dummy proof + const batchesEmpty = state.pendingSTBatches.commitment.equals(Field(0)); + const verifyStProof = deferSTProof.not().and(batchesEmpty.not()); + log.provable.debug("Verify STProof", verifyStProof); + stateTransitionProof.verifyIf(verifyStProof); - return BlockProverState.choose(block.isDummy, current, result); - }, - state + // Apply STProof if not deferred + const stateProofResult = this.includeSTProof( + stateTransitionProof, + verifyStProof, + state.stateRoot, + state.pendingSTBatches.commitment, + state.witnessedRoots.commitment ); + state.stateRoot = stateProofResult.stateRoot; + state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; + state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; + } + private verifyTransactionProof( + state: BlockProverState, + transactionProof: TransactionProof, + deferTransactionProof: Bool + ) { // Verify Transaction proof if it has at least 1 tx and it isn't deferred const finalizeBlockProof = deferTransactionProof.not(); const verifyTransactionProof = finalizeBlockProof.and( @@ -332,27 +301,61 @@ export class BlockProverProgrammable extends ZkProgrammable< verifyTransactionProof, "bundles hash" ); + } - // Verify ST Proof only if STs have been emitted, - // and we don't defer the verification of the STs - // otherwise we can input a dummy proof - const batchesEmpty = state.pendingSTBatches.commitment.equals(Field(0)); - const verifyStProof = deferSTProof.not().and(batchesEmpty.not()); - log.provable.debug("Verify STProof", verifyStProof); - stateTransitionProof.verifyIf(verifyStProof); + private parseState( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness + ) { + const hasNoStateRemained = publicInput.proverStateRemainder.equals(0); - // Apply STProof if not deferred - const stateProofResult = this.includeSTProof( - stateTransitionProof, - verifyStProof, - state.stateRoot, - state.pendingSTBatches.commitment, - state.witnessedRoots.commitment + // If the state is supplied as a witness, we check that it is equals the PI's stateHash + stateWitness + .hash() + .equals(publicInput.proverStateRemainder) + .or(hasNoStateRemained) + .assertTrue("Input state witness is invalid"); + + const stateInputs = Provable.if( + hasNoStateRemained, + BlockProverStateInput, + BlockProverStateInput.fromPublicInput(publicInput), + stateWitness ); - state.stateRoot = stateProofResult.stateRoot; - state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; - state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; + stateInputs.networkStateHash.assertEquals( + networkState.hash(), + "Network state not valid" + ); + + const state = BlockProverState.blockProverFromCommitments( + stateInputs, + networkState, + blockWitness + ); + + // Verify block witness validity + const blockIndex = blockWitness.calculateIndex(); + + blockIndex.assertEquals(stateInputs.blockNumber); + + blockWitness + .calculateRoot(Field(0)) + .assertEquals( + stateInputs.blockHashRoot, + "Supplied block hash witness not matching state root" + ); + + return state; + } + + private computeOutput( + publicInput: BlockProverPublicInput, + state: BlockProverState, + finalizeBlockProof: Bool + ) { const finalizedOutput = state.toCommitments(); const deferredOutput = { @@ -370,6 +373,84 @@ export class BlockProverProgrammable extends ZkProgrammable< ); } + @provableMethod() + public async proveBlockBatchNoProofs( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + finalize: Bool + ) { + return await this.proveBlockBatch( + false, + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + // TODO Don't do this -> very confusing + finalize, + finalize + ); + } + + @provableMethod() + public async proveBlockBatchWithProofs( + ...args: Required>> + ) { + return await this.proveBlockBatch(true, ...args); + } + + public async proveBlockBatch( + doProofVerification: boolean, + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, + stateTransitionProof?: StateTransitionProof, + transactionProof?: TransactionProof + ): Promise { + let state = this.parseState( + publicInput, + stateWitness, + networkState, + blockWitness + ); + + // Prove blocks iteratively + state = await reduceSequential( + batch.batch, + async (current, block) => { + const result = await this.proveBlock(current.copy(), block); + + this.stateServiceProvider.popCurrentStateService(); + + return BlockProverState.choose(block.isDummy, current, result); + }, + state + ); + + if (doProofVerification) { + this.verifyTransactionProof( + state, + transactionProof!, + deferTransactionProof + ); + this.verifySTProof(state, stateTransitionProof!, deferSTProof); + } + + const finalizeBlockProof = deferTransactionProof.or(deferSTProof).not(); + // .or() + // .or(state.bundleList.isEmpty().and(state.pendingSTBatches.isEmpty())); + // TODO This finalizes immediately if nothing happened - which we don't account for in tracer currently + + return this.computeOutput(publicInput, state, finalizeBlockProof); + } + private async proveBlock( state: BlockProverState, args: BlockArguments @@ -445,6 +526,8 @@ export class BlockProverProgrammable extends ZkProgrammable< startingPendingStBatches ); + // TODO Cover case when we witness root but pendingSTBatches is completely empty + state.witnessedRoots.witnessRoot( { appliedBatchListState: state.pendingSTBatches.commitment, @@ -525,7 +608,9 @@ export class BlockProverProgrammable extends ZkProgrammable< const { prover, stateTransitionProver, transactionProver } = this; const StateTransitionProofClass = stateTransitionProver.zkProgram[0].Proof; const TransactionProofClass = transactionProver.zkProgram[0].Proof; - const proveBlockBatch = prover.proveBlockBatch.bind(prover); + const proveBlockBatchWithProofs = + prover.proveBlockBatchWithProofs.bind(prover); + const proveBlockBatchNoProofs = prover.proveBlockBatchNoProofs.bind(prover); const merge = prover.merge.bind(prover); const program = ZkProgram({ @@ -534,39 +619,68 @@ export class BlockProverProgrammable extends ZkProgrammable< publicOutput: BlockProverPublicOutput, methods: { - proveBlockBatch: { + proveBlockBatchWithProofs: { privateInputs: [ BlockProverStateInput, NetworkState, BlockHashMerkleTreeWitness, - StateTransitionProofClass, + BlockArgumentsBatch, Bool, - TransactionProofClass, Bool, - BlockArgumentsBatch, + StateTransitionProofClass, + TransactionProofClass, ], async method( publicInput: BlockProverPublicInput, stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, - stateTransitionProof: StateTransitionProof, + batch: BlockArgumentsBatch, deferSTProof: Bool, - transactionProof: TransactionProof, deferTransactionProof: Bool, - batch: BlockArgumentsBatch + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ) { return { - publicOutput: await proveBlockBatch( + publicOutput: await proveBlockBatchWithProofs( publicInput, stateWitness, networkState, blockWitness, - stateTransitionProof, + batch, deferSTProof, - transactionProof, deferTransactionProof, - batch + stateTransitionProof, + transactionProof + ), + }; + }, + }, + + proveBlockBatchNoProofs: { + privateInputs: [ + BlockProverStateInput, + NetworkState, + BlockHashMerkleTreeWitness, + BlockArgumentsBatch, + Bool, + ], + async method( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + finalize: Bool + ) { + return { + publicOutput: await proveBlockBatchNoProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + finalize ), }; }, @@ -590,7 +704,8 @@ export class BlockProverProgrammable extends ZkProgrammable< }); const methods = { - proveBlockBatch: program.proveBlockBatch, + proveBlockBatchWithProofs: program.proveBlockBatchWithProofs, + proveBlockBatchNoProofs: program.proveBlockBatchNoProofs, merge: program.merge, }; @@ -659,27 +774,45 @@ export class BlockProver }); } - public proveBlockBatch( + public proveBlockBatchNoProofs( publicInput: BlockProverPublicInput, stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, - stateTransitionProof: StateTransitionProof, + batch: BlockArgumentsBatch, + finalize: Bool + ): Promise { + return this.zkProgrammable.proveBlockBatchNoProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + finalize + ); + } + + public proveBlockBatchWithProofs( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, deferSTProof: Bool, - transactionProof: TransactionProof, deferTransactionProof: Bool, - batch: BlockArgumentsBatch + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ): Promise { - return this.zkProgrammable.proveBlockBatch( + return this.zkProgrammable.proveBlockBatchWithProofs( publicInput, stateWitness, networkState, blockWitness, - stateTransitionProof, + batch, deferSTProof, - transactionProof, deferTransactionProof, - batch + stateTransitionProof, + transactionProof ); } diff --git a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts index e752f922..e2267ec6 100644 --- a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts @@ -125,6 +125,8 @@ export class BatchFlow { } ); + // TODO Cover case where either 0 STs or 0 Transactions are in a batch + // Push all blocks except the last one with dummy proofs // except the last one, which will wait on the two proofs to complete await mapSequential( diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index 427ee0ca..1b25581c 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -16,7 +16,7 @@ import { BlockProverStateInput, BLOCK_ARGUMENT_BATCH_SIZE, } from "@proto-kit/protocol"; -import { Bool } from "o1js"; +import { Bool, Provable } from "o1js"; import { ProvableMethodExecutionContext, CompileRegistry, @@ -129,26 +129,42 @@ export class NewBlockTask this.protocol.stateServiceProvider, stateRecords, async () => { - await this.blockProver.proveBlockBatch( - publicInput, - stateWitness, - networkState, - blockWitness, - input1, - deferSTProof, - input2, - deferTransactionProof, - blockArgumentBatch - ); + if (deferSTProof.toBoolean() && deferTransactionProof.toBoolean()) { + await this.blockProver.proveBlockBatchNoProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + blockArgumentBatch, + deferSTProof.or(deferTransactionProof) + ); + } else { + await this.blockProver.proveBlockBatchWithProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + blockArgumentBatch, + deferSTProof, + deferTransactionProof, + input1, + input2 + ); + } } ); - return await executeWithPrefilledStateService( + const proof = await executeWithPrefilledStateService( this.protocol.stateServiceProvider, stateRecords, async () => await this.executionContext.current().result.prove() ); + + Provable.log("Input", proof.publicInput); + Provable.log("Output", proof.publicOutput); + + return proof; } public async prepare(): Promise { From d4099670e96168ea01e7d44cb5286a78026d35b6 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Wed, 28 Jan 2026 15:11:33 +0100 Subject: [PATCH 11/12] Added dummy transaction proof for empty batches --- .../protocol/src/prover/block/BlockProver.ts | 38 ++++++++++++++----- .../prover/transaction/TransactionProvable.ts | 4 ++ .../prover/transaction/TransactionProver.ts | 20 ++++++++++ .../src/protocol/production/flow/BlockFlow.ts | 26 ++++++------- .../protocol/production/tasks/NewBlockTask.ts | 3 +- .../tasks/TransactionProvingTask.ts | 24 ++++++++++++ ...ansactionProvingTaskParameterSerializer.ts | 8 ++++ .../types/TransactionProvingTypes.ts | 10 +++-- 8 files changed, 104 insertions(+), 29 deletions(-) diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 4bb709c2..f959f035 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -389,17 +389,39 @@ export class BlockProverProgrammable extends ZkProgrammable< networkState, blockWitness, batch, - // TODO Don't do this -> very confusing - finalize, + Bool(true), + Bool(true), finalize ); } @provableMethod() public async proveBlockBatchWithProofs( - ...args: Required>> + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ) { - return await this.proveBlockBatch(true, ...args); + const finalize = deferTransactionProof.or(deferSTProof).not(); + + return await this.proveBlockBatch( + true, + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + deferSTProof, + deferTransactionProof, + finalize, + stateTransitionProof, + transactionProof + ); } public async proveBlockBatch( @@ -411,6 +433,7 @@ export class BlockProverProgrammable extends ZkProgrammable< batch: BlockArgumentsBatch, deferSTProof: Bool, deferTransactionProof: Bool, + finalize: Bool, stateTransitionProof?: StateTransitionProof, transactionProof?: TransactionProof ): Promise { @@ -443,12 +466,7 @@ export class BlockProverProgrammable extends ZkProgrammable< this.verifySTProof(state, stateTransitionProof!, deferSTProof); } - const finalizeBlockProof = deferTransactionProof.or(deferSTProof).not(); - // .or() - // .or(state.bundleList.isEmpty().and(state.pendingSTBatches.isEmpty())); - // TODO This finalizes immediately if nothing happened - which we don't account for in tracer currently - - return this.computeOutput(publicInput, state, finalizeBlockProof); + return this.computeOutput(publicInput, state, finalize); } private async proveBlock( diff --git a/packages/protocol/src/prover/transaction/TransactionProvable.ts b/packages/protocol/src/prover/transaction/TransactionProvable.ts index bb99ab0c..0803946a 100644 --- a/packages/protocol/src/prover/transaction/TransactionProvable.ts +++ b/packages/protocol/src/prover/transaction/TransactionProvable.ts @@ -171,6 +171,10 @@ export interface TransactionProvable executionData2: TransactionProverExecutionData ) => Promise; + dummy: ( + publicInput: TransactionProverPublicInput + ) => Promise; + merge: ( publicInput: TransactionProverPublicInput, proof1: TransactionProof, diff --git a/packages/protocol/src/prover/transaction/TransactionProver.ts b/packages/protocol/src/prover/transaction/TransactionProver.ts index c2fb917a..7d8291cf 100644 --- a/packages/protocol/src/prover/transaction/TransactionProver.ts +++ b/packages/protocol/src/prover/transaction/TransactionProver.ts @@ -302,6 +302,13 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< ); } + @provableMethod() + public async dummy( + publicInput: TransactionProverPublicInput + ): Promise { + return publicInput; + } + @provableMethod() public async merge( publicInput: TransactionProverPublicInput, @@ -367,6 +374,7 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< const proveTransaction = prover.proveTransaction.bind(prover); const proveTransactions = prover.proveTransactions.bind(prover); const merge = prover.merge.bind(prover); + const dummy = prover.dummy.bind(prover); const program = ZkProgram({ name: "TransactionProver", @@ -419,6 +427,13 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< }, }, + dummy: { + privateInputs: [], + async method(publicInput: TransactionProverPublicInput) { + return { publicOutput: await dummy(publicInput) }; + }, + }, + merge: { privateInputs: [ SelfProof< @@ -445,6 +460,7 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< const methods = { proveTransaction: program.proveTransaction, proveTransactions: program.proveTransactions, + dummy: program.dummy, merge: program.merge, }; @@ -531,6 +547,10 @@ export class TransactionProver ); } + public dummy(publicInput: TransactionProverPublicInput) { + return this.zkProgrammable.dummy(publicInput); + } + public merge( publicInput: TransactionProverPublicInput, proof1: TransactionProof, diff --git a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts index 527dd083..e1531fdc 100644 --- a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts @@ -32,23 +32,21 @@ export class BlockFlow { private readonly transactionMergeTask: TransactionReductionTask ) {} + private dummyProof: TransactionProof | undefined = undefined; + private async dummyTransactionProof() { - const publicInput = { - bundlesHash: Field(0), - eternalTransactionsHash: Field(0), - incomingMessagesHash: Field(0), - } satisfies TransactionProverPublicInput; + if (this.dummyProof !== undefined) { + return this.dummyProof; + } - // TODO Set publicInput.stateRoot to result after block hooks! - const publicOutput = new TransactionProverPublicOutput({ - ...publicInput, + const flow = this.flowCreator.createFlow("transaction-dummy", undefined); + const dummy = await flow.withFlow(async (resolve) => { + await flow.pushTask(this.transactionTask, "dummy", async (result) => { + resolve(result); + }); }); - - return await this.protocol.transactionProver.zkProgrammable.zkProgram[0].Proof.dummy( - publicInput, - publicOutput, - 2 - ); + this.dummyProof = dummy; + return dummy; } private async proveTransactions(height: string, traces: TransactionTrace[]) { diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index 1b25581c..dfa151ea 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -136,7 +136,8 @@ export class NewBlockTask networkState, blockWitness, blockArgumentBatch, - deferSTProof.or(deferTransactionProof) + Bool(false) + // deferSTProof.or(deferTransactionProof) ); } else { await this.blockProver.proveBlockBatchWithProofs( diff --git a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts index 88c77d4d..5f6896b9 100644 --- a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts @@ -6,6 +6,7 @@ import { DynamicRuntimeProof, TransactionProvable, TransactionProof, + TransactionProverPublicInput, } from "@proto-kit/protocol"; import { Runtime } from "@proto-kit/module"; import { inject, injectable, Lifecycle, scoped } from "tsyringe"; @@ -87,9 +88,32 @@ export class TransactionProvingTask ); } + private async computeDummy(): Promise { + await executeWithPrefilledStateService( + this.protocol.stateServiceProvider, + [{}, {}], + async () => { + await this.transactionProver.dummy( + TransactionProverPublicInput.empty() + ); + } + ); + + return await executeWithPrefilledStateService( + this.protocol.stateServiceProvider, + [{}, {}], + async () => + await this.executionContext.current().result.prove() + ); + } + public async compute( input: TransactionProvingTaskParameters ): Promise { + if (input === "dummy") { + return await this.computeDummy(); + } + const startingState = input.flatMap((i) => i.parameters.startingState); await executeWithPrefilledStateService( diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts index 2cb30a52..0f34205a 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts @@ -84,6 +84,10 @@ export class TransactionProvingTaskParameterSerializer } public toJSON(inputs: TransactionProvingTaskParameters): string { + if (inputs === "dummy") { + return "dummy"; + } + const taskParamsJson: TransactionProvingTaskParametersJSON = inputs.map( (input) => { const { parameters, proof } = input; @@ -119,6 +123,10 @@ export class TransactionProvingTaskParameterSerializer public async fromJSON( json: string ): Promise { + if (json === "dummy") { + return "dummy"; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const jsonReadyObject: TransactionProvingTaskParametersJSON = JSON.parse(json); diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts b/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts index 0db92f4b..ae4a2ac2 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts @@ -17,7 +17,9 @@ export interface TransactionProverTaskParameters { export type OneOrTwo = [Type] | [Type, Type]; -export type TransactionProvingTaskParameters = OneOrTwo<{ - parameters: TransactionProverTaskParameters; - proof: RuntimeProof; -}>; +export type TransactionProvingTaskParameters = + | "dummy" + | OneOrTwo<{ + parameters: TransactionProverTaskParameters; + proof: RuntimeProof; + }>; From 4e7fd90ed8d1022b6ae24183a182bfc00bbde5bb Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 29 Jan 2026 14:13:26 +0100 Subject: [PATCH 12/12] Fixed linting --- packages/protocol/src/prover/block/BlockProver.ts | 2 -- packages/sequencer/src/protocol/production/flow/BlockFlow.ts | 3 --- 2 files changed, 5 deletions(-) diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index f959f035..f03430c2 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -64,8 +64,6 @@ const errors = { propertyNotMatching: (propertyName: string) => `${propertyName} not matching`, }; -type Tail = T extends [infer A, ...infer Rest] ? Rest : never; - type BlockHookArgument = T extends "before" ? BeforeBlockHookArguments : AfterBlockHookArguments; diff --git a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts index e1531fdc..874a4f03 100644 --- a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts @@ -3,10 +3,7 @@ import { MandatoryProtocolModulesRecord, Protocol, TransactionProof, - TransactionProverPublicInput, - TransactionProverPublicOutput, } from "@proto-kit/protocol"; -import { Field } from "o1js"; import { mapSequential } from "@proto-kit/common"; // eslint-disable-next-line import/no-extraneous-dependencies import chunk from "lodash/chunk";