diff --git a/packages/common/src/trees/sparse/RollupMerkleTree.ts b/packages/common/src/trees/sparse/RollupMerkleTree.ts index c701a091..3cd90301 100644 --- a/packages/common/src/trees/sparse/RollupMerkleTree.ts +++ b/packages/common/src/trees/sparse/RollupMerkleTree.ts @@ -189,6 +189,8 @@ export function createMerkleTree(height: number): AbstractMerkleTreeClass { return hash; } + // TODO Make sure this implementation is as efficient as it gets. + // Especially compared to doing calculateRoot + witness new witness + check index public calculateRootIncrement( leafIndex: Field, leaf: Field diff --git a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts index 7226492d..9671c658 100644 --- a/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts +++ b/packages/protocol/src/prover/accumulators/WitnessedRootHashList.ts @@ -1,4 +1,4 @@ -import { Bool, Field, Struct } from "o1js"; +import { Bool, Field, Provable, Struct } from "o1js"; import { DefaultProvableHashList } from "../../utils/ProvableHashList"; @@ -13,7 +13,6 @@ export class WitnessedRoot extends Struct({ export class WitnessedRootWitness extends Struct({ witnessedRoot: Field, - preimage: Field, }) {} /** @@ -21,7 +20,10 @@ export class WitnessedRootWitness extends Struct({ */ export class WitnessedRootHashList extends DefaultProvableHashList { - public constructor(commitment: Field = Field(0)) { + public constructor( + commitment: Field = Field(0), + public preimage: Field = Field(0) + ) { super(WitnessedRoot, commitment); } @@ -34,20 +36,14 @@ export class WitnessedRootHashList extends DefaultProvableHashList value1.equals(output2[index])) + .reduce((a, b) => a.and(b)); + } + + public clone() { + return new BlockProverPublicInput( + BlockProverPublicInput.fromFields(BlockProverPublicInput.toFields(this)) + ); + } +} + +export const BlockProverStateCommitments = { + remainders: { + // Commitment to the list of unprocessed (pending) batches of STs that need to be proven + pendingSTBatchesHash: Field, + witnessedRootsHash: Field, + bundlesHash: Field, + witnessedRootsPreimage: Field, + }, + ...BlockProverStateBaseFields, +}; + +export class BlockProverStateInput extends Struct(BlockProverStateCommitments) { + public hash() { + return Poseidon.hash(BlockProverStateInput.toFields(this)); + } + + public static fromPublicInput(input: BlockProverPublicInput) { + return new BlockProverStateInput({ + remainders: { + bundlesHash: Field(0), + pendingSTBatchesHash: Field(0), + witnessedRootsHash: Field(0), + witnessedRootsPreimage: Field(0), + }, + eternalTransactionsHash: input.eternalTransactionsHash, + incomingMessagesHash: input.incomingMessagesHash, + stateRoot: input.stateRoot, + blockHashRoot: input.blockHashRoot, + blockNumber: input.blockNumber, + networkStateHash: input.networkStateHash, + }); + } + + public finalize(condition: Bool) { + condition + .implies( + this.remainders.bundlesHash + .equals(0) + .and(this.remainders.pendingSTBatchesHash.equals(0)) + .and(this.remainders.witnessedRootsHash.equals(0)) + ) + .assertTrue("Remainers not fully removed"); + + return new BlockProverPublicInput({ + proverStateRemainder: Field(0), + eternalTransactionsHash: this.eternalTransactionsHash, + incomingMessagesHash: this.incomingMessagesHash, + stateRoot: this.stateRoot, + blockHashRoot: this.blockHashRoot, + blockNumber: this.blockNumber, + networkStateHash: this.networkStateHash, + }); + } +} + +export class BlockProverPublicOutput extends BlockProverPublicInput {} + export class BlockProverState { /** * The network state which gives access to values such as blockHeight @@ -113,12 +198,13 @@ export class BlockProverState { this.incomingMessages = args.incomingMessages; } - public toCommitments(): BlockProverPublicInput { - return { + public toCommitments(): BlockProverStateInput { + return new BlockProverStateInput({ remainders: { bundlesHash: this.bundleList.commitment, pendingSTBatchesHash: this.pendingSTBatches.commitment, witnessedRootsHash: this.witnessedRoots.commitment, + witnessedRootsPreimage: this.witnessedRoots.preimage, }, eternalTransactionsHash: this.eternalTransactionsList.commitment, incomingMessagesHash: this.incomingMessages.commitment, @@ -126,31 +212,32 @@ export class BlockProverState { blockHashRoot: this.blockHashRoot, blockNumber: this.blockNumber, networkStateHash: this.networkState.hash(), - }; + }); } public static blockProverFromCommitments( - publicInput: BlockProverPublicInput, + stateInput: NonMethods, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness ): BlockProverState { return new BlockProverState({ - bundleList: new BundleHashList(publicInput.remainders.bundlesHash), + bundleList: new BundleHashList(stateInput.remainders.bundlesHash), eternalTransactionsList: new TransactionHashList( - publicInput.eternalTransactionsHash + stateInput.eternalTransactionsHash ), incomingMessages: new MinaActionsHashList( - publicInput.incomingMessagesHash + stateInput.incomingMessagesHash ), pendingSTBatches: new AppliedBatchHashList( - publicInput.remainders.pendingSTBatchesHash + stateInput.remainders.pendingSTBatchesHash ), witnessedRoots: new WitnessedRootHashList( - publicInput.remainders.witnessedRootsHash + stateInput.remainders.witnessedRootsHash, + stateInput.remainders.witnessedRootsPreimage ), - stateRoot: publicInput.stateRoot, - blockHashRoot: publicInput.blockHashRoot, - blockNumber: publicInput.blockNumber, + stateRoot: stateInput.stateRoot, + blockHashRoot: stateInput.blockHashRoot, + blockNumber: stateInput.blockNumber, networkState, blockWitness, }); @@ -167,6 +254,7 @@ export class BlockProverState { this.pendingSTBatches.commitment, this.incomingMessages.commitment, this.witnessedRoots.commitment, + this.witnessedRoots.preimage, this.stateRoot, this.blockHashRoot, this.blockNumber, @@ -182,14 +270,14 @@ export class BlockProverState { 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))), + witnessedRoots: new WitnessedRootHashList(fields[4], fields[5]), + stateRoot: fields[6], + blockHashRoot: fields[7], + blockNumber: fields[8], + networkState: new NetworkState(NetworkState.fromFields(fields.slice(9))), blockWitness: new BlockHashMerkleTreeWitness( BlockHashMerkleTreeWitness.fromFields( - fields.slice(8 + NetworkState.sizeInFields()) + fields.slice(9 + NetworkState.sizeInFields()) ) ), }); @@ -230,6 +318,11 @@ export class BlockProverState { condition, a.witnessedRoots.commitment, b.witnessedRoots.commitment + ), + Provable.if( + condition, + a.witnessedRoots.preimage, + b.witnessedRoots.preimage ) ), stateRoot: Provable.if(condition, a.stateRoot, b.stateRoot), @@ -250,51 +343,30 @@ export class BlockProverState { } } -export const BlockProverStateCommitments = { - remainders: { - // Commitment to the list of unprocessed (pending) batches of STs that need to be proven - pendingSTBatchesHash: Field, - witnessedRootsHash: Field, - bundlesHash: Field, - }, - eternalTransactionsHash: Field, - incomingMessagesHash: Field, - stateRoot: Field, - blockHashRoot: Field, - blockNumber: Field, - networkStateHash: Field, -}; - -export class BlockProverPublicInput extends Struct( - BlockProverStateCommitments -) {} - -export class BlockProverPublicOutput extends Struct({ - ...BlockProverStateCommitments, -}) { - public equals(input: BlockProverPublicInput): Bool { - const output2 = BlockProverPublicOutput.toFields(input); - const output1 = BlockProverPublicOutput.toFields(this); - return output1 - .map((value1, index) => value1.equals(output2[index])) - .reduce((a, b) => a.and(b)); - } -} - export type BlockProof = Proof; export interface BlockProvable extends WithZkProgrammable, CompilableModule { - proveBlockBatch: ( + proveBlockBatchNoProofs: ( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, - stateTransitionProof: StateTransitionProof, + batch: BlockArgumentsBatch, + finalize: Bool + ) => Promise; + + proveBlockBatchWithProofs: ( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, deferSTProof: Bool, - transactionProof: TransactionProof, deferTransactionProof: Bool, - batch: BlockArgumentsBatch + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ) => Promise; merge: ( diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 83485f3b..f03430c2 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -6,6 +6,7 @@ import { CompileArtifact, CompileRegistry, log, + NonMethods, PlainZkProgram, provableMethod, reduceSequential, @@ -42,13 +43,14 @@ import { import { Bundle } from "../accumulators/BlockHashList"; import { - BlockProvable, + BlockArguments, + BlockArgumentsBatch, BlockProof, + BlockProvable, BlockProverPublicInput, BlockProverPublicOutput, BlockProverState, - BlockArgumentsBatch, - BlockArguments, + BlockProverStateInput, } from "./BlockProvable"; import { BlockHashMerkleTreeWitness, @@ -220,57 +222,42 @@ export class BlockProverProgrammable extends ZkProgrammable< }; } - @provableMethod() - public async proveBlockBatch( - publicInput: BlockProverPublicInput, - networkState: NetworkState, - blockWitness: BlockHashMerkleTreeWitness, + private verifySTProof( + state: BlockProverState, stateTransitionProof: StateTransitionProof, - deferSTProof: Bool, - transactionProof: TransactionProof, - deferTransactionProof: Bool, - batch: BlockArgumentsBatch - ): Promise { - publicInput.networkStateHash.assertEquals( - networkState.hash(), - "Network state not valid" - ); - - // Calculate the new block tree hash - const blockIndex = blockWitness.calculateIndex(); - - blockIndex.assertEquals(publicInput.blockNumber); - - blockWitness - .calculateRoot(Field(0)) - .assertEquals( - publicInput.blockHashRoot, - "Supplied block hash witness not matching state root" - ); - - let state = BlockProverState.blockProverFromCommitments( - publicInput, - networkState, - blockWitness - ); - - // Prove blocks iteratively - state = await reduceSequential( - batch.batch, - async (current, block) => { - const result = await this.proveBlock(current.copy(), block); - - this.stateServiceProvider.popCurrentStateService(); + deferSTProof: Bool + ) { + // Verify ST Proof only if STs have been emitted, + // and we don't defer the verification of the STs + // otherwise we can input a dummy proof + const batchesEmpty = state.pendingSTBatches.commitment.equals(Field(0)); + const verifyStProof = deferSTProof.not().and(batchesEmpty.not()); + log.provable.debug("Verify STProof", verifyStProof); + stateTransitionProof.verifyIf(verifyStProof); - return BlockProverState.choose(block.isDummy, current, result); - }, - state + // Apply STProof if not deferred + const stateProofResult = this.includeSTProof( + stateTransitionProof, + verifyStProof, + state.stateRoot, + state.pendingSTBatches.commitment, + state.witnessedRoots.commitment ); + state.stateRoot = stateProofResult.stateRoot; + state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; + state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; + } + private verifyTransactionProof( + state: BlockProverState, + transactionProof: TransactionProof, + deferTransactionProof: Bool + ) { // Verify Transaction proof if it has at least 1 tx and it isn't deferred - const verifyTransactionProof = deferTransactionProof - .not() - .and(state.bundleList.isEmpty().not()); + const finalizeBlockProof = deferTransactionProof.not(); + const verifyTransactionProof = finalizeBlockProof.and( + state.bundleList.isEmpty().not() + ); transactionProof.verifyIf(verifyTransactionProof); @@ -312,28 +299,172 @@ export class BlockProverProgrammable extends ZkProgrammable< verifyTransactionProof, "bundles hash" ); + } - // Verify ST Proof only if STs have been emitted, - // and we don't defer the verification of the STs - // otherwise we can input a dummy proof - const batchesEmpty = state.pendingSTBatches.commitment.equals(Field(0)); - const verifyStProof = deferSTProof.not().and(batchesEmpty.not()); - log.provable.debug("Verify STProof", verifyStProof); - stateTransitionProof.verifyIf(verifyStProof); + private parseState( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness + ) { + const hasNoStateRemained = publicInput.proverStateRemainder.equals(0); + + // If the state is supplied as a witness, we check that it is equals the PI's stateHash + stateWitness + .hash() + .equals(publicInput.proverStateRemainder) + .or(hasNoStateRemained) + .assertTrue("Input state witness is invalid"); + + const stateInputs = Provable.if( + hasNoStateRemained, + BlockProverStateInput, + BlockProverStateInput.fromPublicInput(publicInput), + stateWitness + ); - // Apply STProof if not deferred - const stateProofResult = this.includeSTProof( + stateInputs.networkStateHash.assertEquals( + networkState.hash(), + "Network state not valid" + ); + + const state = BlockProverState.blockProverFromCommitments( + stateInputs, + networkState, + blockWitness + ); + + // Verify block witness validity + const blockIndex = blockWitness.calculateIndex(); + + blockIndex.assertEquals(stateInputs.blockNumber); + + blockWitness + .calculateRoot(Field(0)) + .assertEquals( + stateInputs.blockHashRoot, + "Supplied block hash witness not matching state root" + ); + + return state; + } + + private computeOutput( + publicInput: BlockProverPublicInput, + state: BlockProverState, + finalizeBlockProof: Bool + ) { + const finalizedOutput = state.toCommitments(); + + const deferredOutput = { + ...publicInput, + }; + deferredOutput.proverStateRemainder = finalizedOutput.hash(); + + return new BlockProverPublicOutput( + Provable.if( + finalizeBlockProof, + BlockProverPublicOutput, + finalizedOutput.finalize(finalizeBlockProof), + deferredOutput + ) + ); + } + + @provableMethod() + public async proveBlockBatchNoProofs( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + finalize: Bool + ) { + return await this.proveBlockBatch( + false, + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + Bool(true), + Bool(true), + finalize + ); + } + + @provableMethod() + public async proveBlockBatchWithProofs( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof + ) { + const finalize = deferTransactionProof.or(deferSTProof).not(); + + return await this.proveBlockBatch( + true, + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + deferSTProof, + deferTransactionProof, + finalize, stateTransitionProof, - verifyStProof, - state.stateRoot, - state.pendingSTBatches.commitment, - state.witnessedRoots.commitment + transactionProof ); - state.stateRoot = stateProofResult.stateRoot; - state.pendingSTBatches.commitment = stateProofResult.pendingSTBatchesHash; - state.witnessedRoots.commitment = stateProofResult.witnessedRootsHash; + } - return new BlockProverPublicOutput(state.toCommitments()); + public async proveBlockBatch( + doProofVerification: boolean, + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + deferSTProof: Bool, + deferTransactionProof: Bool, + finalize: Bool, + stateTransitionProof?: StateTransitionProof, + transactionProof?: TransactionProof + ): Promise { + let state = this.parseState( + publicInput, + stateWitness, + networkState, + blockWitness + ); + + // Prove blocks iteratively + state = await reduceSequential( + batch.batch, + async (current, block) => { + const result = await this.proveBlock(current.copy(), block); + + this.stateServiceProvider.popCurrentStateService(); + + return BlockProverState.choose(block.isDummy, current, result); + }, + state + ); + + if (doProofVerification) { + this.verifyTransactionProof( + state, + transactionProof!, + deferTransactionProof + ); + this.verifySTProof(state, stateTransitionProof!, deferSTProof); + } + + return this.computeOutput(publicInput, state, finalize); } private async proveBlock( @@ -343,6 +474,8 @@ export class BlockProverProgrammable extends ZkProgrammable< const { networkState, blockWitness } = state; const { afterBlockRootWitness, transactionsHash, isDummy } = args; + const startingPendingStBatches = state.pendingSTBatches.commitment; + // 1. Execute beforeBlock hooks const beforeBlockArgs = toBeforeBlockHookArgument(state); const beforeBlockResult = await this.executeBlockHooks( @@ -405,18 +538,18 @@ export class BlockProverProgrammable extends ZkProgrammable< // 4. Execute afterBlock hooks // Witness root - const isEmpty = state.pendingSTBatches.commitment.equals(0); - isEmpty - .implies(state.stateRoot.equals(afterBlockRootWitness.witnessedRoot)) - .assertTrue(); + const hasNoSTBatches = state.pendingSTBatches.commitment.equals( + startingPendingStBatches + ); + + // TODO Cover case when we witness root but pendingSTBatches is completely empty state.witnessedRoots.witnessRoot( { appliedBatchListState: state.pendingSTBatches.commitment, root: afterBlockRootWitness.witnessedRoot, }, - afterBlockRootWitness.preimage, - isEmpty.not() + hasNoSTBatches.not() ); // Switch state service to afterBlock one @@ -456,13 +589,7 @@ export class BlockProverProgrammable extends ZkProgrammable< proof2.verify(); function checkProperty< - Key extends - | "stateRoot" - | "networkStateHash" - | "blockHashRoot" - | "eternalTransactionsHash" - | "incomingMessagesHash" - | "blockNumber", + Key extends keyof NonMethods, >(key: Key) { // Check state publicInput[key].assertEquals( @@ -475,29 +602,12 @@ export class BlockProverProgrammable extends ZkProgrammable< ); } - function checkRemainderProperty< - Key extends "pendingSTBatchesHash" | "witnessedRootsHash" | "bundlesHash", - >(key: Key) { - // Check state - publicInput.remainders[key].assertEquals( - proof1.publicInput.remainders[key], - errors.propertyNotMatchingStep(key, "publicInput.from -> proof1.from") - ); - proof1.publicOutput.remainders[key].assertEquals( - proof2.publicInput.remainders[key], - errors.propertyNotMatchingStep(key, "proof1.to -> proof2.from") - ); - } - checkProperty("stateRoot"); checkProperty("networkStateHash"); checkProperty("blockHashRoot"); checkProperty("eternalTransactionsHash"); checkProperty("incomingMessagesHash"); - - checkRemainderProperty("bundlesHash"); - checkRemainderProperty("pendingSTBatchesHash"); - checkRemainderProperty("witnessedRootsHash"); + checkProperty("proverStateRemainder"); return proof2.publicOutput; } @@ -514,7 +624,9 @@ export class BlockProverProgrammable extends ZkProgrammable< const { prover, stateTransitionProver, transactionProver } = this; const StateTransitionProofClass = stateTransitionProver.zkProgram[0].Proof; const TransactionProofClass = transactionProver.zkProgram[0].Proof; - const proveBlockBatch = prover.proveBlockBatch.bind(prover); + const proveBlockBatchWithProofs = + prover.proveBlockBatchWithProofs.bind(prover); + const proveBlockBatchNoProofs = prover.proveBlockBatchNoProofs.bind(prover); const merge = prover.merge.bind(prover); const program = ZkProgram({ @@ -523,36 +635,68 @@ export class BlockProverProgrammable extends ZkProgrammable< publicOutput: BlockProverPublicOutput, methods: { - proveBlockBatch: { + proveBlockBatchWithProofs: { privateInputs: [ + BlockProverStateInput, NetworkState, BlockHashMerkleTreeWitness, - StateTransitionProofClass, + BlockArgumentsBatch, Bool, - TransactionProofClass, Bool, - BlockArgumentsBatch, + StateTransitionProofClass, + TransactionProofClass, ], async method( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, - stateTransitionProof: StateTransitionProof, + batch: BlockArgumentsBatch, deferSTProof: Bool, - transactionProof: TransactionProof, deferTransactionProof: Bool, - batch: BlockArgumentsBatch + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ) { return { - publicOutput: await proveBlockBatch( + publicOutput: await proveBlockBatchWithProofs( publicInput, + stateWitness, networkState, blockWitness, - stateTransitionProof, + batch, deferSTProof, - transactionProof, deferTransactionProof, - batch + stateTransitionProof, + transactionProof + ), + }; + }, + }, + + proveBlockBatchNoProofs: { + privateInputs: [ + BlockProverStateInput, + NetworkState, + BlockHashMerkleTreeWitness, + BlockArgumentsBatch, + Bool, + ], + async method( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, + finalize: Bool + ) { + return { + publicOutput: await proveBlockBatchNoProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + finalize ), }; }, @@ -576,7 +720,8 @@ export class BlockProverProgrammable extends ZkProgrammable< }); const methods = { - proveBlockBatch: program.proveBlockBatch, + proveBlockBatchWithProofs: program.proveBlockBatchWithProofs, + proveBlockBatchNoProofs: program.proveBlockBatchNoProofs, merge: program.merge, }; @@ -645,25 +790,45 @@ export class BlockProver }); } - public proveBlockBatch( + public proveBlockBatchNoProofs( publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, networkState: NetworkState, blockWitness: BlockHashMerkleTreeWitness, - stateTransitionProof: StateTransitionProof, + batch: BlockArgumentsBatch, + finalize: Bool + ): Promise { + return this.zkProgrammable.proveBlockBatchNoProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + batch, + finalize + ); + } + + public proveBlockBatchWithProofs( + publicInput: BlockProverPublicInput, + stateWitness: BlockProverStateInput, + networkState: NetworkState, + blockWitness: BlockHashMerkleTreeWitness, + batch: BlockArgumentsBatch, deferSTProof: Bool, - transactionProof: TransactionProof, deferTransactionProof: Bool, - batch: BlockArgumentsBatch + stateTransitionProof: StateTransitionProof, + transactionProof: TransactionProof ): Promise { - return this.zkProgrammable.proveBlockBatch( + return this.zkProgrammable.proveBlockBatchWithProofs( publicInput, + stateWitness, networkState, blockWitness, - stateTransitionProof, + batch, deferSTProof, - transactionProof, deferTransactionProof, - batch + stateTransitionProof, + transactionProof ); } diff --git a/packages/protocol/src/prover/transaction/TransactionProvable.ts b/packages/protocol/src/prover/transaction/TransactionProvable.ts index bb99ab0c..0803946a 100644 --- a/packages/protocol/src/prover/transaction/TransactionProvable.ts +++ b/packages/protocol/src/prover/transaction/TransactionProvable.ts @@ -171,6 +171,10 @@ export interface TransactionProvable executionData2: TransactionProverExecutionData ) => Promise; + dummy: ( + publicInput: TransactionProverPublicInput + ) => Promise; + merge: ( publicInput: TransactionProverPublicInput, proof1: TransactionProof, diff --git a/packages/protocol/src/prover/transaction/TransactionProver.ts b/packages/protocol/src/prover/transaction/TransactionProver.ts index c2fb917a..7d8291cf 100644 --- a/packages/protocol/src/prover/transaction/TransactionProver.ts +++ b/packages/protocol/src/prover/transaction/TransactionProver.ts @@ -302,6 +302,13 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< ); } + @provableMethod() + public async dummy( + publicInput: TransactionProverPublicInput + ): Promise { + return publicInput; + } + @provableMethod() public async merge( publicInput: TransactionProverPublicInput, @@ -367,6 +374,7 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< const proveTransaction = prover.proveTransaction.bind(prover); const proveTransactions = prover.proveTransactions.bind(prover); const merge = prover.merge.bind(prover); + const dummy = prover.dummy.bind(prover); const program = ZkProgram({ name: "TransactionProver", @@ -419,6 +427,13 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< }, }, + dummy: { + privateInputs: [], + async method(publicInput: TransactionProverPublicInput) { + return { publicOutput: await dummy(publicInput) }; + }, + }, + merge: { privateInputs: [ SelfProof< @@ -445,6 +460,7 @@ export class TransactionProverZkProgrammable extends ZkProgrammable< const methods = { proveTransaction: program.proveTransaction, proveTransactions: program.proveTransactions, + dummy: program.dummy, merge: program.merge, }; @@ -531,6 +547,10 @@ export class TransactionProver ); } + public dummy(publicInput: TransactionProverPublicInput) { + return this.zkProgrammable.dummy(publicInput); + } + public merge( publicInput: TransactionProverPublicInput, proof1: TransactionProof, diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts index e37e35a6..050543b5 100644 --- a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -199,17 +199,9 @@ export abstract class SettlementBase ); // Check remainders are zero - blockProof.publicOutput.remainders.bundlesHash.assertEquals( + blockProof.publicOutput.proverStateRemainder.assertEquals( Field(0), - "Bundles list has not been fully proven" - ); - blockProof.publicOutput.remainders.pendingSTBatchesHash.assertEquals( - Field(0), - "Supplied proof is has outstanding STs to be proven" - ); - blockProof.publicOutput.remainders.witnessedRootsHash.assertEquals( - Field(0), - "Supplied proof is has outstanding witnessed roots hashes to be proven" + "Supplied proof is has outstanding block prover state to be proven" ); // Execute onSettlementHooks for additional checks diff --git a/packages/protocol/src/utils/ProvableHashList.ts b/packages/protocol/src/utils/ProvableHashList.ts index b59da667..da96f7a2 100644 --- a/packages/protocol/src/utils/ProvableHashList.ts +++ b/packages/protocol/src/utils/ProvableHashList.ts @@ -79,9 +79,13 @@ export abstract class ProvableHashList { ) { const { from, to } = transition; - condition - .implies(from.equals(this.commitment)) - .assertTrue(`From-commitment for ${message} not matching`); + // Equal to condition -> (from == this.commitment) + from + .mul(condition.toField()) + .assertEquals( + this.commitment.mul(condition.toField()), + `From-commitment for ${message} not matching` + ); this.commitment = Provable.if(condition, to, this.commitment); } diff --git a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts index 82497089..e2267ec6 100644 --- a/packages/sequencer/src/protocol/production/flow/BatchFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BatchFlow.ts @@ -48,6 +48,11 @@ export class BatchFlow { b.publicInput.eternalTransactionsHash ) ) + .and( + a.publicOutput.proverStateRemainder.equals( + b.publicInput.proverStateRemainder + ) + ) .toBoolean(); } @@ -120,6 +125,8 @@ export class BatchFlow { } ); + // TODO Cover case where either 0 STs or 0 Transactions are in a batch + // Push all blocks except the last one with dummy proofs // except the last one, which will wait on the two proofs to complete await mapSequential( diff --git a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts index 527dd083..874a4f03 100644 --- a/packages/sequencer/src/protocol/production/flow/BlockFlow.ts +++ b/packages/sequencer/src/protocol/production/flow/BlockFlow.ts @@ -3,10 +3,7 @@ import { MandatoryProtocolModulesRecord, Protocol, TransactionProof, - TransactionProverPublicInput, - TransactionProverPublicOutput, } from "@proto-kit/protocol"; -import { Field } from "o1js"; import { mapSequential } from "@proto-kit/common"; // eslint-disable-next-line import/no-extraneous-dependencies import chunk from "lodash/chunk"; @@ -32,23 +29,21 @@ export class BlockFlow { private readonly transactionMergeTask: TransactionReductionTask ) {} + private dummyProof: TransactionProof | undefined = undefined; + private async dummyTransactionProof() { - const publicInput = { - bundlesHash: Field(0), - eternalTransactionsHash: Field(0), - incomingMessagesHash: Field(0), - } satisfies TransactionProverPublicInput; + if (this.dummyProof !== undefined) { + return this.dummyProof; + } - // TODO Set publicInput.stateRoot to result after block hooks! - const publicOutput = new TransactionProverPublicOutput({ - ...publicInput, + const flow = this.flowCreator.createFlow("transaction-dummy", undefined); + const dummy = await flow.withFlow(async (resolve) => { + await flow.pushTask(this.transactionTask, "dummy", async (result) => { + resolve(result); + }); }); - - return await this.protocol.transactionProver.zkProgrammable.zkProgram[0].Proof.dummy( - publicInput, - publicOutput, - 2 - ); + this.dummyProof = dummy; + return dummy; } private async proveTransactions(height: string, traces: TransactionTrace[]) { diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index de5c1501..dfa151ea 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -13,8 +13,10 @@ import { TransactionProvable, BlockArguments, BlockArgumentsBatch, + BlockProverStateInput, + BLOCK_ARGUMENT_BATCH_SIZE, } from "@proto-kit/protocol"; -import { Bool } from "o1js"; +import { Bool, Provable } from "o1js"; import { ProvableMethodExecutionContext, CompileRegistry, @@ -37,6 +39,7 @@ export type NewBlockArguments = { export interface NewBlockProverParameters { publicInput: BlockProverPublicInput; + stateWitness: BlockProverStateInput; networkState: NetworkState; blockWitness: BlockHashMerkleTreeWitness; deferSTProof: Bool; @@ -103,11 +106,16 @@ export class NewBlockTask networkState, blockWitness, publicInput, + stateWitness, deferSTProof, deferTransactionProof, blocks, } = parameters; + if (blocks.length !== BLOCK_ARGUMENT_BATCH_SIZE) { + throw new Error("Given block argument length not exactly batch size"); + } + const blockArgumentBatch = new BlockArgumentsBatch({ batch: blocks.map((block) => block.args), }); @@ -121,25 +129,43 @@ export class NewBlockTask this.protocol.stateServiceProvider, stateRecords, async () => { - await this.blockProver.proveBlockBatch( - publicInput, - networkState, - blockWitness, - input1, - deferSTProof, - input2, - deferTransactionProof, - blockArgumentBatch - ); + if (deferSTProof.toBoolean() && deferTransactionProof.toBoolean()) { + await this.blockProver.proveBlockBatchNoProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + blockArgumentBatch, + Bool(false) + // deferSTProof.or(deferTransactionProof) + ); + } else { + await this.blockProver.proveBlockBatchWithProofs( + publicInput, + stateWitness, + networkState, + blockWitness, + blockArgumentBatch, + deferSTProof, + deferTransactionProof, + input1, + input2 + ); + } } ); - return await executeWithPrefilledStateService( + const proof = await executeWithPrefilledStateService( this.protocol.stateServiceProvider, stateRecords, async () => await this.executionContext.current().result.prove() ); + + Provable.log("Input", proof.publicInput); + Provable.log("Output", proof.publicOutput); + + return proof; } public async prepare(): Promise { diff --git a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts index b7281c64..532a0d5d 100644 --- a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts @@ -9,10 +9,8 @@ import { StateTransitionProvable, StateTransitionProvableBatch, StateTransitionProverPublicInput, - StateTransitionProverPublicOutput, } from "@proto-kit/protocol"; import { - log, ProvableMethodExecutionContext, CompileRegistry, LinkedMerkleTreeWitness, @@ -66,16 +64,12 @@ export class StateTransitionTask public async compute( input: StateTransitionProofParameters ): Promise { - const output = await this.stateTransitionProver.proveBatch( + await this.stateTransitionProver.proveBatch( input.publicInput, input.batch, new MerkleWitnessBatch({ witnesses: input.merkleWitnesses.slice() }), input.batchState ); - log.debug("STTask public io:", { - input: StateTransitionProverPublicInput.toJSON(input.publicInput), - output: StateTransitionProverPublicOutput.toJSON(output), - }); return await this.executionContext .current() diff --git a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts index 88c77d4d..5f6896b9 100644 --- a/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/TransactionProvingTask.ts @@ -6,6 +6,7 @@ import { DynamicRuntimeProof, TransactionProvable, TransactionProof, + TransactionProverPublicInput, } from "@proto-kit/protocol"; import { Runtime } from "@proto-kit/module"; import { inject, injectable, Lifecycle, scoped } from "tsyringe"; @@ -87,9 +88,32 @@ export class TransactionProvingTask ); } + private async computeDummy(): Promise { + await executeWithPrefilledStateService( + this.protocol.stateServiceProvider, + [{}, {}], + async () => { + await this.transactionProver.dummy( + TransactionProverPublicInput.empty() + ); + } + ); + + return await executeWithPrefilledStateService( + this.protocol.stateServiceProvider, + [{}, {}], + async () => + await this.executionContext.current().result.prove() + ); + } + public async compute( input: TransactionProvingTaskParameters ): Promise { + if (input === "dummy") { + return await this.computeDummy(); + } + const startingState = input.flatMap((i) => i.parameters.startingState); await executeWithPrefilledStateService( diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts index d0f8bc12..3c59f730 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/NewBlockProvingParametersSerializer.ts @@ -2,6 +2,7 @@ import { BlockArguments, BlockHashMerkleTreeWitness, BlockProverPublicInput, + BlockProverStateInput, NetworkState, ReturnType, StateTransitionProof, @@ -28,6 +29,7 @@ interface JsonType { input2: string; params: { publicInput: ReturnType; + stateWitness: ReturnType; networkState: ReturnType; blockWitness: ReturnType; deferSTProof: boolean; @@ -68,6 +70,8 @@ export class NewBlockProvingParametersSerializer params: { publicInput: BlockProverPublicInput.toJSON(input.params.publicInput), + stateWitness: BlockProverStateInput.toJSON(input.params.stateWitness), + networkState: NetworkState.toJSON(input.params.networkState), blockWitness: BlockHashMerkleTreeWitness.toJSON( @@ -102,8 +106,12 @@ export class NewBlockProvingParametersSerializer input2: await this.transactionProofSerializer.fromJSON(jsonObject.input2), params: { - publicInput: BlockProverPublicInput.fromJSON( - jsonObject.params.publicInput + publicInput: new BlockProverPublicInput( + BlockProverPublicInput.fromJSON(jsonObject.params.publicInput) + ), + + stateWitness: new BlockProverStateInput( + BlockProverStateInput.fromJSON(jsonObject.params.stateWitness) ), networkState: new NetworkState( diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts index 2cb30a52..0f34205a 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/TransactionProvingTaskParameterSerializer.ts @@ -84,6 +84,10 @@ export class TransactionProvingTaskParameterSerializer } public toJSON(inputs: TransactionProvingTaskParameters): string { + if (inputs === "dummy") { + return "dummy"; + } + const taskParamsJson: TransactionProvingTaskParametersJSON = inputs.map( (input) => { const { parameters, proof } = input; @@ -119,6 +123,10 @@ export class TransactionProvingTaskParameterSerializer public async fromJSON( json: string ): Promise { + if (json === "dummy") { + return "dummy"; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const jsonReadyObject: TransactionProvingTaskParametersJSON = JSON.parse(json); diff --git a/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts b/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts index 0db92f4b..ae4a2ac2 100644 --- a/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts +++ b/packages/sequencer/src/protocol/production/tasks/serializers/types/TransactionProvingTypes.ts @@ -17,7 +17,9 @@ export interface TransactionProverTaskParameters { export type OneOrTwo = [Type] | [Type, Type]; -export type TransactionProvingTaskParameters = OneOrTwo<{ - parameters: TransactionProverTaskParameters; - proof: RuntimeProof; -}>; +export type TransactionProvingTaskParameters = + | "dummy" + | OneOrTwo<{ + parameters: TransactionProverTaskParameters; + proof: RuntimeProof; + }>; diff --git a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts index f09e2071..3df22720 100644 --- a/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BatchTracingService.ts @@ -72,6 +72,11 @@ export class BatchTracingService { const batchState = this.createBatchState(blocks[0]); + const publicInput = this.blockTracingService.openBatch( + batchState, + blocks[0] + ); + // Trace blocks const numBlocks = blocks.length; const numBatches = Math.ceil(numBlocks / BLOCK_ARGUMENT_BATCH_SIZE); @@ -80,9 +85,21 @@ export class BatchTracingService { chunk(blocks, BLOCK_ARGUMENT_BATCH_SIZE), async (state, batch, index) => { // Trace batch of blocks fitting in single proof - const batchTrace = this.blockTracingService.openBlock(state, batch[0]); + const partialBlockTrace = this.blockTracingService.openBlock( + state, + batch[0], + publicInput + ); const start = state.blockNumber.toString(); + // PI is taken for first chunk of batch, all others use the stateWitness + // Copy here because we need a fresh instance + const currentPublicInput = publicInput.clone(); + if (index > 0) { + currentPublicInput.proverStateRemainder = + partialBlockTrace.stateWitness.hash(); + } + const [newState, combinedTraces] = await yieldSequential( batch, async (state2, block, jndex) => { @@ -100,13 +117,15 @@ export class BatchTracingService { state ); + const [blockArgumentBatch, transactionTraces] = unzip(combinedTraces); + // Fill up with dummies const dummyBlockArgs = BlockArguments.noop( newState, Field(blocks.at(-1)!.result.stateRoot) ); const dummies = range( - blocks.length, + blockArgumentBatch.length, BLOCK_ARGUMENT_BATCH_SIZE ).map(() => ({ args: dummyBlockArgs, @@ -114,14 +133,13 @@ export class BatchTracingService { startingStateBeforeHook: {}, })); - const [blockArgumentBatch, transactionTraces] = unzip(combinedTraces); - const blockTrace: BlockTrace = { block: { - ...batchTrace, + ...partialBlockTrace, + publicInput: currentPublicInput, blocks: blockArgumentBatch.concat(dummies), - deferTransactionProof: Bool(numBatches - 1 < index), - deferSTProof: Bool(numBatches - 1 < index), + deferTransactionProof: Bool(numBatches - 1 !== index), + deferSTProof: Bool(numBatches - 1 !== index), }, heights: [start, newState.blockNumber.toString()], }; diff --git a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts index e0e8867c..98c02358 100644 --- a/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts +++ b/packages/sequencer/src/protocol/production/tracing/BlockTracingService.ts @@ -8,6 +8,7 @@ import { WitnessedRootWitness, BundleHashList, BundlePreimage, + BlockProverStateInput, } from "@proto-kit/protocol"; import { Bool, Field } from "o1js"; import { toStateTransitionsHash } from "@proto-kit/module"; @@ -42,29 +43,48 @@ export class BlockTracingService { public readonly tracer: Tracer ) {} - public openBlock( + public openBatch( state: BlockTracingState, { block: firstBlock, result: firstResult }: BlockWithResult + ) { + return new BlockProverPublicInput({ + stateRoot: state.stateRoot, + blockNumber: firstBlock.height, + blockHashRoot: firstBlock.fromBlockHashRoot, + eternalTransactionsHash: firstBlock.fromEternalTransactionsHash, + incomingMessagesHash: firstBlock.fromMessagesHash, + networkStateHash: firstBlock.networkState.before.hash(), + proverStateRemainder: Field(0), + }); + } + + public openBlock( + state: BlockTracingState, + { block: firstBlock, result: firstResult }: BlockWithResult, + batchInput: BlockProverPublicInput ): Pick< NewBlockProverParameters, - "publicInput" | "networkState" | "blockWitness" + "stateWitness" | "networkState" | "blockWitness" > { - const publicInput: BlockProverPublicInput = new BlockProverPublicInput({ + const stateWitness = new BlockProverStateInput({ stateRoot: state.stateRoot, blockNumber: firstBlock.height, blockHashRoot: firstBlock.fromBlockHashRoot, - eternalTransactionsHash: firstBlock.fromEternalTransactionsHash, - incomingMessagesHash: firstBlock.fromMessagesHash, networkStateHash: firstBlock.networkState.before.hash(), + // The next two are properties that we fast-forward only after tx proofs are verified + // Therefore those don't change over multiple block batches + eternalTransactionsHash: batchInput.eternalTransactionsHash, + incomingMessagesHash: batchInput.incomingMessagesHash, remainders: { - witnessedRootsHash: state.witnessedRoots.commitment, pendingSTBatchesHash: state.pendingSTBatches.commitment, bundlesHash: state.bundleList.commitment, + witnessedRootsHash: state.witnessedRoots.commitment, + witnessedRootsPreimage: state.witnessedRoots.preimage, }, }); return { - publicInput, + stateWitness, networkState: firstBlock.networkState.before, blockWitness: firstResult.blockHashWitness, }; @@ -154,14 +174,8 @@ export class BlockTracingService { state.incomingMessages = afterState.incomingMessages; state.eternalTransactionsList = afterState.eternalTransactionsList; - const preimage = afterState.witnessedRoots - .getUnconstrainedValues() - .get() - .at(-2)?.preimage; - const afterBlockRootWitness: WitnessedRootWitness = { witnessedRoot: Field(block.result.witnessedRoots[0]), - preimage: preimage ?? Field(0), }; // We create the batch here, because we need the afterBlockRootWitness, @@ -187,7 +201,6 @@ export class BlockTracingService { appliedBatchListState: afterState.pendingSTBatches.commitment, root: afterBlockRootWitness.witnessedRoot, }, - afterBlockRootWitness.preimage, state.pendingSTBatches.commitment.equals(0).not() ); } diff --git a/packages/sequencer/test/integration/BlockProduction-test.ts b/packages/sequencer/test/integration/BlockProduction-test.ts index 06f6c688..58f1b0c9 100644 --- a/packages/sequencer/test/integration/BlockProduction-test.ts +++ b/packages/sequencer/test/integration/BlockProduction-test.ts @@ -34,6 +34,7 @@ import { DatabasePruneModule, AsyncLinkedLeafStore, AppChain, + BlockProducerModule, } from "../../src"; import { DefaultTestingSequencerModules, @@ -537,10 +538,10 @@ export function testBlockProduction< it.each([ [2, 1, 1], - [1, 2, 1], - [1, 1, 2], + [1, 5, 1], [2, 2, 2], [1, 14, 0], + [1, 6, 5], ])( "should produce multiple blocks with multiple batches with multiple transactions", async (batches, blocksPerBatch, txsPerBlock) => { @@ -684,6 +685,49 @@ export function testBlockProduction< expect(batch!.proof.proof).toBe(MOCK_PROOF); }, 30000); + it.each([4, 6, 9])( + "should produce some filled blocks and some empty blocks", + async (numBlocks) => { + log.setLevel("INFO"); + + (sequencer.resolve("BlockProducerModule") as BlockProducerModule).config = + { + maximumBlockSize: 5, + }; + + const privateKey = PrivateKey.random(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const i of range(0, 7)) { + await test.addTransaction({ + method: ["Balance", "addBalance"], + privateKey, + args: [PrivateKey.random().toPublicKey(), UInt64.from(100)], + }); + } + + // Produce 6 blocks, 5 txs each into 1 batch + const block = await test.produceBlock(); + + expectDefined(block); + expect(block.transactions).toHaveLength(5); + expect(block.transactions[0].status.toBoolean()).toBe(true); + + await mapSequential( + range(0, numBlocks - 1), + async () => await test.produceBlock() + ); + const batch = await test.produceBatch(); + + expectDefined(batch); + + console.log(batch.proof); + + expect(batch.blockHashes).toHaveLength(numBlocks); + }, + 30000 + ); + it("events - should produce block with the right events", async () => { log.setLevel("TRACE");