From eef46f5f73085af5950c9fa1544b40d6af26f5e2 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Sat, 20 Dec 2025 13:24:14 +0100 Subject: [PATCH 1/5] Implemented calculateRootIncrement for merkle witnesses --- .../src/trees/sparse/RollupMerkleTree.ts | 109 +++++++++++++++--- packages/common/test/trees/MerkleTree.test.ts | 33 +++++- 2 files changed, 122 insertions(+), 20 deletions(-) diff --git a/packages/common/src/trees/sparse/RollupMerkleTree.ts b/packages/common/src/trees/sparse/RollupMerkleTree.ts index 1974e7078..f901ccf29 100644 --- a/packages/common/src/trees/sparse/RollupMerkleTree.ts +++ b/packages/common/src/trees/sparse/RollupMerkleTree.ts @@ -7,6 +7,17 @@ import { TypedClass } from "../../types"; import { MerkleTreeStore } from "./MerkleTreeStore"; import { InMemoryMerkleTreeStorage } from "./InMemoryMerkleTreeStorage"; +/** + * More efficient version of `maybeSwapBad` which + * reuses an intermediate variable + */ +export function maybeSwap(b: Bool, x: Field, y: Field): [Field, Field] { + const m = b.toField().mul(x.sub(y)); // b*(x - y) + const x1 = y.add(m); // y + b*(x - y) + const y2 = x.sub(m); // x - b*(x - y) = x + b*(y - x) + return [x1, y2]; +} + export class StructTemplate extends Struct({ path: Provable.Array(Field, 0), isLeft: Provable.Array(Bool, 0), @@ -22,6 +33,11 @@ export interface AbstractMerkleWitness extends StructTemplate { */ calculateRoot(hash: Field): Field; + calculateRootIncrement( + index: Field, + leaf: Field + ): [Field, AbstractMerkleWitness]; + /** * Calculates the index of the leaf node that belongs to this Witness. * @returns Index of the leaf. @@ -119,6 +135,15 @@ export interface AbstractMerkleTreeClass { * It also holds the Witness class under tree.WITNESS */ export function createMerkleTree(height: number): AbstractMerkleTreeClass { + function generateZeroes() { + const zeroes = [0n]; + for (let index = 1; index < height; index += 1) { + const previousLevel = Field(zeroes[index - 1]); + zeroes.push(Poseidon.hash([previousLevel, previousLevel]).toBigInt()); + } + return zeroes; + } + /** * The {@link RollupMerkleWitness} class defines a circuit-compatible base class * for [Merkle Witness'](https://computersciencewiki.org/index.php/Merkle_proof). @@ -147,7 +172,7 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { for (let index = 1; index < n; ++index) { const isLeft = this.isLeft[index - 1]; - // eslint-disable-next-line @typescript-eslint/no-use-before-define + const [left, right] = maybeSwap(isLeft, hash, this.path[index - 1]); hash = Poseidon.hash([left, right]); } @@ -155,6 +180,68 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { return hash; } + public calculateRootIncrement( + leafIndex: Field, + leaf: Field + ): [Field, RollupMerkleWitness] { + let zero: bigint[] = []; + Provable.asProver(() => { + zero = generateZeroes(); + }); + + if (zero.length === 0) { + throw new Error("Zeroes not initialized"); + } + const zeroes = zero.map((x) => Field(x)); + + let hash = leaf; + const n = this.height(); + + let notDiverged = Bool(true); + const newPath = leafIndex.add(1).toBits(); + newPath.push(Bool(false)); + + const newSiblings: Field[] = []; + const newIsLefts: Bool[] = []; + + for (let index = 0; index < n - 1; ++index) { + const isLeft = this.isLeft[index]; + const sibling = this.path[index]; + + const newIsLeft = newPath[index].not(); + + // Bool(true) default for root level + let convergesNextLevel = Bool(true); + if (index < n - 2) { + convergesNextLevel = newPath[index + 1] + .equals(this.isLeft[index + 1]) + .not(); + } + + const nextSibling = Provable.if( + convergesNextLevel.and(notDiverged), + hash, + Provable.if(notDiverged, zeroes[index], sibling) + ); + + notDiverged = notDiverged.and(convergesNextLevel.not()); + + newSiblings.push(nextSibling); + newIsLefts.push(newIsLeft); + + const [left, right] = maybeSwap(isLeft, hash, sibling); + hash = Poseidon.hash([left, right]); + } + + return [ + hash, + new RollupMerkleWitness({ + isLeft: newIsLefts, + path: newSiblings, + }), + ]; + } + /** * Calculates the index of the leaf node that belongs to this Witness. * @returns Index of the leaf. @@ -215,6 +302,7 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { }); } } + return class AbstractRollupMerkleTree implements AbstractMerkleTree { public static HEIGHT = height; @@ -238,13 +326,7 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { public constructor(store: MerkleTreeStore) { this.store = store; - this.zeroes = [0n]; - for (let index = 1; index < AbstractRollupMerkleTree.HEIGHT; index += 1) { - const previousLevel = Field(this.zeroes[index - 1]); - this.zeroes.push( - Poseidon.hash([previousLevel, previousLevel]).toBigInt() - ); - } + this.zeroes = generateZeroes(); } public assertIndexRange(index: bigint) { @@ -414,14 +496,3 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { export class RollupMerkleTree extends createMerkleTree(256) {} export class RollupMerkleTreeWitness extends RollupMerkleTree.WITNESS {} - -/** - * More efficient version of `maybeSwapBad` which - * reuses an intermediate variable - */ -export function maybeSwap(b: Bool, x: Field, y: Field): [Field, Field] { - const m = b.toField().mul(x.sub(y)); // b*(x - y) - const x1 = y.add(m); // y + b*(x - y) - const y2 = x.sub(m); // x - b*(x - y) = x + b*(y - x) - return [x1, y2]; -} diff --git a/packages/common/test/trees/MerkleTree.test.ts b/packages/common/test/trees/MerkleTree.test.ts index 221c8d70e..933e07504 100644 --- a/packages/common/test/trees/MerkleTree.test.ts +++ b/packages/common/test/trees/MerkleTree.test.ts @@ -1,5 +1,5 @@ import { beforeEach } from "@jest/globals"; -import { Field } from "o1js"; +import { Field, Provable } from "o1js"; import { createMerkleTree, @@ -217,4 +217,35 @@ describe.each([4, 16, 256])("cachedMerkleTree - %s", (height) => { tree.getNode(0, index); }).toThrow("Index greater than maximum leaf number"); }); + + it("witness incrementing", () => { + tree.setLeaf(0n, Field(3256)); + tree.setLeaf(1n, Field(3256)); + tree.setLeaf(2n, Field(3256)); + + const witness = tree.getWitness(3n); + + const [root, newWitness] = witness.calculateRootIncrement( + Field(3), + Field(1234) + ); + tree.setLeaf(3n, Field(1234)); + + expect(tree.getRoot().toString()).toStrictEqual(root.toString()); + expect(newWitness.calculateIndex().toString()).toStrictEqual("4"); + + const [root2, newWitness2] = newWitness.calculateRootIncrement( + Field(4), + Field(4321) + ); + tree.setLeaf(4n, Field(4321)); + + expect(tree.getRoot().toString()).toStrictEqual(root2.toString()); + expect(newWitness2.calculateIndex().toString()).toStrictEqual("5"); + + const root3 = newWitness2.calculateRoot(Field(555)); + tree.setLeaf(5n, Field(555)); + + expect(tree.getRoot().toString()).toStrictEqual(root3.toString()); + }); }); From 80d9a69e6090ceb66dfc7e9f23ea8b419fde0c74 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Fri, 9 Jan 2026 00:20:55 +0100 Subject: [PATCH 2/5] Implemented BlockProver v3 circuits --- packages/protocol/src/index.ts | 1 + .../src/protocol/ProvableBlockHook.ts | 41 +- .../src/protocol/ProvableTransactionHook.ts | 14 +- .../src/prover/accumulators/BlockHashList.ts | 117 ++++ .../accumulators/WitnessedRootHashList.ts | 2 +- .../src/prover/block/BlockProvable.ts | 236 ++++++- .../protocol/src/prover/block/BlockProver.ts | 585 ++++++++---------- .../prover/transaction/TransactionProvable.ts | 69 ++- .../prover/transaction/TransactionProver.ts | 198 +++--- packages/protocol/src/prover/utils.ts | 12 +- .../contracts/settlement/SettlementBase.ts | 14 +- .../src/utils/MinaPrefixedProvableHashList.ts | 8 +- .../src/utils/PrefixedProvableHashList.ts | 4 + .../protocol/src/utils/ProvableHashList.ts | 39 +- 14 files changed, 821 insertions(+), 519 deletions(-) create mode 100644 packages/protocol/src/prover/accumulators/BlockHashList.ts diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 8d8cf7d9d..34e794b3d 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -22,6 +22,7 @@ export * from "./prover/accumulators/StateTransitionReductionList"; export * from "./prover/accumulators/AppliedBatchHashList"; export * from "./prover/accumulators/WitnessedRootHashList"; export * from "./prover/accumulators/TransactionHashList"; +export * from "./prover/accumulators/BlockHashList"; export * from "./prover/block/BlockProver"; export * from "./prover/block/BlockProvable"; export * from "./prover/block/accummulators/RuntimeVerificationKeyTree"; diff --git a/packages/protocol/src/protocol/ProvableBlockHook.ts b/packages/protocol/src/protocol/ProvableBlockHook.ts index 7ba6dcd32..4de723a6f 100644 --- a/packages/protocol/src/protocol/ProvableBlockHook.ts +++ b/packages/protocol/src/protocol/ProvableBlockHook.ts @@ -3,47 +3,52 @@ import { NoConfig } from "@proto-kit/common"; import { NetworkState } from "../model/network/NetworkState"; import { - BlockProverState, BlockProverPublicInput, + BlockArguments, + BlockProverState, } from "../prover/block/BlockProvable"; import { TransitioningProtocolModule } from "./TransitioningProtocolModule"; export type ProvableHookBlockState = Pick< - BlockProverPublicInput, - | "transactionsHash" - | "eternalTransactionsHash" - | "incomingMessagesHash" - | "blockHashRoot" + BlockProverPublicInput & BlockArguments, + "eternalTransactionsHash" | "incomingMessagesHash" | "blockHashRoot" >; -export function toProvableHookBlockState( +export function toBeforeBlockHookArgument( state: Pick< BlockProverState, - | "transactionList" - | "eternalTransactionsList" - | "incomingMessages" - | "blockHashRoot" + "eternalTransactionsList" | "incomingMessages" | "blockHashRoot" > ) { - const { - transactionList, - eternalTransactionsList, - incomingMessages, - blockHashRoot, - } = state; + const { eternalTransactionsList, incomingMessages, blockHashRoot } = state; return { - transactionsHash: transactionList.commitment, eternalTransactionsHash: eternalTransactionsList.commitment, incomingMessagesHash: incomingMessages.commitment, blockHashRoot, }; } +export function toAfterBlockHookArgument( + state: Pick< + BlockProverState, + "eternalTransactionsList" | "incomingMessages" | "blockHashRoot" + >, + stateRoot: Field, + transactionsHash: Field +) { + return { + ...toBeforeBlockHookArgument(state), + stateRoot, + transactionsHash, + }; +} + export interface BeforeBlockHookArguments extends ProvableHookBlockState {} export interface AfterBlockHookArguments extends BeforeBlockHookArguments { stateRoot: Field; + transactionsHash: Field; } // Purpose is to build transition from -> to network state diff --git a/packages/protocol/src/protocol/ProvableTransactionHook.ts b/packages/protocol/src/protocol/ProvableTransactionHook.ts index 68fa3bedf..43280ff3c 100644 --- a/packages/protocol/src/protocol/ProvableTransactionHook.ts +++ b/packages/protocol/src/protocol/ProvableTransactionHook.ts @@ -1,28 +1,28 @@ import { NoConfig } from "@proto-kit/common"; -import { Signature } from "o1js"; +import { Field, Signature } from "o1js"; import { RuntimeTransaction } from "../model/transaction/RuntimeTransaction"; import { NetworkState } from "../model/network/NetworkState"; import { MethodPublicOutput } from "../model/MethodPublicOutput"; import { - TransactionProverPublicInput, TransactionProverState, TransactionProverTransactionArguments, } from "../prover/transaction/TransactionProvable"; import { TransitioningProtocolModule } from "./TransitioningProtocolModule"; -export type ProvableHookTransactionState = Pick< - TransactionProverPublicInput, - "transactionsHash" | "eternalTransactionsHash" | "incomingMessagesHash" ->; +export type ProvableHookTransactionState = { + transactionsHash: Field; + eternalTransactionsHash: Field; + incomingMessagesHash: Field; +}; export function toProvableHookTransactionState( state: Pick< TransactionProverState, "transactionList" | "eternalTransactionsList" | "incomingMessages" > -) { +): ProvableHookTransactionState { const { transactionList, eternalTransactionsList, incomingMessages } = state; return { transactionsHash: transactionList.commitment, diff --git a/packages/protocol/src/prover/accumulators/BlockHashList.ts b/packages/protocol/src/prover/accumulators/BlockHashList.ts new file mode 100644 index 000000000..b0e732e65 --- /dev/null +++ b/packages/protocol/src/prover/accumulators/BlockHashList.ts @@ -0,0 +1,117 @@ +import { Field, Struct } from "o1js"; + +import { DefaultProvableHashList } from "../../utils/ProvableHashList"; +import type { TransactionProverState } from "../transaction/TransactionProvable"; +import { NetworkState } from "../../model/network/NetworkState"; + +export class BundlePreimage extends Struct({ + preimage: Field, + fromStateTransitionsHash: Field, + fromWitnessedRootsHash: Field, +}) {} + +export class FieldTransition extends Struct({ + from: Field, + to: Field, +}) {} + +/** + * A bundle represents an ordered list of transactions and their evaluated effects. + * Specifically, this includes beforeTransaction, runtime and afterTransaction evaluation, + * but not block hooks. + */ +export class Bundle extends Struct({ + // Those are per-block trackers + networkStateHash: Field, + transactionsHash: Field, + + // Those are non-linear trackers that we assert later in the blockprover + pendingSTBatchesHash: FieldTransition, + witnessedRootsHash: FieldTransition, +}) {} + +/** + * This hash list collects an ordered list of Bundle instances. + * "Pushing" onto this list can mean either appending a new bundle or updating the + * bundle at the tip of this list, according to the following rules: + * The validated preimage (via checkLastBundleElement) is: + * - == commitment: A new bundle will be appended + * - something else: The preimage is the actual preimage, therefore as a operation, + * the old one will be popped (silently) and the updates bundle will be pushed, + * resulting in an semantic update of the tip. + */ +export class BundleHashList extends DefaultProvableHashList { + public constructor( + commitment: Field = Field(0), + // TODO Refactor this into preimage and "auxiliary batch information" - this is confusing + public preimage?: BundlePreimage + ) { + super(Bundle, commitment); + } + + /** Verifies this list's preimage against the prover's state + * The main impact this function has is that it makes the preimage trusted + * i.e. we can safely use it to add to the bundle/open a new bundle + */ + public checkLastBundleElement( + state: TransactionProverState, + networkState: NetworkState + ) { + const { preimage, fromWitnessedRootsHash, fromStateTransitionsHash } = + this.preimage!; + + // Check and append to bundlelist + const lastElement = new Bundle({ + networkStateHash: networkState.hash(), + transactionsHash: state.transactionList.commitment, + pendingSTBatchesHash: { + from: fromStateTransitionsHash, + to: state.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: fromWitnessedRootsHash, + to: state.witnessedRoots.commitment, + }, + }); + + const newBundle = this.commitment.equals(preimage); + this.witnessTip(preimage, lastElement) + .or(newBundle) + .assertTrue("Last element not valid"); + + newBundle + .implies(state.transactionList.isEmpty()) + .assertTrue("Transaction list not empty for new bundle"); + } + + /** + * This function pushes a new bundle onto this list or updates the bundle at + * the tip of this list, according to the rules of the preimage algorithms (see class docs) + */ + public addToBundle( + state: TransactionProverState, + networkState: NetworkState + ) { + const { preimage, fromWitnessedRootsHash, fromStateTransitionsHash } = + this.preimage!; + + const newElement = new Bundle({ + networkStateHash: networkState.hash(), + transactionsHash: state.transactionList.commitment, + pendingSTBatchesHash: { + from: fromStateTransitionsHash, + to: state.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: fromWitnessedRootsHash, + to: state.witnessedRoots.commitment, + }, + }); + + // We always overwrite here, the invariant is that the preimage is + // either the actual preimage in case of addition to the existing bundle + // or the current commitment in case of a new bundle + this.commitment = preimage; + this.push(newElement); + } +} diff --git a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts index 9a8250168..7226492da 100644 --- a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts +++ b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts @@ -52,7 +52,7 @@ export class WitnessedRootHashList extends DefaultProvableHashList Already covered in BlockProver + // (1) don't append if witnessedRoot == finalizedRoot -> Already covered in BlockProver // (2) don't append if preimage.push({ finalizedRoot, pendingSTBatchesHash }) == this.commitment const skipPush = preimageCheckList.commitment.equals(this.commitment); diff --git a/packages/protocol/src/prover/block/BlockProvable.ts b/packages/protocol/src/prover/block/BlockProvable.ts index dbcb311fe..c0396b177 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, Struct } from "o1js"; +import { Bool, Field, Proof, Provable, Struct } from "o1js"; import { CompilableModule, WithZkProgrammable } from "@proto-kit/common"; import { StateTransitionProof } from "../statetransition/StateTransitionProvable"; @@ -10,15 +10,68 @@ import { WitnessedRootHashList, WitnessedRootWitness, } from "../accumulators/WitnessedRootHashList"; -import { - TransactionProof, - TransactionProverState, - TransactionProverStateCommitments, -} from "../transaction/TransactionProvable"; +import { TransactionProof } from "../transaction/TransactionProvable"; +import { BundleHashList, FieldTransition } from "../accumulators/BlockHashList"; +import { NonMethods } from "../../utils/utils"; import { BlockHashMerkleTreeWitness } from "./accummulators/BlockHashMerkleTree"; -export class BlockProverState extends TransactionProverState { +export const BLOCK_ARGUMENT_BATCH_SIZE = 4; + +export class BlockArguments extends Struct({ + afterBlockRootWitness: WitnessedRootWitness, + transactionsHash: Field, + pendingSTBatchesHash: FieldTransition, + witnessedRootsHash: FieldTransition, + isDummy: Bool, +}) { + public static noop( + state: NonMethods>, + stateRoot: Field + ) { + return new BlockArguments({ + afterBlockRootWitness: { + witnessedRoot: stateRoot, + preimage: Field(0), + }, + transactionsHash: Field(0), + pendingSTBatchesHash: { + from: state.pendingSTBatches.commitment, + to: state.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: state.witnessedRoots.commitment, + to: state.witnessedRoots.commitment, + }, + isDummy: Bool(true), + }); + } +} + +export class BlockArgumentsBatch extends Struct({ + batch: Provable.Array(BlockArguments, BLOCK_ARGUMENT_BATCH_SIZE), +}) {} + +export class BlockProverState { + /** + * The network state which gives access to values such as blockHeight + * This value is the same for the whole batch (L2 block) + */ + bundleList: BundleHashList; + + /** + * A variant of the transactionsHash that is never reset. + * Thought for usage in the sequence state mempool. + * In comparison, transactionsHash restarts at 0 for every new block + */ + eternalTransactionsList: TransactionHashList; + + pendingSTBatches: AppliedBatchHashList; + + incomingMessages: MinaActionsHashList; + + witnessedRoots: WitnessedRootHashList; + /** * The current state root of the block prover */ @@ -32,50 +85,184 @@ export class BlockProverState extends TransactionProverState { blockNumber: Field; + blockWitness: BlockHashMerkleTreeWitness; + + networkState: NetworkState; + constructor(args: { - transactionList: TransactionHashList; networkState: NetworkState; eternalTransactionsList: TransactionHashList; pendingSTBatches: AppliedBatchHashList; - incomingMessages: MinaActionsHashList; - witnessedRoots: WitnessedRootHashList; stateRoot: Field; blockHashRoot: Field; blockNumber: Field; + bundleList: BundleHashList; + blockWitness: BlockHashMerkleTreeWitness; + witnessedRoots: WitnessedRootHashList; + incomingMessages: MinaActionsHashList; }) { - super(args); + this.bundleList = args.bundleList; + this.eternalTransactionsList = args.eternalTransactionsList; + this.pendingSTBatches = args.pendingSTBatches; this.stateRoot = args.stateRoot; this.blockHashRoot = args.blockHashRoot; this.blockNumber = args.blockNumber; + this.networkState = args.networkState; + this.blockWitness = args.blockWitness; + this.witnessedRoots = args.witnessedRoots; + this.incomingMessages = args.incomingMessages; } public toCommitments(): BlockProverPublicInput { return { - ...super.toCommitments(), + remainders: { + bundlesHash: this.bundleList.commitment, + pendingSTBatchesHash: this.pendingSTBatches.commitment, + witnessedRootsHash: this.witnessedRoots.commitment, + }, + eternalTransactionsHash: this.eternalTransactionsList.commitment, + incomingMessagesHash: this.incomingMessages.commitment, stateRoot: this.stateRoot, blockHashRoot: this.blockHashRoot, blockNumber: this.blockNumber, + networkStateHash: this.networkState.hash(), }; } - public static fromCommitments( + public static blockProverFromCommitments( publicInput: BlockProverPublicInput, - networkState: NetworkState + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness ): BlockProverState { return new BlockProverState({ - ...super.fromCommitments(publicInput, networkState), + bundleList: new BundleHashList(publicInput.remainders.bundlesHash), + eternalTransactionsList: new TransactionHashList( + publicInput.eternalTransactionsHash + ), + incomingMessages: new MinaActionsHashList( + publicInput.incomingMessagesHash + ), + pendingSTBatches: new AppliedBatchHashList( + publicInput.remainders.pendingSTBatchesHash + ), + witnessedRoots: new WitnessedRootHashList( + publicInput.remainders.witnessedRootsHash + ), stateRoot: publicInput.stateRoot, blockHashRoot: publicInput.blockHashRoot, blockNumber: publicInput.blockNumber, + networkState, + blockWitness, + }); + } + + public copy() { + return BlockProverState.fromFields(this.toFields()); + } + + public toFields() { + return [ + this.bundleList.commitment, + this.eternalTransactionsList.commitment, + this.pendingSTBatches.commitment, + this.incomingMessages.commitment, + this.witnessedRoots.commitment, + this.stateRoot, + this.blockHashRoot, + this.blockNumber, + ...NetworkState.toFields(this.networkState), + ...BlockHashMerkleTreeWitness.toFields(this.blockWitness), + ]; + } + + // TODO Unit test + public static fromFields(fields: Field[]) { + return new BlockProverState({ + bundleList: new BundleHashList(fields[0]), + eternalTransactionsList: new TransactionHashList(fields[1]), + pendingSTBatches: new AppliedBatchHashList(fields[2]), + incomingMessages: new MinaActionsHashList(fields[3]), + witnessedRoots: new WitnessedRootHashList(fields[4]), + stateRoot: fields[5], + blockHashRoot: fields[6], + blockNumber: fields[7], + networkState: new NetworkState(NetworkState.fromFields(fields.slice(8))), + blockWitness: new BlockHashMerkleTreeWitness( + BlockHashMerkleTreeWitness.fromFields( + fields.slice(8 + NetworkState.sizeInFields()) + ) + ), + }); + } + + public static choose( + condition: Bool, + a: BlockProverState, + b: BlockProverState + ) { + return new BlockProverState({ + bundleList: new BundleHashList( + Provable.if(condition, a.bundleList.commitment, b.bundleList.commitment) + ), + eternalTransactionsList: new TransactionHashList( + Provable.if( + condition, + a.eternalTransactionsList.commitment, + b.eternalTransactionsList.commitment + ) + ), + pendingSTBatches: new AppliedBatchHashList( + Provable.if( + condition, + a.pendingSTBatches.commitment, + b.pendingSTBatches.commitment + ) + ), + incomingMessages: new MinaActionsHashList( + Provable.if( + condition, + a.incomingMessages.commitment, + b.incomingMessages.commitment + ) + ), + witnessedRoots: new WitnessedRootHashList( + Provable.if( + condition, + a.witnessedRoots.commitment, + b.witnessedRoots.commitment + ) + ), + stateRoot: Provable.if(condition, a.stateRoot, b.stateRoot), + blockHashRoot: Provable.if(condition, a.blockHashRoot, b.blockHashRoot), + blockWitness: new BlockHashMerkleTreeWitness( + Provable.if( + condition, + BlockHashMerkleTreeWitness, + a.blockWitness, + b.blockWitness + ) + ), + blockNumber: Provable.if(condition, a.blockNumber, b.blockNumber), + networkState: new NetworkState( + Provable.if(condition, NetworkState, a.networkState, b.networkState) + ), }); } } export const BlockProverStateCommitments = { - ...TransactionProverStateCommitments, + 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( @@ -84,13 +271,9 @@ export class BlockProverPublicInput extends Struct( export class BlockProverPublicOutput extends Struct({ ...BlockProverStateCommitments, - closed: Bool, }) { - public equals(input: BlockProverPublicInput, closed: Bool): Bool { - const output2 = BlockProverPublicOutput.toFields({ - ...input, - closed, - }); + public equals(input: BlockProverPublicInput): Bool { + const output2 = BlockProverPublicOutput.toFields(input); const output1 = BlockProverPublicOutput.toFields(this); return output1 .map((value1, index) => value1.equals(output2[index])) @@ -103,14 +286,15 @@ export type BlockProof = Proof; export interface BlockProvable extends WithZkProgrammable, CompilableModule { - proveBlock: ( + proveBlockBatch: ( publicInput: BlockProverPublicInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, - deferSTs: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: TransactionProof + deferSTProof: Bool, + transactionProof: TransactionProof, + deferTransactionProof: Bool, + batch: BlockArgumentsBatch ) => Promise; merge: ( diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index ede4ea8c9..c110e28eb 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -6,9 +6,9 @@ import { CompileArtifact, CompileRegistry, log, - MAX_FIELD, PlainZkProgram, provableMethod, + reduceSequential, WithZkProgrammable, ZkProgrammable, } from "@proto-kit/common"; @@ -26,11 +26,11 @@ import { AfterBlockHookArguments, BeforeBlockHookArguments, ProvableBlockHook, - toProvableHookBlockState, + toAfterBlockHookArgument, + toBeforeBlockHookArgument, } from "../../protocol/ProvableBlockHook"; import { NetworkState } from "../../model/network/NetworkState"; import { assertEqualsIf } from "../../utils/utils"; -import { WitnessedRootWitness } from "../accumulators/WitnessedRootHashList"; import { StateServiceProvider } from "../../state/StateServiceProvider"; import { executeHooks } from "../utils"; import { @@ -39,6 +39,7 @@ import { TransactionProverPublicInput, TransactionProverPublicOutput, } from "../transaction/TransactionProvable"; +import { Bundle } from "../accumulators/BlockHashList"; import { BlockProvable, @@ -46,6 +47,8 @@ import { BlockProverPublicInput, BlockProverPublicOutput, BlockProverState, + BlockArgumentsBatch, + BlockArguments, } from "./BlockProvable"; import { BlockHashMerkleTreeWitness, @@ -57,17 +60,12 @@ const errors = { `${propertyName} not matching: ${step}`, propertyNotMatching: (propertyName: string) => `${propertyName} not matching`, - - stateRootNotMatching: (step: string) => - errors.propertyNotMatchingStep("StateRoots", step), - - transactionsHashNotMatching: (step: string) => - errors.propertyNotMatchingStep("Transactions hash", step), - - networkStateHashNotMatching: (step: string) => - errors.propertyNotMatchingStep("Network state hash", step), }; +type BlockHookArgument = T extends "before" + ? BeforeBlockHookArguments + : AfterBlockHookArguments; + export class BlockProverProgrammable extends ZkProgrammable< BlockProverPublicInput, BlockProverPublicOutput @@ -94,16 +92,16 @@ export class BlockProverProgrammable extends ZkProgrammable< return this.prover.areProofsEnabled; } - public async executeBlockHooks< - T extends BeforeBlockHookArguments | AfterBlockHookArguments, - >( + public async executeBlockHooks( + type: T, hook: ( module: ProvableBlockHook, networkState: NetworkState, - args: T + args: BlockHookArgument ) => Promise, - hookArguments: T, - inputNetworkState: NetworkState + hookArguments: BlockHookArgument, + inputNetworkState: NetworkState, + isDummy: Bool ) { const transaction = RuntimeTransaction.dummyTransaction(); const startingInputs = { @@ -111,26 +109,33 @@ export class BlockProverProgrammable extends ZkProgrammable< networkState: inputNetworkState, }; - return await executeHooks(startingInputs, async () => { - const executionContext = container.resolve(RuntimeMethodExecutionContext); - - return await this.blockHooks.reduce>( - async (networkStatePromise, blockHook) => { - const networkState = await networkStatePromise; - - // Setup context for potential calls to runtime methods. - // With the special case that we set the new networkstate for every hook - // We also have to put in a dummy transaction for network.transaction - executionContext.setup({ - transaction: RuntimeTransaction.dummyTransaction(), - networkState, - }); - - return await hook(blockHook, networkState, hookArguments); - }, - Promise.resolve(inputNetworkState) - ); - }); + return await executeHooks( + startingInputs, + `${type}Block`, + async () => { + const executionContext = container.resolve( + RuntimeMethodExecutionContext + ); + + return await this.blockHooks.reduce>( + async (networkStatePromise, blockHook) => { + const networkState = await networkStatePromise; + + // Setup context for potential calls to runtime methods. + // With the special case that we set the new networkstate for every hook + // We also have to put in a dummy transaction for network.transaction + executionContext.setup({ + transaction: RuntimeTransaction.dummyTransaction(), + networkState, + }); + + return await hook(blockHook, networkState, hookArguments); + }, + Promise.resolve(inputNetworkState) + ); + }, + isDummy + ); } public includeSTProof( @@ -216,102 +221,22 @@ export class BlockProverProgrammable extends ZkProgrammable< } @provableMethod() - public async proveBlock( + public async proveBlockBatch( publicInput: BlockProverPublicInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, deferSTProof: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: TransactionProof + transactionProof: TransactionProof, + deferTransactionProof: Bool, + batch: BlockArgumentsBatch ): Promise { - // 1. Make assertions about the inputs - publicInput.transactionsHash.assertEquals( - Field(0), - "Transactionshash has to start at 0" - ); - - // TransactionProof format checks - transactionProof.publicInput.networkStateHash.assertEquals( - transactionProof.publicOutput.networkStateHash, - "TransactionProof cannot alter the network state" - ); - - const state = BlockProverState.fromCommitments(publicInput, networkState); - - // Verify Transaction proof if it has at least 1 tx - i.e. the - // input and output doesn't match fully - // We have to compare the whole input and output because we can make no - // assumptions about the values, since it can be an arbitrary dummy-proof - const isEmptyTransition = TransactionProverPublicInput.equals( - transactionProof.publicOutput, - transactionProof.publicInput - ); - const skipTransactionProofVerification = isEmptyTransition; - const verifyTransactionProof = isEmptyTransition.not(); - log.provable.debug("VerifyIf TxProof", verifyTransactionProof); - transactionProof.verifyIf(verifyTransactionProof); - - // 2. Execute beforeBlock hooks - const beforeBlockArgs = toProvableHookBlockState(state); - const beforeBlockResult = await this.executeBlockHooks( - async (module, networkStateArg, args) => - await module.beforeBlock(networkStateArg, args), - beforeBlockArgs, - networkState - ); - - state.pendingSTBatches.push(beforeBlockResult.batch); - - // 4. Apply TX-type BlockProof - transactionProof.publicInput.networkStateHash - .equals(beforeBlockResult.result.hash()) - .or(skipTransactionProofVerification) - .assertTrue( - "TransactionProof networkstate hash not matching beforeBlock hook result" - ); - - // Check that the transaction proof's STs start after the beforeBlock hook - transactionProof.publicInput.pendingSTBatchesHash.assertEquals( - state.pendingSTBatches.commitment, - "Transaction proof doesn't start their STs after the beforeBlockHook" - ); - // Fast-forward the stBatchHashList to after all transactions appended - state.pendingSTBatches.commitment = - transactionProof.publicOutput.pendingSTBatchesHash; - - // Fast-forward block content commitments by the results of the aggregated transaction proof - // Implicitly, the 'from' values here are asserted against the publicInput, since the hashlists - // are created out of the public input - state.transactionList.fastForward({ - from: transactionProof.publicInput.transactionsHash, - to: transactionProof.publicOutput.transactionsHash, - }); - state.eternalTransactionsList.fastForward({ - from: transactionProof.publicInput.eternalTransactionsHash, - to: transactionProof.publicOutput.eternalTransactionsHash, - }); - state.incomingMessages.fastForward({ - from: transactionProof.publicInput.incomingMessagesHash, - to: transactionProof.publicOutput.incomingMessagesHash, - }); - - // Witness root - const isEmpty = state.pendingSTBatches.commitment.equals(0); - isEmpty - .implies(state.stateRoot.equals(afterBlockRootWitness.witnessedRoot)) - .assertTrue(); - - state.witnessedRoots.witnessRoot( - { - appliedBatchListState: state.pendingSTBatches.commitment, - root: afterBlockRootWitness.witnessedRoot, - }, - afterBlockRootWitness.preimage, - isEmpty.not() + publicInput.networkStateHash.assertEquals( + networkState.hash(), + "Network state not valid" ); - // 5. Calculate the new block tree hash + // Calculate the new block tree hash const blockIndex = blockWitness.calculateIndex(); blockIndex.assertEquals(publicInput.blockNumber); @@ -323,37 +248,70 @@ export class BlockProverProgrammable extends ZkProgrammable< "Supplied block hash witness not matching state root" ); - state.blockHashRoot = blockWitness.calculateRoot( - new BlockHashTreeEntry({ - block: { - index: blockIndex, - transactionListHash: state.transactionList.commitment, - }, - closed: Bool(true), - }).hash() + let state = BlockProverState.blockProverFromCommitments( + publicInput, + networkState, + blockWitness ); - // 6. Execute afterBlock hooks + // Prove blocks iteratively + state = await reduceSequential( + batch.batch, + async (current, block) => { + const result = await this.proveBlock(current.copy(), block); - // Switch state service to afterBlock one - this.stateServiceProvider.popCurrentStateService(); + this.stateServiceProvider.popCurrentStateService(); - const afterBlockHookArgs = toProvableHookBlockState(state); - const afterBlockResult = await this.executeBlockHooks( - async (module, networkStateArg, args) => - await module.afterBlock(networkStateArg, args), + return BlockProverState.choose(block.isDummy, current, result); + }, + state + ); + + // Verify Transaction proof if it has at least 1 tx and it isn't deferred + const verifyTransactionProof = deferTransactionProof + .not() + .and(state.bundleList.isEmpty().not()); + + transactionProof.verifyIf(verifyTransactionProof); + + // Fast-forward transaction trackers by the results of the aggregated transaction proof + // Implicitly, the 'from' values here are asserted against the publicInput, since the hashlists + // are created out of the public input + state.eternalTransactionsList.fastForwardIf( { - ...afterBlockHookArgs, - stateRoot: afterBlockRootWitness.witnessedRoot, + from: transactionProof.publicInput.eternalTransactionsHash, + to: transactionProof.publicOutput.eternalTransactionsHash, }, - beforeBlockResult.result + verifyTransactionProof, + "eternalTransactionsList" ); - state.pendingSTBatches.push(afterBlockResult.batch); + state.incomingMessages.fastForwardIf( + { + from: transactionProof.publicInput.incomingMessagesHash, + to: transactionProof.publicOutput.incomingMessagesHash, + }, + verifyTransactionProof, + "incomingMessages" + ); - state.networkState = afterBlockResult.result; + // Cancel out remainders for transaction proof + assertEqualsIf( + transactionProof.publicInput.bundlesHash, + Field(0), + verifyTransactionProof, + "TransactionProof has to start bundles at 0" + ); - // 7. Close block + // Fast Backwards actually, but logic holds + state.bundleList.fastForwardIf( + { + from: transactionProof.publicOutput.bundlesHash, + to: state.bundleList.empty(), + }, + verifyTransactionProof, + "bundles hash" + ); // Verify ST Proof only if STs have been emitted, // and we don't defer the verification of the STs @@ -375,172 +333,174 @@ export class BlockProverProgrammable extends ZkProgrammable< state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; - state.blockNumber = blockIndex.add(1); - - return new BlockProverPublicOutput({ - ...state.toCommitments(), - closed: Bool(true), - }); + return new BlockProverPublicOutput(state.toCommitments()); } - @provableMethod() - public async merge( - publicInput: BlockProverPublicInput, - proof1: BlockProof, - proof2: BlockProof - ): Promise { - proof1.verify(); - proof2.verify(); + private async proveBlock( + state: BlockProverState, + args: BlockArguments + ): Promise { + const { networkState, blockWitness } = state; + const { afterBlockRootWitness, transactionsHash, isDummy } = args; - // Check state - publicInput.stateRoot.assertEquals( - proof1.publicInput.stateRoot, - errors.stateRootNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.stateRoot.assertEquals( - proof2.publicInput.stateRoot, - errors.stateRootNotMatching("proof1.to -> proof2.from") + // 1. Execute beforeBlock hooks + const beforeBlockArgs = toBeforeBlockHookArgument(state); + const beforeBlockResult = await this.executeBlockHooks( + "before", + async (module, networkStateArg, hookArgs) => + await module.beforeBlock(networkStateArg, hookArgs), + beforeBlockArgs, + networkState, + isDummy ); - // Check transaction list hash. - // Only assert them if these are tx proofs, skip for closed proofs - publicInput.transactionsHash - .equals(proof1.publicInput.transactionsHash) - .or(proof1.publicOutput.closed) - .assertTrue( - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.transactionsHash - .equals(proof2.publicInput.transactionsHash) - .or(proof1.publicOutput.closed) - .assertTrue( - errors.transactionsHashNotMatching("proof1.to -> proof2.from") - ); + state.pendingSTBatches.push(beforeBlockResult.batch); - // Check networkhash - publicInput.networkStateHash.assertEquals( - proof1.publicInput.networkStateHash, - errors.networkStateHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.networkStateHash.assertEquals( - proof2.publicInput.networkStateHash, - errors.networkStateHashNotMatching("proof1.to -> proof2.from") + // 2. "Apply" TX-type BlockProof + args.pendingSTBatchesHash.from.assertEquals( + state.pendingSTBatches.commitment ); + args.witnessedRootsHash.from.assertEquals(state.witnessedRoots.commitment); + const isEmptyBlock = transactionsHash.equals(Field(0)); + const isNotEmptyBlock = isEmptyBlock.not(); - // Check blockHashRoot - publicInput.blockHashRoot.assertEquals( - proof1.publicInput.blockHashRoot, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + // Check & fast-forward the stBatchHashList to after all transactions appended + state.pendingSTBatches.fastForward( + args.pendingSTBatchesHash, + "Transaction proof doesn't start their STs after the beforeBlockHook" ); - proof1.publicOutput.blockHashRoot.assertEquals( - proof2.publicInput.blockHashRoot, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + // Same for witnessedRootsHash + state.witnessedRoots.fastForward( + args.witnessedRootsHash, + "Transaction proof doesn't start with correct witnessed roots hash" ); - // Check eternalTransactionsHash - publicInput.eternalTransactionsHash.assertEquals( - proof1.publicInput.eternalTransactionsHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.eternalTransactionsHash.assertEquals( - proof2.publicInput.eternalTransactionsHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + // Add block to bundles list + const bundle = new Bundle({ + transactionsHash: transactionsHash, + networkStateHash: beforeBlockResult.result.hash(), + pendingSTBatchesHash: args.pendingSTBatchesHash, + witnessedRootsHash: args.witnessedRootsHash, + }); + Provable.log("Pushing", isNotEmptyBlock, bundle); + state.bundleList.pushIf(bundle, isNotEmptyBlock); + + // 3. + // Calculate new block tree root and increment witness + // Blocknumber as the index here is already authenticated previously + const [root, newWitness] = blockWitness.calculateRootIncrement( + state.blockNumber, + new BlockHashTreeEntry({ + block: { + index: state.blockNumber, + transactionListHash: transactionsHash, + }, + closed: Bool(true), + }).hash() ); - // Check incomingMessagesHash - publicInput.incomingMessagesHash.assertEquals( - proof1.publicInput.incomingMessagesHash, - errors.propertyNotMatchingStep( - "IncomingMessagesHash", - "publicInput.from -> proof1.from" - ) - ); - proof1.publicOutput.incomingMessagesHash.assertEquals( - proof2.publicInput.incomingMessagesHash, - errors.propertyNotMatchingStep( - "IncomingMessagesHash", - "proof1.to -> proof2.from" - ) - ); + state.blockHashRoot = root; + state.blockWitness = newWitness; - // Check pendingSTBatchesHash - publicInput.pendingSTBatchesHash.assertEquals( - proof1.publicInput.pendingSTBatchesHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.pendingSTBatchesHash.assertEquals( - proof2.publicInput.pendingSTBatchesHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + state.blockNumber = state.blockNumber.add(1); + + // 4. Execute afterBlock hooks + // Witness root + const isEmpty = state.pendingSTBatches.commitment.equals(0); + isEmpty + .implies(state.stateRoot.equals(afterBlockRootWitness.witnessedRoot)) + .assertTrue(); + + state.witnessedRoots.witnessRoot( + { + appliedBatchListState: state.pendingSTBatches.commitment, + root: afterBlockRootWitness.witnessedRoot, + }, + afterBlockRootWitness.preimage, + isEmpty.not() ); - // Check witnessedRootsHash - publicInput.witnessedRootsHash.assertEquals( - proof1.publicInput.witnessedRootsHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + // Switch state service to afterBlock one + this.stateServiceProvider.popCurrentStateService(); + + // Execute hooks + const afterBlockHookArgs = toAfterBlockHookArgument( + state, + afterBlockRootWitness.witnessedRoot, + transactionsHash ); - proof1.publicOutput.witnessedRootsHash.assertEquals( - proof2.publicInput.witnessedRootsHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") + const afterBlockResult = await this.executeBlockHooks( + "after", + async (module, networkStateArg, hookArgs) => + await module.afterBlock(networkStateArg, hookArgs), + { + ...afterBlockHookArgs, + }, + beforeBlockResult.result, + isDummy ); - // Assert closed indicator matches - // (i.e. we can only merge TX-Type and Block-Type with each other) - proof1.publicOutput.closed.assertEquals( - proof2.publicOutput.closed, - "Closed indicators not matching" - ); + // Apply state and network state changes + state.pendingSTBatches.push(afterBlockResult.batch); + state.networkState = afterBlockResult.result; - // Either - // blockNumbers are unset and proofs are unclosed or - // both blocks are closed, then they have to increment or - // one block is closed, then height has to be the same - - // Imperative algo would look like - // if(proof1.height == MAX && proof2.height == MAX){ - // assert !proof1.closed && !proof2.closed; - // }else if(proof1.closed && proof2.closed){ - // assert proof1.height + 1 == proof2.height - // // next one is omitted for now - // }else if(proof1.closed || proof2.closed{ - // assert proof1.height == proof2.height - // } - - const proof1Closed = proof1.publicOutput.closed; - const proof2Closed = proof2.publicOutput.closed; - - const blockNumberProgressionValid = publicInput.blockNumber - .equals(proof1.publicInput.blockNumber) - .and( - proof1.publicOutput.blockNumber.equals(proof2.publicInput.blockNumber) + return state; + } + + @provableMethod() + public async merge( + publicInput: BlockProverPublicInput, + proof1: BlockProof, + proof2: BlockProof + ): Promise { + proof1.verify(); + proof2.verify(); + + function checkProperty< + Key extends + | "stateRoot" + | "networkStateHash" + | "blockHashRoot" + | "eternalTransactionsHash" + | "incomingMessagesHash" + | "blockNumber", + >(key: Key) { + // Check state + publicInput[key].assertEquals( + proof1.publicInput[key], + errors.propertyNotMatchingStep(key, "publicInput.from -> proof1.from") + ); + proof1.publicOutput[key].assertEquals( + proof2.publicInput[key], + errors.propertyNotMatchingStep(key, "proof1.to -> proof2.from") + ); + } + + 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") + ); + } - // For tx proofs, we check that the progression starts and end with MAX - // in addition to that both proofs are non-closed - const isValidTransactionMerge = publicInput.blockNumber - .equals(MAX_FIELD) - .and(blockNumberProgressionValid) - .and(proof1Closed.or(proof2Closed).not()); - - const isValidClosedMerge = proof1Closed - .and(proof2Closed) - .and(blockNumberProgressionValid); - - isValidTransactionMerge - .or(isValidClosedMerge) - .assertTrue("Invalid BlockProof merge"); - - return new BlockProverPublicOutput({ - stateRoot: proof2.publicOutput.stateRoot, - transactionsHash: proof2.publicOutput.transactionsHash, - networkStateHash: proof2.publicOutput.networkStateHash, - blockHashRoot: proof2.publicOutput.blockHashRoot, - eternalTransactionsHash: proof2.publicOutput.eternalTransactionsHash, - incomingMessagesHash: proof2.publicOutput.incomingMessagesHash, - closed: isValidClosedMerge, - blockNumber: proof2.publicOutput.blockNumber, - pendingSTBatchesHash: proof2.publicOutput.pendingSTBatchesHash, - witnessedRootsHash: proof2.publicOutput.witnessedRootsHash, - }); + checkProperty("stateRoot"); + checkProperty("networkStateHash"); + checkProperty("blockHashRoot"); + checkProperty("eternalTransactionsHash"); + checkProperty("incomingMessagesHash"); + + checkRemainderProperty("bundlesHash"); + checkRemainderProperty("pendingSTBatchesHash"); + checkRemainderProperty("witnessedRootsHash"); + + return proof2.publicOutput; } /** @@ -555,7 +515,7 @@ export class BlockProverProgrammable extends ZkProgrammable< const { prover, stateTransitionProver, transactionProver } = this; const StateTransitionProofClass = stateTransitionProver.zkProgram[0].Proof; const TransactionProofClass = transactionProver.zkProgram[0].Proof; - const proveBlock = prover.proveBlock.bind(prover); + const proveBlockBatch = prover.proveBlockBatch.bind(prover); const merge = prover.merge.bind(prover); const program = ZkProgram({ @@ -564,33 +524,36 @@ export class BlockProverProgrammable extends ZkProgrammable< publicOutput: BlockProverPublicOutput, methods: { - proveBlock: { + proveBlockBatch: { privateInputs: [ NetworkState, BlockHashMerkleTreeWitness, StateTransitionProofClass, Bool, - WitnessedRootWitness, TransactionProofClass, + Bool, + BlockArgumentsBatch, ], async method( publicInput: BlockProverPublicInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, - deferSTs: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: BlockProof + deferSTProof: Bool, + transactionProof: TransactionProof, + deferTransactionProof: Bool, + batch: BlockArgumentsBatch ) { return { - publicOutput: await proveBlock( + publicOutput: await proveBlockBatch( publicInput, networkState, blockWitness, stateTransitionProof, - deferSTs, - afterBlockRootWitness, - transactionProof + deferSTProof, + transactionProof, + deferTransactionProof, + batch ), }; }, @@ -614,7 +577,7 @@ export class BlockProverProgrammable extends ZkProgrammable< }); const methods = { - proveBlock: program.proveBlock, + proveBlockBatch: program.proveBlockBatch, merge: program.merge, }; @@ -683,23 +646,25 @@ export class BlockProver }); } - public proveBlock( + public proveBlockBatch( publicInput: BlockProverPublicInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, stateTransitionProof: StateTransitionProof, - deferSTs: Bool, - afterBlockRootWitness: WitnessedRootWitness, - transactionProof: TransactionProof + deferSTProof: Bool, + transactionProof: TransactionProof, + deferTransactionProof: Bool, + batch: BlockArgumentsBatch ): Promise { - return this.zkProgrammable.proveBlock( + return this.zkProgrammable.proveBlockBatch( publicInput, networkState, blockWitness, stateTransitionProof, - deferSTs, - afterBlockRootWitness, - transactionProof + deferSTProof, + transactionProof, + deferTransactionProof, + batch ); } diff --git a/packages/protocol/src/prover/transaction/TransactionProvable.ts b/packages/protocol/src/prover/transaction/TransactionProvable.ts index 4180fd906..bb99ab0cc 100644 --- a/packages/protocol/src/prover/transaction/TransactionProvable.ts +++ b/packages/protocol/src/prover/transaction/TransactionProvable.ts @@ -10,6 +10,7 @@ import { TransactionHashList } from "../accumulators/TransactionHashList"; import { AppliedBatchHashList } from "../accumulators/AppliedBatchHashList"; import { MinaActionsHashList } from "../../utils/MinaPrefixedProvableHashList"; import { WitnessedRootHashList } from "../accumulators/WitnessedRootHashList"; +import { BundleHashList, BundlePreimage } from "../accumulators/BlockHashList"; export class TransactionProverState { /** @@ -22,7 +23,7 @@ export class TransactionProverState { * The network state which gives access to values such as blockHeight * This value is the same for the whole batch (L2 block) */ - networkState: NetworkState; + bundleList: BundleHashList; /** * A variant of the transactionsHash that is never reset. @@ -39,14 +40,14 @@ export class TransactionProverState { constructor(args: { transactionList: TransactionHashList; - networkState: NetworkState; + bundleList: BundleHashList; eternalTransactionsList: TransactionHashList; pendingSTBatches: AppliedBatchHashList; incomingMessages: MinaActionsHashList; witnessedRoots: WitnessedRootHashList; }) { this.transactionList = args.transactionList; - this.networkState = args.networkState; + this.bundleList = args.bundleList; this.eternalTransactionsList = args.eternalTransactionsList; this.pendingSTBatches = args.pendingSTBatches; this.incomingMessages = args.incomingMessages; @@ -55,51 +56,56 @@ export class TransactionProverState { public toCommitments(): TransactionProverPublicInput { return { - networkStateHash: this.networkState.hash(), - pendingSTBatchesHash: this.pendingSTBatches.commitment, - transactionsHash: this.transactionList.commitment, + bundlesHash: this.bundleList.commitment, + // pendingSTBatchesHash: this.pendingSTBatches.commitment, + // transactionsHash: this.transactionList.commitment, eternalTransactionsHash: this.eternalTransactionsList.commitment, incomingMessagesHash: this.incomingMessages.commitment, - witnessedRootsHash: this.witnessedRoots.commitment, + // witnessedRootsHash: this.witnessedRoots.commitment, }; } public static fromCommitments( publicInput: TransactionProverPublicInput, - networkState: NetworkState + args: TransactionProverArguments ): TransactionProverState { - publicInput.networkStateHash.assertEquals( - networkState.hash(), - "ExecutionData Networkstate doesn't equal public input hash" - ); - return new TransactionProverState({ - networkState, - transactionList: new TransactionHashList(publicInput.transactionsHash), + // Stuff that has to be authenticated via public input, since it's not inside the bundle hash + bundleList: new BundleHashList( + publicInput.bundlesHash, + args.bundleListPreimage + ), eternalTransactionsList: new TransactionHashList( publicInput.eternalTransactionsHash ), incomingMessages: new MinaActionsHashList( publicInput.incomingMessagesHash ), - pendingSTBatches: new AppliedBatchHashList( - publicInput.pendingSTBatchesHash - ), - witnessedRoots: new WitnessedRootHashList(publicInput.witnessedRootsHash), + // Remainders (i.e. stuff that goes into the bundle) + transactionList: new TransactionHashList(args.transactionHash), + pendingSTBatches: new AppliedBatchHashList(args.pendingSTBatchesHash), + witnessedRoots: new WitnessedRootHashList(args.witnessedRootsHash), }); } } +// These are all linear trackers, i.e. continuously progressing without +// interruptions from the block prover export const TransactionProverStateCommitments = { - transactionsHash: Field, - // Commitment to the list of unprocessed (pending) batches of STs that need to be proven - pendingSTBatchesHash: Field, - witnessedRootsHash: Field, - networkStateHash: Field, + bundlesHash: Field, eternalTransactionsHash: Field, incomingMessagesHash: Field, }; +export class TransactionProverArguments extends Struct({ + // Commitment to the list of unprocessed (pending) batches of STs that need to be proven + pendingSTBatchesHash: Field, + witnessedRootsHash: Field, + transactionHash: Field, + bundleListPreimage: BundlePreimage, + networkState: NetworkState, +}) {} + export class TransactionProverPublicInput extends Struct( TransactionProverStateCommitments ) { @@ -135,15 +141,9 @@ export class DynamicRuntimeProof extends DynamicProof< static maxProofsVerified = 0 as const; } -export class BlockProverSingleTransactionExecutionData extends Struct({ +export class TransactionProverExecutionData extends Struct({ transaction: TransactionProverTransactionArguments, - networkState: NetworkState, -}) {} - -export class BlockProverMultiTransactionExecutionData extends Struct({ - transaction1: TransactionProverTransactionArguments, - transaction2: TransactionProverTransactionArguments, - networkState: NetworkState, + args: TransactionProverArguments, }) {} export type TransactionProof = Proof< @@ -160,14 +160,15 @@ export interface TransactionProvable proveTransaction: ( publicInput: TransactionProverPublicInput, runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData + executionData: TransactionProverExecutionData ) => Promise; proveTransactions: ( publicInput: TransactionProverPublicInput, runtimeProof1: DynamicRuntimeProof, runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData ) => Promise; merge: ( diff --git a/packages/protocol/src/prover/transaction/TransactionProver.ts b/packages/protocol/src/prover/transaction/TransactionProver.ts index 6c686ba17..b74a3660f 100644 --- a/packages/protocol/src/prover/transaction/TransactionProver.ts +++ b/packages/protocol/src/prover/transaction/TransactionProver.ts @@ -32,11 +32,11 @@ import { } from "../block/accummulators/RuntimeVerificationKeyTree"; import { - BlockProverMultiTransactionExecutionData, - BlockProverSingleTransactionExecutionData, + TransactionProverExecutionData, DynamicRuntimeProof, TransactionProof, TransactionProvable, + TransactionProverArguments, TransactionProverPublicInput, TransactionProverPublicOutput, TransactionProverState, @@ -53,8 +53,8 @@ const errors = { transactionsHashNotMatching: (step: string) => errors.propertyNotMatchingStep("Transactions hash", step), - networkStateHashNotMatching: (step: string) => - errors.propertyNotMatchingStep("Network state hash", step), + bundlesHashNotMatching: (step: string) => + errors.propertyNotMatchingStep("Bundles hash", step), }; type ApplyTransactionArguments = Omit< @@ -62,6 +62,10 @@ type ApplyTransactionArguments = Omit< "verificationKeyAttestation" >; +type TransactionHookArgument = T extends "before" + ? BeforeTransactionHookArguments + : AfterTransactionHookArguments; + export class TransactionProverZkProgrammable extends ZkProgrammable< TransactionProverPublicInput, TransactionProverPublicOutput @@ -96,6 +100,7 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< * @param runtimeOutput * @param executionData * @param networkState + * @param bundleListPreimage * @returns The new BlockProver-state to be used as public output */ public async applyTransaction( @@ -118,13 +123,12 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< // Apply beforeTransaction hook state transitions const beforeBatch = await this.executeTransactionHooks( + "before", async (module, args) => await module.beforeTransaction(args), beforeTxHookArguments, isMessage ); - state = addTransactionToBundle(state, runtimeOutput.isMessage, transaction); - state.pendingSTBatches.push(beforeBatch); state.pendingSTBatches.push({ @@ -132,6 +136,8 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< applied: runtimeOutput.status, }); + state = addTransactionToBundle(state, runtimeOutput.isMessage, transaction); + // Apply afterTransaction hook state transitions const afterTxHookArguments = toAfterTransactionHookArgument( executionData, @@ -144,6 +150,7 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< this.stateServiceProvider.popCurrentStateService(); const afterBatch = await this.executeTransactionHooks( + "after", async (module, args) => await module.afterTransaction(args), afterTxHookArguments, isMessage @@ -170,14 +177,6 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< // Validate layout of transaction witness transaction.assertTransactionType(isMessage); - // Check network state integrity against appProof - state.networkState - .hash() - .assertEquals( - runtimeOutput.networkStateHash, - "Network state does not match state used in AppProof" - ); - return new TransactionProverState(state); } @@ -201,15 +200,18 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< return verificationKey; } - private async executeTransactionHooks< - T extends BeforeTransactionHookArguments | AfterTransactionHookArguments, - >( - hook: (module: ProvableTransactionHook, args: T) => Promise, - hookArguments: T, + private async executeTransactionHooks( + type: T, + hook: ( + module: ProvableTransactionHook, + args: TransactionHookArgument + ) => Promise, + hookArguments: TransactionHookArgument, isMessage: Bool ) { const { batch, rawStatus } = await executeHooks( hookArguments, + `${type}Transaction`, async () => { for (const module of this.transactionHooks) { // eslint-disable-next-line no-await-in-loop @@ -227,10 +229,15 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< } public async proveTransactionInternal( - fromState: TransactionProverState, + publicInput: TransactionProverPublicInput, runtimeProof: DynamicRuntimeProof, - { transaction, networkState }: BlockProverSingleTransactionExecutionData - ): Promise { + transaction: TransactionProverTransactionArguments, + args: TransactionProverArguments + ): Promise { + const state = TransactionProverState.fromCommitments(publicInput, args); + + state.bundleList.checkLastBundleElement(state, args.networkState); + const verificationKey = this.verifyVerificationKeyAttestation( transaction.verificationKeyAttestation, transaction.transaction.methodId @@ -238,32 +245,30 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< runtimeProof.verify(verificationKey); - return await this.applyTransaction( - fromState, + const result = await this.applyTransaction( + state, runtimeProof.publicOutput, transaction, - networkState + args.networkState ); + + result.bundleList.addToBundle(result, args.networkState); + + return result.toCommitments(); } @provableMethod() public async proveTransaction( publicInput: TransactionProverPublicInput, runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData + executionData: TransactionProverExecutionData ): Promise { - const state = TransactionProverState.fromCommitments( + return await this.proveTransactionInternal( publicInput, - executionData.networkState - ); - - const stateTo = await this.proveTransactionInternal( - state, runtimeProof, - executionData + executionData.transaction, + executionData.args ); - - return new TransactionProverPublicOutput(stateTo.toCommitments()); } @provableMethod() @@ -271,30 +276,25 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< publicInput: TransactionProverPublicInput, runtimeProof1: DynamicRuntimeProof, runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData ): Promise { - const state = TransactionProverState.fromCommitments( + const state1 = await this.proveTransactionInternal( publicInput, - executionData.networkState + runtimeProof1, + executionData1.transaction, + executionData1.args ); - // this.staticChecks(publicInput); - - const state1 = await this.proveTransactionInternal(state, runtimeProof1, { - transaction: executionData.transaction1, - networkState: executionData.networkState, - }); - // Switch to next state record for 2nd tx beforeTx hook - // TODO Can be prevented by merging 1st afterTx + 2nd beforeTx this.stateServiceProvider.popCurrentStateService(); - const stateTo = await this.proveTransactionInternal(state1, runtimeProof2, { - transaction: executionData.transaction2, - networkState: executionData.networkState, - }); - - return new TransactionProverPublicOutput(stateTo.toCommitments()); + return await this.proveTransactionInternal( + state1, + runtimeProof2, + executionData2.transaction, + executionData2.args + ); } @provableMethod() @@ -306,27 +306,14 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< proof1.verify(); proof2.verify(); - // Check transaction list hash. - // Only assert them if these are tx proofs, skip for closed proofs - publicInput.transactionsHash - .equals(proof1.publicInput.transactionsHash) - .assertTrue( - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.transactionsHash - .equals(proof2.publicInput.transactionsHash) - .assertTrue( - errors.transactionsHashNotMatching("proof1.to -> proof2.from") - ); - - // Check networkhash - publicInput.networkStateHash.assertEquals( - proof1.publicInput.networkStateHash, - errors.networkStateHashNotMatching("publicInput.from -> proof1.from") + // Check bundlesHash + publicInput.bundlesHash.assertEquals( + proof1.publicInput.bundlesHash, + errors.bundlesHashNotMatching("publicInput.from -> proof1.from") ); - proof1.publicOutput.networkStateHash.assertEquals( - proof2.publicInput.networkStateHash, - errors.networkStateHashNotMatching("proof1.to -> proof2.from") + proof1.publicOutput.bundlesHash.assertEquals( + proof2.publicInput.bundlesHash, + errors.bundlesHashNotMatching("proof1.to -> proof2.from") ); // Check eternalTransactionsHash @@ -356,32 +343,31 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< ); // Check pendingSTBatchesHash - publicInput.pendingSTBatchesHash.assertEquals( - proof1.publicInput.pendingSTBatchesHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.pendingSTBatchesHash.assertEquals( - proof2.publicInput.pendingSTBatchesHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") - ); - - // Check witnessedRootsHash - publicInput.witnessedRootsHash.assertEquals( - proof1.publicInput.witnessedRootsHash, - errors.transactionsHashNotMatching("publicInput.from -> proof1.from") - ); - proof1.publicOutput.witnessedRootsHash.assertEquals( - proof2.publicInput.witnessedRootsHash, - errors.transactionsHashNotMatching("proof1.to -> proof2.from") - ); + // publicInput.pendingSTBatchesHash.assertEquals( + // proof1.publicInput.pendingSTBatchesHash, + // errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + // ); + // proof1.publicOutput.pendingSTBatchesHash.assertEquals( + // proof2.publicInput.pendingSTBatchesHash, + // errors.transactionsHashNotMatching("proof1.to -> proof2.from") + // ); + // + // // Check witnessedRootsHash + // publicInput.witnessedRootsHash.assertEquals( + // proof1.publicInput.witnessedRootsHash, + // errors.transactionsHashNotMatching("publicInput.from -> proof1.from") + // ); + // proof1.publicOutput.witnessedRootsHash.assertEquals( + // proof2.publicInput.witnessedRootsHash, + // errors.transactionsHashNotMatching("proof1.to -> proof2.from") + // ); return new TransactionProverPublicOutput({ - transactionsHash: proof2.publicOutput.transactionsHash, - networkStateHash: proof2.publicOutput.networkStateHash, + bundlesHash: proof2.publicOutput.bundlesHash, eternalTransactionsHash: proof2.publicOutput.eternalTransactionsHash, incomingMessagesHash: proof2.publicOutput.incomingMessagesHash, - pendingSTBatchesHash: proof2.publicOutput.pendingSTBatchesHash, - witnessedRootsHash: proof2.publicOutput.witnessedRootsHash, + // pendingSTBatchesHash: proof2.publicOutput.pendingSTBatchesHash, + // witnessedRootsHash: proof2.publicOutput.witnessedRootsHash, }); } @@ -406,15 +392,12 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< methods: { proveTransaction: { - privateInputs: [ - DynamicRuntimeProof, - BlockProverSingleTransactionExecutionData, - ], + privateInputs: [DynamicRuntimeProof, TransactionProverExecutionData], async method( publicInput: TransactionProverPublicInput, runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData + executionData: TransactionProverExecutionData ) { return { publicOutput: await proveTransaction( @@ -430,21 +413,24 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< privateInputs: [ DynamicRuntimeProof, DynamicRuntimeProof, - BlockProverMultiTransactionExecutionData, + TransactionProverExecutionData, + TransactionProverExecutionData, ], async method( publicInput: TransactionProverPublicInput, runtimeProof1: DynamicRuntimeProof, runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData ) { return { publicOutput: await proveTransactions( publicInput, runtimeProof1, runtimeProof2, - executionData + executionData1, + executionData2 ), }; }, @@ -537,7 +523,7 @@ export class TransactionProver public proveTransaction( publicInput: TransactionProverPublicInput, runtimeProof: DynamicRuntimeProof, - executionData: BlockProverSingleTransactionExecutionData + executionData: TransactionProverExecutionData ): Promise { return this.zkProgrammable.proveTransaction( publicInput, @@ -550,13 +536,15 @@ export class TransactionProver publicInput: TransactionProverPublicInput, runtimeProof1: DynamicRuntimeProof, runtimeProof2: DynamicRuntimeProof, - executionData: BlockProverMultiTransactionExecutionData + executionData1: TransactionProverExecutionData, + executionData2: TransactionProverExecutionData ): Promise { return this.zkProgrammable.proveTransactions( publicInput, runtimeProof1, runtimeProof2, - executionData + executionData1, + executionData2 ); } diff --git a/packages/protocol/src/prover/utils.ts b/packages/protocol/src/prover/utils.ts index 5b67de0f1..611561baf 100644 --- a/packages/protocol/src/prover/utils.ts +++ b/packages/protocol/src/prover/utils.ts @@ -43,8 +43,10 @@ export function constructBatch( // TODO How does this interact with the RuntimeMethodExecutionContext when executing runtimemethods? export async function executeHooks( contextArguments: RuntimeMethodExecutionData, + hookName: string, method: () => Promise, - isMessage: Bool | undefined = undefined + // This can be either that the tx is a message, or we are inside a dummy block hook + skipEnforceStatus: Bool | undefined = undefined ) { const executionContext = container.resolve(RuntimeMethodExecutionContext); executionContext.clear(); @@ -63,16 +65,16 @@ export async function executeHooks( executionContext.current().result; // See https://github.com/proto-kit/framework/issues/321 for why we do this here - if (isMessage !== undefined) { + if (skipEnforceStatus !== undefined) { // isMessage is defined for all tx hooks status - .or(isMessage) + .or(skipEnforceStatus) .assertTrue( - `Transaction hook call failed for non-message tx: ${statusMessage ?? "-"}` + `${hookName} hook call failed for non-message tx: ${statusMessage ?? "-"}` ); } else { // isMessage is undefined for all block hooks - status.assertTrue(`Block hook call failed: ${statusMessage ?? "-"}`); + status.assertTrue(`${hookName} hook call failed: ${statusMessage ?? "-"}`); } return { diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts index cc6401c03..e37e35a60 100644 --- a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -1,5 +1,4 @@ import { - Bool, DeployArgs, DynamicProof, Field, @@ -199,14 +198,19 @@ export abstract class SettlementBase "OutputNetworkState witness not valid" ); - blockProof.publicOutput.closed.assertEquals( - Bool(true), - "Supplied proof is not a closed BlockProof" + // Check remainders are zero + blockProof.publicOutput.remainders.bundlesHash.assertEquals( + Field(0), + "Bundles list has not been fully proven" ); - blockProof.publicOutput.pendingSTBatchesHash.assertEquals( + 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" + ); // Execute onSettlementHooks for additional checks const stateRecord: SettlementStateRecord = { diff --git a/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts b/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts index eaa2ff8f8..bfa6cb98e 100644 --- a/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts +++ b/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts @@ -47,11 +47,15 @@ export class MinaPrefixedProvableHashList< public constructor( valueType: ProvablePure, public readonly prefix: string, - internalCommitment: Field = Field(0) + internalCommitment?: Field ) { super(valueType, internalCommitment); } + public empty(): Field { + return Field(0); + } + protected hash(elements: Field[]): Field { const init = salt(this.prefix); const digest = Poseidon.update(init, elements); @@ -60,7 +64,7 @@ export class MinaPrefixedProvableHashList< } export class MinaActionsHashList extends MinaPrefixedProvableHashList { - public constructor(internalCommitment: Field = Field(0)) { + public constructor(internalCommitment?: Field) { super(Field, MINA_PREFIXES.sequenceEvents, internalCommitment); } } diff --git a/packages/protocol/src/utils/PrefixedProvableHashList.ts b/packages/protocol/src/utils/PrefixedProvableHashList.ts index a31b78eea..4e9d4a755 100644 --- a/packages/protocol/src/utils/PrefixedProvableHashList.ts +++ b/packages/protocol/src/utils/PrefixedProvableHashList.ts @@ -15,6 +15,10 @@ export class PrefixedProvableHashList extends ProvableHashList { this.prefix = stringToField(prefix); } + public empty(): Field { + return Field(0); + } + protected hash(elements: Field[]): Field { return Poseidon.hash([this.prefix, ...elements]); } diff --git a/packages/protocol/src/utils/ProvableHashList.ts b/packages/protocol/src/utils/ProvableHashList.ts index 9b824d60a..1061b167b 100644 --- a/packages/protocol/src/utils/ProvableHashList.ts +++ b/packages/protocol/src/utils/ProvableHashList.ts @@ -23,13 +23,19 @@ export type VerifiedTransition = { * Utilities for creating a hash list from a given value type. */ export abstract class ProvableHashList { + public commitment: Field; + public constructor( protected readonly valueType: ProvablePure, - public commitment: Field = Field(0), + commitment?: Field | undefined, private unconstrainedList: Unconstrained< ProvableHashListData[] > = Unconstrained.from([]) - ) {} + ) { + this.commitment = commitment ?? this.empty(); + } + + protected abstract empty(): Field; protected abstract hash(elements: Field[]): Field; @@ -61,11 +67,24 @@ export abstract class ProvableHashList { this.commitment = to; } + public fastForwardIf( + transition: VerifiedTransition, + condition: Bool, + message: string = "some hashlist" + ) { + const { from, to } = transition; + + condition + .implies(from.equals(this.commitment)) + .assertTrue(`From-commitment for ${message} not matching`); + + this.commitment = Provable.if(condition, to, this.commitment); + } + public witnessTip(preimage: Field, value: Value): Bool { - return this.hash([ - this.commitment, - ...this.valueType.toFields(value), - ]).equals(this.commitment); + return this.hash([preimage, ...this.valueType.toFields(value)]).equals( + this.commitment + ); } /** @@ -111,6 +130,10 @@ export abstract class ProvableHashList { return this.commitment; } + public isEmpty(): Bool { + return this.commitment.equals(this.empty()); + } + public getUnconstrainedValues(): Unconstrained< ProvableHashListData[] > { @@ -122,4 +145,8 @@ export class DefaultProvableHashList extends ProvableHashList { public hash(elements: Field[]): Field { return Poseidon.hash(elements); } + + public empty(): Field { + return Field(0); + } } From 6bf6511e055041b40e3cde09711ea0bb4b7f61e5 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Fri, 9 Jan 2026 00:21:49 +0100 Subject: [PATCH 3/5] Adapted tracing to new block prover architecture --- .../sequencing/BlockProductionService.ts | 6 +- .../sequencing/TransactionExecutionService.ts | 11 +- .../production/tracing/BatchTracingService.ts | 119 ++++++++--- .../production/tracing/BlockTracingService.ts | 195 ++++++++++++------ .../tracing/TransactionTracingService.ts | 135 +++++------- 5 files changed, 277 insertions(+), 189 deletions(-) diff --git a/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts b/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts index 18010fa28..60fc710b5 100644 --- a/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts +++ b/packages/sequencer/src/protocol/production/sequencing/BlockProductionService.ts @@ -10,7 +10,7 @@ import { reduceStateTransitions, RuntimeTransaction, StateServiceProvider, - toProvableHookBlockState, + toBeforeBlockHookArgument, TransactionHashList, } from "@proto-kit/protocol"; import { Field } from "o1js"; @@ -128,7 +128,7 @@ export class BlockProductionService { // Get used networkState by executing beforeBlock() hooks const beforeHookResult = await this.executeBeforeBlockHook( - toProvableHookBlockState(blockState), + toBeforeBlockHookArgument(blockState), lastResult.afterNetworkState, stateService ); @@ -171,7 +171,7 @@ export class BlockProductionService { height: lastBlock.hash.toBigInt() !== 0n ? lastBlock.height.add(1) : Field(0), fromBlockHashRoot: Field(lastResult.blockHashRoot), - fromMessagesHash: lastBlock.toMessagesHash, + fromMessagesHash: lastBlock.fromMessagesHash, fromStateRoot: Field(lastResult.stateRoot), toMessagesHash: newBlockState.incomingMessages.commitment, previousBlockHash, diff --git a/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts b/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts index 24dc9852a..e3dc6ecdb 100644 --- a/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts +++ b/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts @@ -22,6 +22,7 @@ import { ProvableStateTransition, DefaultProvableHashList, addTransactionToBundle, + TransactionProverState, } from "@proto-kit/protocol"; import { Bool, Field } from "o1js"; import { AreProofsEnabled, log, mapSequential } from "@proto-kit/common"; @@ -58,12 +59,10 @@ export type RuntimeContextReducedExecutionResult = Pick< >; export type BlockTrackers = Pick< - BlockProverState, - | "transactionList" - | "eternalTransactionsList" - | "incomingMessages" - | "blockHashRoot" ->; + TransactionProverState, + "eternalTransactionsList" | "incomingMessages" | "transactionList" +> & + Pick; function getAreProofsEnabledFromModule( module: RuntimeModule diff --git a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts index f54d52e6f..f09e20710 100644 --- a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts @@ -1,29 +1,43 @@ -import { log, yieldSequential } from "@proto-kit/common"; +import { log, range, unzip, yieldSequential } from "@proto-kit/common"; import { AppliedBatchHashList, + BLOCK_ARGUMENT_BATCH_SIZE, MinaActionsHashList, TransactionHashList, WitnessedRootHashList, + BundleHashList, + BlockArguments, } from "@proto-kit/protocol"; import { inject, injectable } from "tsyringe"; +// eslint-disable-next-line import/no-extraneous-dependencies +import chunk from "lodash/chunk"; +import { Bool, Field } from "o1js"; import { StateTransitionProofParameters } from "../tasks/StateTransitionTask"; import { BlockWithResult } from "../../../storage/model/Block"; import { trace } from "../../../logging/trace"; import { Tracer } from "../../../logging/Tracer"; import { CachedLinkedLeafStore } from "../../../state/lmt/CachedLinkedLeafStore"; - import { - BlockTrace, - BlockTracingService, - BlockTracingState, -} from "./BlockTracingService"; + NewBlockArguments, + NewBlockProverParameters, +} from "../tasks/NewBlockTask"; + +import { BlockTracingService, BlockTracingState } from "./BlockTracingService"; import { StateTransitionTracingService } from "./StateTransitionTracingService"; +import { TransactionTrace } from "./TransactionTracingService"; + +type BatchTracingState = BlockTracingState; -type BatchTracingState = Omit; +export type BlockTrace = { + block: NewBlockProverParameters; + // Only for debugging and logging + heights: [string, string]; +}; export type BatchTrace = { blocks: BlockTrace[]; + transactions: TransactionTrace[]; stateTransitionTrace: StateTransitionProofParameters[]; }; @@ -40,12 +54,15 @@ export class BatchTracingService { return { pendingSTBatches: new AppliedBatchHashList(), witnessedRoots: new WitnessedRootHashList(), + bundleList: new BundleHashList(), stateRoot: block.block.fromStateRoot, eternalTransactionsList: new TransactionHashList( block.block.fromEternalTransactionsHash ), incomingMessages: new MinaActionsHashList(block.block.fromMessagesHash), networkState: block.block.networkState.before, + blockNumber: block.block.height, + blockHashRoot: block.block.fromBlockHashRoot, }; } @@ -57,25 +74,71 @@ export class BatchTracingService { // Trace blocks const numBlocks = blocks.length; + const numBatches = Math.ceil(numBlocks / BLOCK_ARGUMENT_BATCH_SIZE); + const [, blockTraces] = await yieldSequential( - blocks, - async (state, block, index) => { - const blockProverState: BlockTracingState = { - ...state, - transactionList: new TransactionHashList(), + 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 start = state.blockNumber.toString(); + + const [newState, combinedTraces] = await yieldSequential( + batch, + async (state2, block, jndex) => { + const [newState2, blockTrace, transactions] = + await this.blockTracingService.traceBlock(state2, block); + return [ + newState2, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + [blockTrace, transactions] as [ + NewBlockArguments, + TransactionTrace[], + ], + ]; + }, + state + ); + + // Fill up with dummies + const dummyBlockArgs = BlockArguments.noop( + newState, + Field(blocks.at(-1)!.result.stateRoot) + ); + const dummies = range( + blocks.length, + BLOCK_ARGUMENT_BATCH_SIZE + ).map(() => ({ + args: dummyBlockArgs, + startingStateAfterHook: {}, + startingStateBeforeHook: {}, + })); + + const [blockArgumentBatch, transactionTraces] = unzip(combinedTraces); + + const blockTrace: BlockTrace = { + block: { + ...batchTrace, + blocks: blockArgumentBatch.concat(dummies), + deferTransactionProof: Bool(numBatches - 1 < index), + deferSTProof: Bool(numBatches - 1 < index), + }, + heights: [start, newState.blockNumber.toString()], }; - const [newState, blockTrace] = - await this.blockTracingService.traceBlock( - blockProverState, - block, - index === numBlocks - 1 - ); - return [newState, blockTrace]; + + return [ + newState, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + [blockTrace, transactionTraces] as [BlockTrace, TransactionTrace[][]], + ]; }, batchState ); - return blockTraces; + return { + blockTraces: blockTraces.map(([x]) => x), + transactionTraces: blockTraces.map(([, x]) => x), + }; } @trace("batch.trace.transitions") @@ -102,20 +165,22 @@ export class BatchTracingService { batchId: number ): Promise { if (blocks.length === 0) { - return { blocks: [], stateTransitionTrace: [] }; + return { blocks: [], stateTransitionTrace: [], transactions: [] }; } // Traces the STs and the blocks in parallel, however not in separate processes // Therefore, we only optimize the idle time for async operations like DB reads - const [blockTraces, stateTransitionTrace] = await Promise.all([ - // Trace blocks - this.traceBlocks(blocks), - // Trace STs - this.traceStateTransitions(blocks, merkleTreeStore), - ]); + const [{ blockTraces, transactionTraces }, stateTransitionTrace] = + await Promise.all([ + // Trace blocks + this.traceBlocks(blocks), + // Trace STs + this.traceStateTransitions(blocks, merkleTreeStore), + ]); return { blocks: blockTraces, + transactions: transactionTraces.flat(2), stateTransitionTrace, }; } diff --git a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts index 8b6a98bbf..e0e8867c8 100644 --- a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts @@ -1,17 +1,24 @@ import { + BlockArguments, BlockProverPublicInput, BlockProverState, + Bundle, + TransactionHashList, + TransactionProverState, WitnessedRootWitness, + BundleHashList, + BundlePreimage, } from "@proto-kit/protocol"; import { Bool, Field } from "o1js"; import { toStateTransitionsHash } from "@proto-kit/module"; -import { yieldSequential } from "@proto-kit/common"; -// eslint-disable-next-line import/no-extraneous-dependencies -import chunk from "lodash/chunk"; +import { NonMethods, yieldSequential } from "@proto-kit/common"; import { inject, injectable } from "tsyringe"; import { BlockWithResult } from "../../../storage/model/Block"; -import type { NewBlockProverParameters } from "../tasks/NewBlockTask"; +import type { + NewBlockArguments, + NewBlockProverParameters, +} from "../tasks/NewBlockTask"; import { Tracer } from "../../../logging/Tracer"; import { trace } from "../../../logging/trace"; @@ -23,24 +30,10 @@ import { export type TaskStateRecord = Record; -export type BlockTracingState = Pick< - BlockProverState, - | "witnessedRoots" - | "stateRoot" - | "pendingSTBatches" - | "networkState" - | "transactionList" - | "eternalTransactionsList" - | "incomingMessages" +export type BlockTracingState = NonMethods< + Omit >; -export type BlockTrace = { - blockParams: NewBlockProverParameters; - transactions: TransactionTrace[]; - // Only for debugging and logging - height: string; -}; - @injectable() export class BlockTracingService { public constructor( @@ -49,38 +42,45 @@ export class BlockTracingService { public readonly tracer: Tracer ) {} - @trace("batch.trace.block", ([, block]) => ({ - height: block.block.height.toString(), - })) - public async traceBlock( + public openBlock( state: BlockTracingState, - block: BlockWithResult, - includeSTProof: boolean - ): Promise<[BlockTracingState, BlockTrace]> { + { block: firstBlock, result: firstResult }: BlockWithResult + ): Pick< + NewBlockProverParameters, + "publicInput" | "networkState" | "blockWitness" + > { const publicInput: BlockProverPublicInput = new BlockProverPublicInput({ stateRoot: state.stateRoot, - blockNumber: block.block.height, - blockHashRoot: block.block.fromBlockHashRoot, - eternalTransactionsHash: block.block.fromEternalTransactionsHash, - incomingMessagesHash: block.block.fromMessagesHash, - transactionsHash: Field(0), - networkStateHash: block.block.networkState.before.hash(), - witnessedRootsHash: state.witnessedRoots.commitment, - pendingSTBatchesHash: state.pendingSTBatches.commitment, + blockNumber: firstBlock.height, + blockHashRoot: firstBlock.fromBlockHashRoot, + eternalTransactionsHash: firstBlock.fromEternalTransactionsHash, + incomingMessagesHash: firstBlock.fromMessagesHash, + networkStateHash: firstBlock.networkState.before.hash(), + remainders: { + witnessedRootsHash: state.witnessedRoots.commitment, + pendingSTBatchesHash: state.pendingSTBatches.commitment, + bundlesHash: state.bundleList.commitment, + }, }); + return { + publicInput, + networkState: firstBlock.networkState.before, + blockWitness: firstResult.blockHashWitness, + }; + } + + @trace("batch.trace.block", ([, block]) => ({ + height: block.block.height.toString(), + })) + public async traceBlock( + state: BlockTracingState, + block: BlockWithResult + ): Promise<[BlockTracingState, NewBlockArguments, TransactionTrace[]]> { const startingStateBeforeHook = collectStartingState( block.block.beforeBlockStateTransitions ); - const blockTrace = { - publicInput, - networkState: block.block.networkState.before, - deferSTProof: Bool(!includeSTProof), - blockWitness: block.result.blockHashWitness, - startingStateBeforeHook, - } satisfies Partial; - state.pendingSTBatches.push({ batchHash: toStateTransitionsHash( block.block.beforeBlockStateTransitions @@ -89,26 +89,71 @@ export class BlockTracingService { }); state.networkState = block.block.networkState.during; + const blockArgsPartial = { + fromPendingSTBatchesHash: state.pendingSTBatches.commitment, + fromWitnessedRootsHash: state.witnessedRoots.commitment, + }; + + const transactionProverState = new TransactionProverState({ + transactionList: new TransactionHashList(), + witnessedRoots: state.witnessedRoots, + pendingSTBatches: state.pendingSTBatches, + incomingMessages: state.incomingMessages, + eternalTransactionsList: state.eternalTransactionsList, + bundleList: new BundleHashList( + state.bundleList.commitment, + // The preimage here is just the current state (the start of the block) + // Internally, both provers will detect commitment == preimage and start + // a new bundle + new BundlePreimage({ + preimage: state.bundleList.commitment, + fromStateTransitionsHash: state.pendingSTBatches.commitment, + fromWitnessedRootsHash: state.witnessedRoots.commitment, + }) + ), + }); + const [afterState, transactionTraces] = await yieldSequential( - chunk(block.block.transactions, 2), - async (input, [transaction1, transaction2]) => { + block.block.transactions, + async (input, transaction) => { const [output, transactionTrace] = - transaction2 !== undefined - ? await this.transactionTracing.createMultiTransactionTrace( - input, - transaction1, - transaction2 - ) - : await this.transactionTracing.createSingleTransactionTrace( - input, - transaction1 - ); + await this.transactionTracing.createTransactionTrace( + input, + state.networkState, + transaction + ); return [output, transactionTrace]; }, - state + transactionProverState + ); + + // TODO Maybe replace this with replicating the in-circuit version inside createTransactionTrace + // Add to bundleList (before all the afterBlock stuff since bundles only care about + // all the stuff that happens in the TransactionProver) + // Also, this list is a different instance than the one used in transaction tracing + const finishedBundle = new Bundle({ + networkStateHash: state.networkState.hash(), + transactionsHash: block.block.transactionsHash, + pendingSTBatchesHash: { + from: blockArgsPartial.fromPendingSTBatchesHash, + to: afterState.pendingSTBatches.commitment, + }, + witnessedRootsHash: { + from: blockArgsPartial.fromWitnessedRootsHash, + to: afterState.witnessedRoots.commitment, + }, + }); + state.bundleList.pushIf( + finishedBundle, + afterState.transactionList.isEmpty().not() ); + state.pendingSTBatches = afterState.pendingSTBatches; + state.witnessedRoots = afterState.witnessedRoots; + state.incomingMessages = afterState.incomingMessages; + state.eternalTransactionsList = afterState.eternalTransactionsList; + const preimage = afterState.witnessedRoots .getUnconstrainedValues() .get() @@ -119,6 +164,23 @@ export class BlockTracingService { preimage: preimage ?? Field(0), }; + // We create the batch here, because we need the afterBlockRootWitness, + // but the afterBlock's witnessed root can't be in the arguments, because + // it is temporally **after** the bundle, not inside it + const args = new BlockArguments({ + transactionsHash: afterState.transactionList.commitment, + afterBlockRootWitness, + witnessedRootsHash: { + from: blockArgsPartial.fromWitnessedRootsHash, + to: state.witnessedRoots.commitment, + }, + pendingSTBatchesHash: { + from: blockArgsPartial.fromPendingSTBatchesHash, + to: state.pendingSTBatches.commitment, + }, + isDummy: Bool(false), + }); + if (afterState.pendingSTBatches.commitment.equals(0).not().toBoolean()) { state.witnessedRoots.witnessRoot( { @@ -133,19 +195,24 @@ export class BlockTracingService { const startingStateAfterHook = collectStartingState( block.result.afterBlockStateTransitions ); + state.pendingSTBatches.push({ + batchHash: toStateTransitionsHash( + block.result.afterBlockStateTransitions + ), + applied: Bool(true), + }); state.networkState = block.result.afterNetworkState; + state.blockNumber = state.blockNumber.add(1); + return [ - afterState, + state, { - blockParams: { - ...blockTrace, - startingStateAfterHook, - afterBlockRootWitness, - }, - transactions: transactionTraces, - height: block.block.height.toString(), + args, + startingStateBeforeHook, + startingStateAfterHook, }, + transactionTraces, ]; } } diff --git a/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts b/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts index ee591d282..89e391e50 100644 --- a/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/TransactionTracingService.ts @@ -1,39 +1,30 @@ import { addTransactionToBundle, - BlockProverMultiTransactionExecutionData, - BlockProverPublicInput, - BlockProverSingleTransactionExecutionData, NetworkState, + TransactionProverArguments, + TransactionProverPublicInput, + TransactionProverState, TransactionProverTransactionArguments, } from "@proto-kit/protocol"; -import { Bool, Field } from "o1js"; -import { MAX_FIELD } from "@proto-kit/common"; +import { Bool } from "o1js"; import { toStateTransitionsHash } from "@proto-kit/module"; import { injectable } from "tsyringe"; import { TransactionExecutionResult } from "../../../storage/model/Block"; import { PendingTransaction } from "../../../mempool/PendingTransaction"; import type { RuntimeProofParameters } from "../tasks/RuntimeProvingTask"; -import { - TransactionProverTaskParameters, - TransactionProvingType, -} from "../tasks/serializers/types/TransactionProvingTypes"; +import { TransactionProverTaskParameters } from "../tasks/serializers/types/TransactionProvingTypes"; import { UntypedStateTransition } from "../helpers/UntypedStateTransition"; import { VerificationKeyService } from "../../runtime/RuntimeVerificationKeyService"; -import type { BlockTracingState, TaskStateRecord } from "./BlockTracingService"; - -export type TransactionTrace = - | { - type: TransactionProvingType.SINGLE; - transaction: TransactionProverTaskParameters; - runtime: [RuntimeProofParameters]; - } - | { - type: TransactionProvingType.MULTI; - transaction: TransactionProverTaskParameters; - runtime: [RuntimeProofParameters, RuntimeProofParameters]; - }; +import type { TaskStateRecord } from "./BlockTracingService"; + +export type TransactionTrace = { + transaction: TransactionProverTaskParameters; + runtime: RuntimeProofParameters; +}; + +export type TransactionTracingState = TransactionProverState; export function collectStartingState( stateTransitions: UntypedStateTransition[] @@ -76,23 +67,17 @@ export class TransactionTracingService { } private getTransactionProofPublicInput( - previousState: BlockTracingState - ): BlockProverPublicInput { + previousState: TransactionTracingState + ): TransactionProverPublicInput { return { - stateRoot: previousState.stateRoot, - transactionsHash: previousState.transactionList.commitment, + bundlesHash: previousState.bundleList.commitment, eternalTransactionsHash: previousState.eternalTransactionsList.commitment, incomingMessagesHash: previousState.incomingMessages.commitment, - networkStateHash: previousState.networkState.hash(), - witnessedRootsHash: previousState.witnessedRoots.commitment, - pendingSTBatchesHash: previousState.pendingSTBatches.commitment, - blockHashRoot: Field(0), - blockNumber: MAX_FIELD, }; } private appendTransactionToState( - previousState: BlockTracingState, + previousState: TransactionTracingState, transaction: TransactionExecutionResult ) { // TODO Remove this call and instead reuse results from sequencing @@ -128,7 +113,8 @@ export class TransactionTracingService { } private async traceTransaction( - previousState: BlockTracingState, + previousState: TransactionTracingState, + networkState: NetworkState, transaction: TransactionExecutionResult ) { const beforeHookStartingState = collectStartingState( @@ -137,90 +123,61 @@ export class TransactionTracingService { const runtimeTrace1 = this.createRuntimeProofParams( transaction, - previousState.networkState + networkState ); const afterHookStartingState = collectStartingState( transaction.stateTransitions[2].stateTransitions.flat() ); + const args: TransactionProverArguments = { + networkState: networkState, + transactionHash: previousState.transactionList.commitment, + pendingSTBatchesHash: previousState.pendingSTBatches.commitment, + witnessedRootsHash: previousState.witnessedRoots.commitment, + bundleListPreimage: previousState.bundleList.preimage!, + }; + const newState = this.appendTransactionToState(previousState, transaction); + newState.bundleList.addToBundle(newState, networkState); + return { state: newState, runtime: runtimeTrace1, startingState: [beforeHookStartingState, afterHookStartingState], + args, }; } - public async createSingleTransactionTrace( - previousState: BlockTracingState, + public async createTransactionTrace( + previousState: TransactionTracingState, + networkState: NetworkState, transaction: TransactionExecutionResult - ): Promise<[BlockTracingState, TransactionTrace]> { + ): Promise<[TransactionTracingState, TransactionTrace]> { const publicInput = this.getTransactionProofPublicInput(previousState); const { state: newState, startingState, runtime, - } = await this.traceTransaction(previousState, transaction); + args, + } = await this.traceTransaction(previousState, networkState, transaction); - const transactionTrace: TransactionProverTaskParameters = - { - executionData: { - transaction: await this.getTransactionData(transaction.tx), - networkState: previousState.networkState, - }, - startingState, - publicInput, - }; - - return [ - newState, - { - type: TransactionProvingType.SINGLE, - transaction: transactionTrace, - runtime: [runtime], + const transactionTrace: TransactionProverTaskParameters = { + executionData: { + transaction: await this.getTransactionData(transaction.tx), + args, }, - ]; - } - - public async createMultiTransactionTrace( - previousState: BlockTracingState, - transaction1: TransactionExecutionResult, - transaction2: TransactionExecutionResult - ): Promise<[BlockTracingState, TransactionTrace]> { - const publicInput = this.getTransactionProofPublicInput(previousState); - - const { - state: tmpState, - startingState: startingState1, - runtime: runtime1, - } = await this.traceTransaction(previousState, transaction1); - - const { - state: resultState, - startingState: startingState2, - runtime: runtime2, - } = await this.traceTransaction(tmpState, transaction2); - - const transactionTrace: TransactionProverTaskParameters = - { - executionData: { - transaction1: await this.getTransactionData(transaction1.tx), - transaction2: await this.getTransactionData(transaction2.tx), - networkState: previousState.networkState, - }, - startingState: [...startingState1, ...startingState2], - publicInput, - }; + startingState, + publicInput, + }; return [ - resultState, + newState, { - type: TransactionProvingType.MULTI, transaction: transactionTrace, - runtime: [runtime1, runtime2], + runtime, }, ]; } From 157135604cc57bcedc2e53ad13c922016d1beabd Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Fri, 9 Jan 2026 00:22:16 +0100 Subject: [PATCH 4/5] Adapted flows to new block prover architecture --- packages/common/src/utils.ts | 18 +- packages/common/test/trees/MerkleTree.test.ts | 2 +- packages/module/src/method/runtimeMethod.ts | 5 - .../test-integration/SequencerRestart.test.ts | 2 +- .../src/mempool/private/PrivateMempool.ts | 8 +- .../src/protocol/production/flow/BatchFlow.ts | 98 ++++----- .../src/protocol/production/flow/BlockFlow.ts | 82 ++++---- .../production/flow/TransactionFlow.ts | 65 +++--- .../protocol/production/tasks/NewBlockTask.ts | 53 +++-- .../tasks/TransactionProvingTask.ts | 24 ++- .../NewBlockProvingParametersSerializer.ts | 57 ++--- ...ansactionProvingTaskParameterSerializer.ts | 195 ++++++------------ .../types/TransactionProvingTypes.ts | 38 +--- .../src/worker/queue/LocalTaskQueue.ts | 8 +- .../test/integration/BlockProduction-test.ts | 2 +- 15 files changed, 293 insertions(+), 364 deletions(-) diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 0576aacb5..27a0a036c 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -87,7 +87,11 @@ export function yieldSequential( array, async ([state, collectedTargets], curr, index, arr) => { const [newState, addition] = await callbackfn(state, curr, index, arr); - return [newState, collectedTargets.concat(addition)]; + // The reason we wrap this in an array here is for a special case where Target is a tuple + // or array itself. In this case, js interprets by flattening the Value in the array + // (which it does when a function (like concat) uses a spread operator and the + // input is an array) + return [newState, collectedTargets.concat([addition])]; }, [initialValue, []] ); @@ -105,6 +109,18 @@ export function mapSequential( }, Promise.resolve([])); } +export function unzip(array: [A, B][]): [A[], B[]] { + const as = array.map(([a]) => a); + const bs = array.map(([, b]) => b); + return [as, bs]; +} + +export function assertSizeOneOrTwo(arr: T[]): asserts arr is [T] | [T, T] { + if (!(arr.length === 1 || arr.length === 2)) { + throw new Error("Given array not size 1 or 2"); + } +} + /** * Computes a dummy value for the given value type. * diff --git a/packages/common/test/trees/MerkleTree.test.ts b/packages/common/test/trees/MerkleTree.test.ts index 933e07504..b2c673eb8 100644 --- a/packages/common/test/trees/MerkleTree.test.ts +++ b/packages/common/test/trees/MerkleTree.test.ts @@ -1,5 +1,5 @@ import { beforeEach } from "@jest/globals"; -import { Field, Provable } from "o1js"; +import { Field } from "o1js"; import { createMerkleTree, diff --git a/packages/module/src/method/runtimeMethod.ts b/packages/module/src/method/runtimeMethod.ts index d53a9439c..ffa8145af 100644 --- a/packages/module/src/method/runtimeMethod.ts +++ b/packages/module/src/method/runtimeMethod.ts @@ -25,11 +25,6 @@ const errors = { runtimeNotProvided: (name: string) => new Error(`Runtime was not provided for module: ${name}`), - methodInputsNotProvided: () => - new Error( - "Method execution inputs not provided, provide them via context.inputs" - ), - runtimeNameNotSet: () => new Error("Runtime name was not set"), fieldNotConstant: (name: string) => diff --git a/packages/persistance/test-integration/SequencerRestart.test.ts b/packages/persistance/test-integration/SequencerRestart.test.ts index 940bc40ca..2864424b9 100644 --- a/packages/persistance/test-integration/SequencerRestart.test.ts +++ b/packages/persistance/test-integration/SequencerRestart.test.ts @@ -40,7 +40,7 @@ describe("sequencer restart", () => { }; const teardown = async () => { - await appChain.sequencer.resolve("Database").close(); + await appChain.close(); }; beforeAll(async () => { diff --git a/packages/sequencer/src/mempool/private/PrivateMempool.ts b/packages/sequencer/src/mempool/private/PrivateMempool.ts index e750060ea..ca82f42cc 100644 --- a/packages/sequencer/src/mempool/private/PrivateMempool.ts +++ b/packages/sequencer/src/mempool/private/PrivateMempool.ts @@ -7,11 +7,10 @@ import { import { container, inject } from "tsyringe"; import { AccountStateHook, - BlockHashMerkleTree, MandatoryProtocolModulesRecord, NetworkState, Protocol, - ProvableHookBlockState, + ProvableHookTransactionState, RuntimeMethodExecutionContext, RuntimeMethodExecutionData, StateServiceProvider, @@ -170,10 +169,7 @@ export class PrivateMempool // TODO This is not sound currently as the prover state changes all the time // in the actual blockprover. We need to properly simulate that - const proverState: ProvableHookBlockState = { - blockHashRoot: Field( - previousBlock?.result.blockHashRoot ?? BlockHashMerkleTree.EMPTY_ROOT - ), + const proverState: ProvableHookTransactionState = { eternalTransactionsHash: previousBlock?.block.toEternalTransactionsHash ?? Field(0), transactionsHash: previousBlock?.block.transactionsHash ?? Field(0), diff --git a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts index 63720c4fc..824970891 100644 --- a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts @@ -5,14 +5,9 @@ import { Protocol, StateTransitionProverPublicInput, StateTransitionProverPublicOutput, + TransactionProverPublicInput, } from "@proto-kit/protocol"; -import { - isFull, - mapSequential, - MAX_FIELD, - Nullable, - range, -} from "@proto-kit/common"; +import { isFull, mapSequential, Nullable } from "@proto-kit/common"; import { FlowCreator } from "../../../worker/flow/Flow"; import { NewBlockProvingParameters, NewBlockTask } from "../tasks/NewBlockTask"; @@ -33,7 +28,7 @@ export class BatchFlow { private readonly blockProvingTask: NewBlockTask, private readonly blockReductionTask: BlockReductionTask, private readonly stateTransitionFlow: StateTransitionFlow, - private readonly blockFlow: BlockFlow, + private readonly transactionFlow: BlockFlow, @inject("Protocol") private readonly protocol: Protocol, @inject("Tracer") @@ -42,7 +37,7 @@ export class BatchFlow { private isBlockProofsMergable(a: BlockProof, b: BlockProof): boolean { // TODO Proper replication of merge logic - const part1 = a.publicOutput.stateRoot + return a.publicOutput.stateRoot .equals(b.publicInput.stateRoot) .and(a.publicOutput.blockHashRoot.equals(b.publicInput.blockHashRoot)) .and( @@ -53,26 +48,7 @@ export class BatchFlow { b.publicInput.eternalTransactionsHash ) ) - .and(a.publicOutput.closed.equals(b.publicOutput.closed)) .toBoolean(); - - const proof1Closed = a.publicOutput.closed; - const proof2Closed = b.publicOutput.closed; - - const blockNumberProgressionValid = a.publicOutput.blockNumber.equals( - b.publicInput.blockNumber - ); - - const isValidTransactionMerge = a.publicInput.blockNumber - .equals(MAX_FIELD) - .and(blockNumberProgressionValid) - .and(proof1Closed.or(proof2Closed).not()); - - const isValidClosedMerge = proof1Closed - .and(proof2Closed) - .and(blockNumberProgressionValid); - - return part1 && isValidClosedMerge.or(isValidTransactionMerge).toBoolean(); } private async pushBlockInput( @@ -92,6 +68,14 @@ export class BatchFlow { ); } + private dummyTransactionProof() { + return this.protocol.transactionProver.zkProgrammable.zkProgram[0].Proof.dummy( + TransactionProverPublicInput.empty(), + TransactionProverPublicInput.empty(), + 2 + ); + } + @trace("batch.prove", ([, batchId]) => ({ batchId })) public async executeBatch(batch: BatchTrace, batchId: number) { const batchFlow = new ReductionTaskFlow( @@ -105,24 +89,14 @@ export class BatchFlow { this.flowCreator ); - const map: Record< - number, - Nullable - > = Object.fromEntries( - batch.blocks.map((blockTrace, i) => [ - i, - { - params: blockTrace.blockParams, - input1: undefined, - input2: undefined, - }, - ]) - ); + const lastBlockProofCollector: Nullable = { + params: batch.blocks.at(-1)!.block, + input1: undefined, + input2: undefined, + }; const dummySTProof = await this.dummySTProof(); - range(0, batch.blocks.length - 1).forEach((index) => { - map[index].input1 = dummySTProof; - }); + const dummyTransactionProof = await this.dummyTransactionProof(); // TODO Make sure we use deferErrorsTo to everywhere (preferably with a nice pattern) // Currently, a lot of errors just get eaten and the chain just halts with no @@ -131,18 +105,36 @@ export class BatchFlow { batch.stateTransitionTrace, batchId, async (proof) => { - const index = batch.blocks.length - 1; - map[index].input1 = proof; - await this.pushBlockInput(map[index], batchFlow); + lastBlockProofCollector.input1 = proof; + await this.pushBlockInput(lastBlockProofCollector, batchFlow); } ); - await mapSequential(batch.blocks, async (blockTrace, blockIndex) => { - await this.blockFlow.executeBlock(blockTrace, async (proof) => { - map[blockIndex].input2 = proof; - await this.pushBlockInput(map[blockIndex], batchFlow); - }); - }); + // TODO Proper height + await this.transactionFlow.createTransactionProof( + batch.blocks[0].heights[0], + batch.transactions, + async (proof) => { + lastBlockProofCollector.input2 = proof; + await this.pushBlockInput(lastBlockProofCollector, batchFlow); + } + ); + + // 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( + batch.blocks.slice(0, batch.blocks.length - 1), + async (blockTrace) => { + await this.pushBlockInput( + { + input1: dummySTProof, + input2: dummyTransactionProof, + params: blockTrace.block, + }, + batchFlow + ); + } + ); return await new Promise((res, rej) => { batchFlow.onCompletion(async (result) => res(result)); diff --git a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts index 542bd1ca3..527dd0833 100644 --- a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts @@ -7,16 +7,19 @@ import { 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"; import { TransactionProvingTask } from "../tasks/TransactionProvingTask"; -import { TransactionProvingTaskParameters } from "../tasks/serializers/types/TransactionProvingTypes"; import { FlowCreator } from "../../../worker/flow/Flow"; -import { BlockTrace } from "../tracing/BlockTracingService"; import { TransactionReductionTask } from "../tasks/TransactionReductionTask"; +import { TransactionTrace } from "../tracing/TransactionTracingService"; import { ReductionTaskFlow } from "./ReductionTaskFlow"; import { TransactionFlow } from "./TransactionFlow"; +// TODO Rename to TransactionFlow @injectable() @scoped(Lifecycle.ContainerScoped) export class BlockFlow { @@ -24,16 +27,16 @@ export class BlockFlow { private readonly flowCreator: FlowCreator, @inject("Protocol") private readonly protocol: Protocol, - private readonly transactionProvingTask: TransactionProvingTask, - private readonly transactionReductionTask: TransactionReductionTask, - private readonly transactionFlow: TransactionFlow + private readonly runtimeFlow: TransactionFlow, + private readonly transactionTask: TransactionProvingTask, + private readonly transactionMergeTask: TransactionReductionTask ) {} - private async dummyTransactionProof(trace: BlockTrace) { + private async dummyTransactionProof() { const publicInput = { - ...trace.blockParams.publicInput, - networkStateHash: Field(0), - transactionsHash: Field(0), + bundlesHash: Field(0), + eternalTransactionsHash: Field(0), + incomingMessagesHash: Field(0), } satisfies TransactionProverPublicInput; // TODO Set publicInput.stateRoot to result after block hooks! @@ -48,24 +51,19 @@ export class BlockFlow { ); } - private async executeTransactions( - trace: BlockTrace - ): Promise< - ReductionTaskFlow - > { - const transactionFlow = new ReductionTaskFlow( + private async proveTransactions(height: string, traces: TransactionTrace[]) { + const flow = new ReductionTaskFlow( { - name: `transactions-${trace.height}`, - inputLength: trace.transactions.length, - mappingTask: this.transactionProvingTask, - reductionTask: this.transactionReductionTask, - + name: `transaction-${height}`, + inputLength: Math.ceil(traces.length / 2), + mappingTask: this.transactionTask, + reductionTask: this.transactionMergeTask, mergableFunction: (a, b) => - a.publicOutput.transactionsHash - .equals(b.publicInput.transactionsHash) + a.publicOutput.eternalTransactionsHash + .equals(b.publicInput.eternalTransactionsHash) .and( - a.publicInput.networkStateHash.equals( - b.publicInput.networkStateHash + a.publicOutput.incomingMessagesHash.equals( + b.publicInput.incomingMessagesHash ) ) .toBoolean(), @@ -73,32 +71,30 @@ export class BlockFlow { this.flowCreator ); - await transactionFlow.flow.forEach( - trace.transactions, - async (transactionTrace, txIndex) => { - await this.transactionFlow.proveRuntimes( - transactionTrace, - trace.height, - txIndex, - async (parameters) => { - await transactionFlow.pushInput(parameters); - } - ); - } - ); + await mapSequential(chunk(traces, 2), async (traceChunk, index) => { + await this.runtimeFlow.proveRuntimes( + traceChunk, + height, + index, + async (result) => { + await flow.pushInput(result); + } + ); + }); - return transactionFlow; + return flow; } - public async executeBlock( - trace: BlockTrace, + public async createTransactionProof( + height: string, + trace: TransactionTrace[], callback: (proof: TransactionProof) => Promise ) { - if (trace.transactions.length === 0) { - const proof = await this.dummyTransactionProof(trace); + if (trace.length === 0) { + const proof = await this.dummyTransactionProof(); await callback(proof); } else { - const flow = await this.executeTransactions(trace); + const flow = await this.proveTransactions(height, trace); flow.onCompletion(async (result) => { await callback(result); }); diff --git a/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts b/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts index d124a352c..5d968758a 100644 --- a/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/TransactionFlow.ts @@ -1,10 +1,10 @@ import { injectable } from "tsyringe"; +import { assertSizeOneOrTwo } from "@proto-kit/common"; import { Flow, FlowCreator } from "../../../worker/flow/Flow"; import { RuntimeProof, TransactionProvingTaskParameters, - TransactionProvingType, } from "../tasks/serializers/types/TransactionProvingTypes"; import { RuntimeProvingTask } from "../tasks/RuntimeProvingTask"; import { TransactionTrace } from "../tracing/TransactionTracingService"; @@ -20,31 +20,31 @@ export class TransactionFlow { flow: Flow<{ runtimeProofs: { proof: RuntimeProof; index: number }[]; }>, - trace: TransactionTrace, + trace: [TransactionTrace] | [TransactionTrace, TransactionTrace], callback: (params: TransactionProvingTaskParameters) => Promise ) { - const requiredLength = trace.type === TransactionProvingType.MULTI ? 2 : 1; + const requiredLength = trace.length; if (flow.state.runtimeProofs.length === requiredLength) { let parameters: TransactionProvingTaskParameters; - if (trace.type === TransactionProvingType.MULTI) { + if (requiredLength === 2) { // Sort ascending const sorted = flow.state.runtimeProofs.sort( ({ index: a }, { index: b }) => a - b ); - parameters = { - type: trace.type, - parameters: trace.transaction, - proof1: sorted[0].proof, - proof2: sorted[1].proof, - }; + + parameters = [ + { parameters: trace[0].transaction, proof: sorted[0].proof }, + { parameters: trace[1].transaction, proof: sorted[1].proof }, + ]; } else { - parameters = { - type: trace.type, - parameters: trace.transaction, - proof1: flow.state.runtimeProofs[0].proof, - }; + parameters = [ + { + parameters: trace[0].transaction, + proof: flow.state.runtimeProofs[0].proof, + }, + ]; } await callback(parameters); @@ -52,13 +52,15 @@ export class TransactionFlow { } public async proveRuntimes( - trace: TransactionTrace, + trace: TransactionTrace[], blockHeight: string, txIndex: number, callback: (params: TransactionProvingTaskParameters) => Promise ) { + assertSizeOneOrTwo(trace); + const name = `transaction-${blockHeight}-${txIndex}${ - trace.type === TransactionProvingType.MULTI ? "-double" : "" + trace.length === 2 ? "-double" : "" }`; const flow = this.flowCreator.createFlow<{ runtimeProofs: { proof: RuntimeProof; index: number }[]; @@ -66,24 +68,17 @@ export class TransactionFlow { runtimeProofs: [], }); - await flow.pushTask( - this.runtimeProvingTask, - trace.runtime[0], - async (proof) => { - flow.state.runtimeProofs.push({ proof, index: 0 }); - await this.resolveTransactionFlow(flow, trace, callback); - } + await Promise.all( + trace.map(async (transaction, index) => { + await flow.pushTask( + this.runtimeProvingTask, + transaction.runtime, + async (proof) => { + flow.state.runtimeProofs.push({ proof, index }); + await this.resolveTransactionFlow(flow, trace, callback); + } + ); + }) ); - - if (trace.type === TransactionProvingType.MULTI) { - await flow.pushTask( - this.runtimeProvingTask, - trace.runtime[1], - async (proof) => { - flow.state.runtimeProofs.push({ proof, index: 1 }); - await this.resolveTransactionFlow(flow, trace, callback); - } - ); - } } } diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index 338ce9514..de5c15015 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -8,10 +8,11 @@ import { StateTransitionProvable, BlockHashMerkleTreeWitness, MandatoryProtocolModulesRecord, - WitnessedRootWitness, TransactionProof, BlockProof, TransactionProvable, + BlockArguments, + BlockArgumentsBatch, } from "@proto-kit/protocol"; import { Bool } from "o1js"; import { @@ -28,14 +29,19 @@ import type { TaskStateRecord } from "../tracing/BlockTracingService"; import { NewBlockProvingParametersSerializer } from "./serializers/NewBlockProvingParametersSerializer"; import { executeWithPrefilledStateService } from "./TransactionProvingTask"; +export type NewBlockArguments = { + args: BlockArguments; + startingStateBeforeHook: TaskStateRecord; + startingStateAfterHook: TaskStateRecord; +}; + export interface NewBlockProverParameters { publicInput: BlockProverPublicInput; networkState: NetworkState; blockWitness: BlockHashMerkleTreeWitness; deferSTProof: Bool; - afterBlockRootWitness: WitnessedRootWitness; - startingStateBeforeHook: TaskStateRecord; - startingStateAfterHook: TaskStateRecord; + deferTransactionProof: Bool; + blocks: NewBlockArguments[]; } export type NewBlockProvingParameters = PairingDerivedInput< @@ -96,32 +102,41 @@ export class NewBlockTask const { networkState, blockWitness, - startingStateBeforeHook, - startingStateAfterHook, publicInput, deferSTProof, - afterBlockRootWitness, + deferTransactionProof, + blocks, } = parameters; - await this.blockProver.proveBlock( - publicInput, - networkState, - blockWitness, - input1, - deferSTProof, - afterBlockRootWitness, - input2 - ); + const blockArgumentBatch = new BlockArgumentsBatch({ + batch: blocks.map((block) => block.args), + }); + + const stateRecords = blocks.flatMap((block) => [ + block.startingStateBeforeHook, + block.startingStateAfterHook, + ]); await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - [startingStateBeforeHook, startingStateAfterHook], - async () => {} + stateRecords, + async () => { + await this.blockProver.proveBlockBatch( + publicInput, + networkState, + blockWitness, + input1, + deferSTProof, + input2, + deferTransactionProof, + blockArgumentBatch + ); + } ); return await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - [startingStateBeforeHook, startingStateAfterHook], + stateRecords, async () => await this.executionContext.current().result.prove() ); diff --git a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts index de4b80b63..88c77d4db 100644 --- a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts @@ -21,10 +21,7 @@ import { TaskWorkerModule } from "../../../worker/worker/TaskWorkerModule"; import type { TaskStateRecord } from "../tracing/BlockTracingService"; import { TransactionProvingTaskParameterSerializer } from "./serializers/TransactionProvingTaskParameterSerializer"; -import { - TransactionProvingTaskParameters, - TransactionProvingType, -} from "./serializers/types/TransactionProvingTypes"; +import { TransactionProvingTaskParameters } from "./serializers/types/TransactionProvingTypes"; export async function executeWithPrefilledStateService( stateServiceProvider: StateServiceProvider, @@ -93,26 +90,31 @@ export class TransactionProvingTask public async compute( input: TransactionProvingTaskParameters ): Promise { + const startingState = input.flatMap((i) => i.parameters.startingState); + await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - input.parameters.startingState, + startingState, async () => { - const { type, parameters } = input; + const { parameters, proof } = input[0]; - const proof1 = DynamicRuntimeProof.fromProof(input.proof1); + const proof1 = DynamicRuntimeProof.fromProof(proof); - if (type === TransactionProvingType.SINGLE) { + if (input.length === 1) { await this.transactionProver.proveTransaction( parameters.publicInput, proof1, parameters.executionData ); } else { + const { parameters: parameters2, proof: proof2 } = input[1]; + await this.transactionProver.proveTransactions( parameters.publicInput, proof1, - DynamicRuntimeProof.fromProof(input.proof2), - parameters.executionData + DynamicRuntimeProof.fromProof(proof2), + parameters.executionData, + parameters2.executionData ); } } @@ -120,7 +122,7 @@ export class TransactionProvingTask return await executeWithPrefilledStateService( this.protocol.stateServiceProvider, - input.parameters.startingState, + startingState, async () => await this.executionContext.current().result.prove() ); diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts index 58771b1df..d0f8bc122 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts @@ -1,4 +1,5 @@ import { + BlockArguments, BlockHashMerkleTreeWitness, BlockProverPublicInput, NetworkState, @@ -9,7 +10,6 @@ import { TransactionProof, TransactionProverPublicInput, TransactionProverPublicOutput, - WitnessedRootWitness, } from "@proto-kit/protocol"; import { Bool } from "o1js"; @@ -30,10 +30,13 @@ interface JsonType { publicInput: ReturnType; networkState: ReturnType; blockWitness: ReturnType; - startingStateBeforeHook: JSONEncodableState; - startingStateAfterHook: JSONEncodableState; deferSTProof: boolean; - afterBlockRootWitness: ReturnType; + deferTransactionProof: boolean; + blocks: { + startingStateBeforeHook: JSONEncodableState; + startingStateAfterHook: JSONEncodableState; + args: ReturnType; + }[]; }; } @@ -71,19 +74,22 @@ export class NewBlockProvingParametersSerializer input.params.blockWitness ), - startingStateBeforeHook: DecodedStateSerializer.toJSON( - input.params.startingStateBeforeHook - ), + blocks: input.params.blocks.map((block) => { + return { + startingStateBeforeHook: DecodedStateSerializer.toJSON( + block.startingStateBeforeHook + ), - startingStateAfterHook: DecodedStateSerializer.toJSON( - input.params.startingStateAfterHook - ), + startingStateAfterHook: DecodedStateSerializer.toJSON( + block.startingStateAfterHook + ), - deferSTProof: input.params.deferSTProof.toBoolean(), + args: BlockArguments.toJSON(block.args), + }; + }), - afterBlockRootWitness: WitnessedRootWitness.toJSON( - input.params.afterBlockRootWitness - ), + deferSTProof: input.params.deferSTProof.toBoolean(), + deferTransactionProof: input.params.deferTransactionProof.toBoolean(), }, } satisfies JsonType); } @@ -108,19 +114,22 @@ export class NewBlockProvingParametersSerializer BlockHashMerkleTreeWitness.fromJSON(jsonObject.params.blockWitness) ), - startingStateBeforeHook: DecodedStateSerializer.fromJSON( - jsonObject.params.startingStateBeforeHook - ), + blocks: jsonObject.params.blocks.map((block) => { + return { + startingStateBeforeHook: DecodedStateSerializer.fromJSON( + block.startingStateBeforeHook + ), - startingStateAfterHook: DecodedStateSerializer.fromJSON( - jsonObject.params.startingStateBeforeHook - ), + startingStateAfterHook: DecodedStateSerializer.fromJSON( + block.startingStateBeforeHook + ), - deferSTProof: Bool(jsonObject.params.deferSTProof), + args: BlockArguments.fromJSON(block.args), + }; + }), - afterBlockRootWitness: WitnessedRootWitness.fromJSON( - jsonObject.params.afterBlockRootWitness - ), + deferSTProof: Bool(jsonObject.params.deferSTProof), + deferTransactionProof: Bool(jsonObject.params.deferTransactionProof), }, }; } diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts index 3e08b804e..2cb30a520 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts @@ -1,19 +1,20 @@ import { - BlockProverPublicInput, MethodPublicOutput, - NetworkState, ReturnType, RuntimeTransaction, + TransactionProverPublicInput, TransactionProverTransactionArguments, + TransactionProverArguments, } from "@proto-kit/protocol"; import { JsonProof, Signature } from "o1js"; +import { assertSizeOneOrTwo, mapSequential } from "@proto-kit/common"; import { TaskSerializer } from "../../../../worker/flow/Task"; import { ProofTaskSerializer } from "../../../../helpers/utils"; import { + TransactionProverTaskParameters, TransactionProvingTaskParameters, - TransactionProvingType, } from "./types/TransactionProvingTypes"; import { DecodedStateSerializer, @@ -21,6 +22,20 @@ import { } from "./DecodedStateSerializer"; import { RuntimeVerificationKeyAttestationSerializer } from "./RuntimeVerificationKeyAttestationSerializer"; +export type TransactionProvingTaskParametersJSON = { + parameters: TransactionProverTaskParametersJSON; + proof: JsonProof; +}[]; + +export type TransactionProverTaskParametersJSON = { + startingState: JSONEncodableState[]; + publicInput: ReturnType; + executionData: { + transaction: TransactionProverTransactionArgumentsJSON; + args: ReturnType; + }; +}; + export type TransactionProverTransactionArgumentsJSON = { transaction: ReturnType; signature: ReturnType; @@ -29,38 +44,6 @@ export type TransactionProverTransactionArgumentsJSON = { >; }; -export type SingleExecutionDataJSON = { - transaction: TransactionProverTransactionArgumentsJSON; - networkState: ReturnType; -}; - -export type MultiExecutionDataJSON = { - transaction1: TransactionProverTransactionArgumentsJSON; - transaction2: TransactionProverTransactionArgumentsJSON; - networkState: ReturnType; -}; - -export type TransactionProverTaskParametersJSON< - ExecutionData extends SingleExecutionDataJSON | MultiExecutionDataJSON, -> = { - startingState: JSONEncodableState[]; - publicInput: ReturnType; - executionData: ExecutionData; -}; - -export type TransactionProvingTaskParametersJSON = - | { - type: TransactionProvingType.SINGLE; - proof1: JsonProof; - parameters: TransactionProverTaskParametersJSON; - } - | { - type: TransactionProvingType.MULTI; - proof1: JsonProof; - proof2: JsonProof; - parameters: TransactionProverTaskParametersJSON; - }; - export class TransactionProvingTaskParameterSerializer implements TaskSerializer { @@ -100,61 +83,35 @@ export class TransactionProvingTaskParameterSerializer }; } - public toJSON(input: TransactionProvingTaskParameters): string { - let taskParamsJson: TransactionProvingTaskParametersJSON; + public toJSON(inputs: TransactionProvingTaskParameters): string { + const taskParamsJson: TransactionProvingTaskParametersJSON = inputs.map( + (input) => { + const { parameters, proof } = input; + const { executionData } = parameters; - const { type, parameters } = input; + const proofJSON = this.runtimeProofSerializer.toJSONProof(proof); - const partialParameters = { - publicInput: BlockProverPublicInput.toJSON(parameters.publicInput), + const parametersJSON: TransactionProverTaskParametersJSON = { + publicInput: TransactionProverPublicInput.toJSON( + parameters.publicInput + ), - startingState: parameters.startingState.map((stateRecord) => - DecodedStateSerializer.toJSON(stateRecord) - ), - }; + startingState: parameters.startingState.map((stateRecord) => + DecodedStateSerializer.toJSON(stateRecord) + ), - // The reason we can't just use the structs toJSON is that the VerificationKey - // toJSON and fromJSON isn't consistent -> i.e. the serialization doesn't work - // the same both ways. We fix that in our custom serializer - if (type === TransactionProvingType.SINGLE) { - const { executionData } = parameters; - const executionDataJson: SingleExecutionDataJSON = { - networkState: NetworkState.toJSON(executionData.networkState), - transaction: this.transactionProverArgumentsToJson( - executionData.transaction - ), - }; + executionData: { + args: TransactionProverArguments.toJSON(executionData.args), - taskParamsJson = { - type, - proof1: this.runtimeProofSerializer.toJSONProof(input.proof1), - parameters: { - ...partialParameters, - executionData: executionDataJson, - }, - }; - } else { - const { executionData } = parameters; - const executionDataJson: MultiExecutionDataJSON = { - networkState: NetworkState.toJSON(executionData.networkState), - transaction1: this.transactionProverArgumentsToJson( - executionData.transaction1 - ), - transaction2: this.transactionProverArgumentsToJson( - executionData.transaction2 - ), - }; + transaction: this.transactionProverArgumentsToJson( + executionData.transaction + ), + }, + }; - taskParamsJson = { - type, - proof1: this.runtimeProofSerializer.toJSONProof(input.proof1), - proof2: this.runtimeProofSerializer.toJSONProof(input.proof2), - parameters: { - ...partialParameters, - executionData: executionDataJson, - }, - }; - } + return { parameters: parametersJSON, proof: proofJSON }; + } + ); return JSON.stringify(taskParamsJson); } @@ -166,58 +123,36 @@ export class TransactionProvingTaskParameterSerializer const jsonReadyObject: TransactionProvingTaskParametersJSON = JSON.parse(json); - const { type, parameters } = jsonReadyObject; + const result = await mapSequential(jsonReadyObject, async (input) => { + const { parameters, proof } = input; - const partialParameters = { - publicInput: BlockProverPublicInput.fromJSON(parameters.publicInput), + const decodedProof = + await this.runtimeProofSerializer.fromJSONProof(proof); - startingState: parameters.startingState.map((stateRecord) => - DecodedStateSerializer.fromJSON(stateRecord) - ), - }; - - if (type === TransactionProvingType.SINGLE) { - return { - type, - proof1: await this.runtimeProofSerializer.fromJSONProof( - jsonReadyObject.proof1 + const decodedParameters: TransactionProverTaskParameters = { + publicInput: TransactionProverPublicInput.fromJSON( + parameters.publicInput + ), + startingState: parameters.startingState.map((stateRecord) => + DecodedStateSerializer.fromJSON(stateRecord) ), - parameters: { - ...partialParameters, - executionData: { - transaction: this.transactionProverArgumentsFromJson( - parameters.executionData.transaction - ), - networkState: new NetworkState( - NetworkState.fromJSON(parameters.executionData.networkState) - ), - }, - }, - }; - } - - return { - type, - proof1: await this.runtimeProofSerializer.fromJSONProof( - jsonReadyObject.proof1 - ), - proof2: await this.runtimeProofSerializer.fromJSONProof( - jsonReadyObject.proof2 - ), - parameters: { - ...partialParameters, executionData: { - transaction1: this.transactionProverArgumentsFromJson( - parameters.executionData.transaction1 - ), - transaction2: this.transactionProverArgumentsFromJson( - parameters.executionData.transaction2 + transaction: this.transactionProverArgumentsFromJson( + parameters.executionData.transaction ), - networkState: new NetworkState( - NetworkState.fromJSON(parameters.executionData.networkState) + args: TransactionProverArguments.fromJSON( + parameters.executionData.args ), }, - }, - }; + }; + return { + parameters: decodedParameters, + proof: decodedProof, + }; + }); + + assertSizeOneOrTwo(result); + + return result; } } 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 b091abdc2..0db92f4b7 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts @@ -1,8 +1,7 @@ import { - BlockProverMultiTransactionExecutionData, - BlockProverPublicInput, - BlockProverSingleTransactionExecutionData, MethodPublicOutput, + TransactionProverExecutionData, + TransactionProverPublicInput, } from "@proto-kit/protocol"; import { Proof } from "o1js"; @@ -10,30 +9,15 @@ import type { TaskStateRecord } from "../../../tracing/BlockTracingService"; export type RuntimeProof = Proof; -export enum TransactionProvingType { - SINGLE, - MULTI, -} - -export interface TransactionProverTaskParameters< - ExecutionData extends - | BlockProverSingleTransactionExecutionData - | BlockProverMultiTransactionExecutionData, -> { - publicInput: BlockProverPublicInput; - executionData: ExecutionData; +export interface TransactionProverTaskParameters { + publicInput: TransactionProverPublicInput; + executionData: TransactionProverExecutionData; startingState: TaskStateRecord[]; } -export type TransactionProvingTaskParameters = - | { - type: TransactionProvingType.SINGLE; - parameters: TransactionProverTaskParameters; - proof1: RuntimeProof; - } - | { - type: TransactionProvingType.MULTI; - parameters: TransactionProverTaskParameters; - proof1: RuntimeProof; - proof2: RuntimeProof; - }; +export type OneOrTwo = [Type] | [Type, Type]; + +export type TransactionProvingTaskParameters = OneOrTwo<{ + parameters: TransactionProverTaskParameters; + proof: RuntimeProof; +}>; diff --git a/packages/sequencer/src/worker/queue/LocalTaskQueue.ts b/packages/sequencer/src/worker/queue/LocalTaskQueue.ts index c6046477e..e325508ca 100644 --- a/packages/sequencer/src/worker/queue/LocalTaskQueue.ts +++ b/packages/sequencer/src/worker/queue/LocalTaskQueue.ts @@ -1,4 +1,4 @@ -import { log, mapSequential, noop } from "@proto-kit/common"; +import { log, mapSequential, noop, sleep } from "@proto-kit/common"; import { sequencerModule } from "../../sequencer/builder/SequencerModule"; import { TaskPayload } from "../flow/Task"; @@ -8,12 +8,6 @@ import { InstantiatedQueue, TaskQueue } from "./TaskQueue"; import { ListenerList } from "./ListenerList"; import { AbstractTaskQueue } from "./AbstractTaskQueue"; -async function sleep(ms: number) { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - // Had to extract it to here bc eslint would ruin the code interface QueueListener { (payload: TaskPayload): Promise; diff --git a/packages/sequencer/test/integration/BlockProduction-test.ts b/packages/sequencer/test/integration/BlockProduction-test.ts index 71c4172f9..62f29565c 100644 --- a/packages/sequencer/test/integration/BlockProduction-test.ts +++ b/packages/sequencer/test/integration/BlockProduction-test.ts @@ -418,7 +418,7 @@ export function testBlockProduction< const numberTxs = 3; - it("should produce block with multiple transaction", async () => { + it("should produce block with multiple transactions", async () => { log.setLevel("TRACE"); expect.assertions(6 + 4 * numberTxs); From e96b20890770da640476df3fe57b1071cd20fcb9 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Fri, 9 Jan 2026 00:42:25 +0100 Subject: [PATCH 5/5] Removed dormant log --- packages/protocol/src/prover/block/BlockProver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index c110e28eb..83485f3be 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -382,7 +382,6 @@ export class BlockProverProgrammable extends ZkProgrammable< pendingSTBatchesHash: args.pendingSTBatchesHash, witnessedRootsHash: args.witnessedRootsHash, }); - Provable.log("Pushing", isNotEmptyBlock, bundle); state.bundleList.pushIf(bundle, isNotEmptyBlock); // 3.