From bb2a6242d58d010089268ac2f30051e0a620bc95 Mon Sep 17 00:00:00 2001 From: stanlou Date: Tue, 25 Nov 2025 02:04:42 +0100 Subject: [PATCH 1/5] add unit tests for proveBlock and proveTransaction in BlockProver --- .../test/prover/block/BlockProver.test.ts | 877 +++++++++++++++++- packages/protocol/test/prover/block/utils.ts | 462 +++++++++ 2 files changed, 1337 insertions(+), 2 deletions(-) create mode 100644 packages/protocol/test/prover/block/utils.ts diff --git a/packages/protocol/test/prover/block/BlockProver.test.ts b/packages/protocol/test/prover/block/BlockProver.test.ts index 852f5833c..ae9f1478f 100644 --- a/packages/protocol/test/prover/block/BlockProver.test.ts +++ b/packages/protocol/test/prover/block/BlockProver.test.ts @@ -12,7 +12,880 @@ */ /* eslint-enable max-len */ +import { MAX_FIELD } from "@proto-kit/common"; +import { + Bool, + Field, + Proof, + Signature, + UInt64, +} from "o1js"; +import "reflect-metadata"; -it("dummy", () => { - expect(1).toBe(1); +import { + BlockProverMultiTransactionExecutionData, + BlockProverPublicInput, + BlockProverPublicOutput, + BlockProverSingleTransactionExecutionData, + BlockProverTransactionArguments, + NetworkState, + RuntimeTransaction, + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput, +} from "../../../src"; + +import { createAndInitTestingProtocol } from "../../TestingProtocol"; +import { createBlockProverPublicInput, createDummyStateTransitionProof, createRuntimeTransactionWithProof, createStateTransitionProofWithTransitions, proveBlock, proveTransaction, setupStateService, setupVerificationKeyAttestation } from "./utils"; + +describe("BlockProver", () => { + const protocol = createAndInitTestingProtocol(); + describe("Block Proving", () => { + it("should prove a block", async () => { + const blockProofPublicOutput = await proveBlock(protocol); + expect(blockProofPublicOutput).toBeDefined(); + expect(blockProofPublicOutput.closed.toBoolean()).toBe(true); + expect(blockProofPublicOutput.blockNumber).toEqual(Field(1)); + }); + + it("should prove a block with no transaction", async () => { + const blockProofPublicOutput = await proveBlock(protocol, { + isEmptyTransition: true, + }); + expect(blockProofPublicOutput).toBeDefined(); + expect(blockProofPublicOutput.closed.toBoolean()).toBe(true); + expect(blockProofPublicOutput.blockNumber).toEqual(Field(1)); + }); + + it("should defer state transitions in block proving", async () => { + const blockProofPublicOutput = await proveBlock(protocol, { + deferSTProof: true, + }); + expect(blockProofPublicOutput).toBeDefined(); + expect(blockProofPublicOutput.closed.toBoolean()).toBe(true); + expect(blockProofPublicOutput.blockNumber).toEqual(Field(1)); + expect(blockProofPublicOutput.witnessedRootsHash).not.toEqual(Field(0)); + expect(blockProofPublicOutput.pendingSTBatchesHash).not.toEqual(Field(0)); + }); + + describe("Assertion Failures", () => { + it("should fail when transactionsHash does not start from 0", async () => { + const errorMsg = `Transactionshash has to start at 0`; + await expect(async () => { + await proveBlock(protocol, { + publicInputOverrides: { transactionsHash: Field(123) }, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction proof blockHashRoot (publicInput) is not empty", async () => { + const errorMsg = `TransactionProof cannot carry the blockHashRoot - publicInput`; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const stProof = await createDummyStateTransitionProof(); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(123), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof, + transactionProofOverride: badTransactionProof, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when blockHashRoot is not empty", async () => { + const errorMsg = `TransactionProof cannot carry the blockHashRoot - publicOutput`; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const stProof = await createDummyStateTransitionProof(); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(456), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof, + transactionProofOverride: badTransactionProof, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction proof alter the network state", async () => { + const errorMsg = `TransactionProof cannot alter the network state`; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const badNetworkState = new NetworkState({ + block: { height: UInt64.from(1) }, + previous: { rootHash: Field(1) }, + }); + + const stProof = await createDummyStateTransitionProof(); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: badNetworkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof, + transactionProofOverride: badTransactionProof, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when networkStateHash mismatches in proveBlock", async () => { + const errorMsg = `ExecutionData Networkstate doesn't equal public input hash`; + const badNetworkStateHash = new NetworkState({ + block: { height: UInt64.from(1) }, + previous: { rootHash: Field(1) }, + }).hash(); + + await expect(async () => { + await proveBlock(protocol, { + networkStateHash: badNetworkStateHash, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction proof networkStateHash does not match beforeBlock hook result", async () => { + const errorMsg = `TransactionProof networkstate hash not matching beforeBlock hook result`; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const stProof = await createDummyStateTransitionProof(); + + // Create a transaction proof with mismatched networkStateHash + // This should not match the beforeBlock hook result + const badNetworkState = new NetworkState({ + block: { height: UInt64.from(1) }, + previous: { rootHash: Field(1) }, + }); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: badNetworkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(123), + networkStateHash: badNetworkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof, + transactionProofOverride: badTransactionProof, + networkStateHash: networkState.hash(), + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction proof changes the state root", async () => { + const errorMsg = `TransactionProofs can't change the state root`; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const stProof = await createDummyStateTransitionProof(); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: Field(999), + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof, + transactionProofOverride: badTransactionProof, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction proof does not start STs after beforeBlock hook", async () => { + const errorMsg = `Transaction proof doesn't start their STs after the beforeBlockHook`; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const stProof = await createStateTransitionProofWithTransitions( + initialStateRoot, + protocol.resolve("StateTransitionProver") + ); + + // Create a transaction proof with incorrect pendingSTBatchesHash + // It should match the state after beforeBlock hook, not before + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(999), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: stProof.publicOutput.batchesHash, + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof, + transactionProofOverride: badTransactionProof, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when state root does not match witnessed root with empty state transitions", async () => { + await expect(async () => { + await proveBlock(protocol, { + isEmptyTransition: true, + publicInputOverrides: { + stateRoot: Field(999), + }, + }); + }).rejects.toThrow(); + }); + it("should fail when block number does not match block witnessed root", async () => { + await expect(async () => { + await proveBlock(protocol, { + isEmptyTransition: true, + publicInputOverrides: { + blockNumber: Field(999), + }, + }); + }).rejects.toThrow(); + }); + it("should fail when block hash does not match block witness", async () => { + const errorMsg = "Supplied block hash witness not matching state root"; + await expect(async () => { + await proveBlock(protocol, { + publicInputOverrides: { + blockHashRoot: Field(999), + }, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when state transition proof currentBatchStateHash is not empty at start", async () => { + const errorMsg = "State for STProof has to be empty at the start"; + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const badSTProof = new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput: new StateTransitionProverPublicInput({ + root: initialStateRoot, + batchesHash: Field(0), + currentBatchStateHash: Field(999), + witnessedRootsHash: Field(0), + }), + publicOutput: new StateTransitionProverPublicOutput({ + root: initialStateRoot, + batchesHash: Field(0), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }), + proof: "", + maxProofsVerified: 2, + }); + const transactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(999), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(999), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof: badSTProof, + transactionProofOverride: transactionProof, + publicInputOverrides: { + pendingSTBatchesHash: Field(999), + }, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when state transition proof currentBatchStateHash is not empty at end", async () => { + const errorMsg = "State for STProof has to be empty at the end"; + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const badSTProof = new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput: new StateTransitionProverPublicInput({ + root: initialStateRoot, + batchesHash: Field(0), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }), + publicOutput: new StateTransitionProverPublicOutput({ + root: initialStateRoot, + batchesHash: Field(0), + currentBatchStateHash: Field(999), + witnessedRootsHash: Field(0), + }), + proof: "", + maxProofsVerified: 2, + }); + const transactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(999), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(999), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + await expect(async () => { + await proveBlock(protocol, { + stProof: badSTProof, + transactionProofOverride: transactionProof, + publicInputOverrides: { pendingSTBatchesHash: Field(999) }, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when state transition proof batchesHash does not start at 0", async () => { + const errorMsg = "Batcheshash doesn't start at 0"; + const initialStateRoot = Field(0); + + const badSTProof = new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput: new StateTransitionProverPublicInput({ + root: initialStateRoot, + batchesHash: Field(123), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }), + publicOutput: new StateTransitionProverPublicOutput({ + root: initialStateRoot, + batchesHash: Field(456), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }), + proof: "", + maxProofsVerified: 2, + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof: badSTProof, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when state transition proof input root does not match state root", async () => { + const errorMsg = "from state root not matching"; + const initialStateRoot = Field(0); + const badStateRoot = Field(999); + const networkState = NetworkState.empty(); + + const badSTProof = new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput: new StateTransitionProverPublicInput({ + root: badStateRoot, + batchesHash: Field(0), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }), + publicOutput: new StateTransitionProverPublicOutput({ + root: initialStateRoot, + batchesHash: Field(0), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }), + proof: "", + maxProofsVerified: 2, + }); + const transactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(999), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }), + publicOutput: new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + networkStateHash: networkState.hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(999), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + await expect(async () => { + await proveBlock(protocol, { + stProof: badSTProof, + publicInputOverrides: { + stateRoot: initialStateRoot, + pendingSTBatchesHash: Field(999), + }, + transactionProofOverride: transactionProof, + }); + }).rejects.toThrow(errorMsg); + }); + }); + }); + describe("Transaction Proving", () => { + it("should prove a single transaction", async () => { + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + + const result = await proveTransaction(protocol, { + initialStateRoot, + networkState, + isMessage: false, + }); + + expect(result).toBeDefined(); + expect(result.networkStateHash.value).toEqual(networkState.hash().value); + expect(result.stateRoot.value).toEqual(initialStateRoot.value); + expect(result.blockNumber.value).toEqual(MAX_FIELD.value); + expect(result.closed.toBoolean()).toEqual(false); + expect(result.witnessedRootsHash.value).toEqual(Field(0).value); + expect(result.incomingMessagesHash.value).toEqual(Field(0).value); + expect(result.eternalTransactionsHash.value).not.toEqual(Field(0).value); + expect(result.transactionsHash.value).not.toEqual(Field(0).value); + expect(result.pendingSTBatchesHash.value).not.toEqual(Field(0).value); + }); + it("should prove a message with empty signature", async () => { + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + + const result = await proveTransaction(protocol, { + initialStateRoot, + networkState, + isMessage: true, + }); + + expect(result).toBeDefined(); + expect(result.networkStateHash.value).toEqual(networkState.hash().value); + expect(result.stateRoot.value).toEqual(initialStateRoot.value); + expect(result.blockNumber.value).toEqual(MAX_FIELD.value); + expect(result.closed.toBoolean()).toEqual(false); + expect(result.witnessedRootsHash.value).toEqual(Field(0).value); + expect(result.incomingMessagesHash.value).not.toEqual(Field(0).value); + expect(result.eternalTransactionsHash.value).not.toEqual(Field(0).value); + expect(result.transactionsHash.value).toEqual(Field(0).value); + expect(result.pendingSTBatchesHash.value).not.toEqual(Field(0).value); + }); + + describe("Assertion Failures", () => { + it("Should fail due to Networkstate mismatch", async () => { + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const badNetworkStateHash = new NetworkState({ + block: { height: UInt64.from(1) }, + previous: { rootHash: Field(1) }, + }).hash(); + + await expect(async () => { + await proveTransaction(protocol, { + initialStateRoot, + networkState, + badNetworkStateHash, + }); + }).rejects.toThrow( + "ExecutionData Networkstate doesn't equal public input hash" + ); + }); + it("Should fail due to blockNumber not equal max_field ", async () => { + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + + await expect(async () => { + await proveTransaction(protocol, { + initialStateRoot, + networkState, + publicInputOverrides: { blockNumber: MAX_FIELD.sub(1) }, + }); + }).rejects.toThrow("blockNumber has to be MAX for transaction proofs"); + }); + + it("should fail when verification key root hash is invalid", async () => { + const errorMsg = + "Root hash of the provided zkProgram config witness is invalid"; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + + await expect(async () => { + await proveTransaction(protocol, { + initialStateRoot, + networkState, + isMessage: false, + useInvalidVK: true, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction hash does not match runtime proof hash", async () => { + const errorMsg = + "Transactions provided in AppProof and BlockProof do not match"; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + const methodId = Field(1); + const argsHash = Field(999); + + const { runtimeProof, signature } = createRuntimeTransactionWithProof(); + + const { verificationKeyAttestation: vk } = + await setupVerificationKeyAttestation(protocol); + + setupStateService(protocol); + + // Create execution data with a different transaction (different argsHash) + const badTransaction = RuntimeTransaction.fromMessage({ + methodId, + argsHash: Field(888), // Different argsHash causes different hash + }); + + const publicInput = createBlockProverPublicInput({ + stateRoot: initialStateRoot, + networkStateHash: networkState.hash(), + }); + + const executionData = new BlockProverSingleTransactionExecutionData({ + transaction: new BlockProverTransactionArguments({ + transaction: badTransaction, + signature, + verificationKeyAttestation: vk, + }), + networkState, + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.proveTransaction( + publicInput, + runtimeProof, + executionData + ); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction signature is invalid", async () => { + const errorMsg = "Transaction signature not valid"; + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + + const { + runtimeTx, + runtimeProof, + signature: validSignature, + } = createRuntimeTransactionWithProof(); + + const { verificationKeyAttestation: vk } = + await setupVerificationKeyAttestation(protocol); + + setupStateService(protocol); + + // Create a bad signature + const badSignature = Signature.empty(); + + const publicInput = createBlockProverPublicInput({ + stateRoot: initialStateRoot, + networkStateHash: networkState.hash(), + }); + + const executionData = new BlockProverSingleTransactionExecutionData({ + transaction: new BlockProverTransactionArguments({ + transaction: runtimeTx, + signature: badSignature, + verificationKeyAttestation: vk, + }), + networkState, + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.proveTransaction( + publicInput, + runtimeProof, + executionData + ); + }).rejects.toThrow(errorMsg); + }); + }); + + it("should prove two consecutive transactions correctly", async () => { + setupStateService(protocol); + + const initialStateRoot = Field(0); + const networkState = NetworkState.empty(); + + const publicInput = createBlockProverPublicInput({ + stateRoot: initialStateRoot, + networkStateHash: networkState.hash(), + }); + + const { + runtimeTx: runtimeMsg1, + runtimeProof: runtimeProof1, + signature: signature1, + } = createRuntimeTransactionWithProof(); + + const { + runtimeTx: runtimeMsg2, + runtimeProof: runtimeProof2, + signature: signature2, + } = createRuntimeTransactionWithProof({ argsHash: Field(888) }); + + const { verificationKeyAttestation } = + await setupVerificationKeyAttestation(protocol); + setupStateService(protocol); + + const executionData = new BlockProverMultiTransactionExecutionData({ + transaction1: new BlockProverTransactionArguments({ + transaction: runtimeMsg1, + verificationKeyAttestation, + signature: signature1, + }), + transaction2: new BlockProverTransactionArguments({ + transaction: runtimeMsg2, + verificationKeyAttestation, + signature: signature2, + }), + networkState, + }); + + const blockProver = protocol.resolve("BlockProver"); + const result = await blockProver.proveTransactions( + publicInput, + runtimeProof1, + runtimeProof2, + executionData + ); + + expect(result).toBeDefined(); + expect(result.networkStateHash.value).toEqual(networkState.hash().value); + expect(result.stateRoot.value).toEqual(initialStateRoot.value); + expect(result.blockNumber.value).toEqual(publicInput.blockNumber.value); + expect(result.closed.toBoolean()).toEqual(false); + expect(result.transactionsHash.value).not.toEqual( + publicInput.transactionsHash.value + ); + expect(result.eternalTransactionsHash.value).not.toEqual( + publicInput.eternalTransactionsHash.value + ); + }); + }); }); diff --git a/packages/protocol/test/prover/block/utils.ts b/packages/protocol/test/prover/block/utils.ts new file mode 100644 index 000000000..6e581c275 --- /dev/null +++ b/packages/protocol/test/prover/block/utils.ts @@ -0,0 +1,462 @@ +import { LinkedMerkleTree, MAX_FIELD } from "@proto-kit/common"; +import { + Bool, + Field, + PrivateKey, + Proof, + Signature, + UInt64, + VerificationKey, +} from "o1js"; +import "reflect-metadata"; + +import { + BlockHashMerkleTreeWitness, + BlockProverPublicInput, + BlockProverPublicOutput, + BlockProverSingleTransactionExecutionData, + BlockProverTransactionArguments, + DynamicRuntimeProof, + MethodPublicOutput, + NetworkState, + ProvableStateTransition, + RuntimeTransaction, + RuntimeVerificationKeyAttestation, + StateTransitionProof, + StateTransitionProver, + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput, + WitnessedRootWitness, +} from "../../../src"; + +import { InMemoryMerkleTreeStorage } from "@proto-kit/common"; +import { + VKTree, + MethodVKConfigData, +} from "../../../src/prover/block/accummulators/RuntimeVerificationKeyTree"; +import { RuntimeVerificationKeyRootService } from "../../../src"; +import { DummyStateService } from "@proto-kit/sequencer/src/state/state/DummyStateService"; +import { + StateTransitionProvableBatch, + MerkleWitnessBatch, +} from "../../../src/model/StateTransitionProvableBatch"; +import { AppliedStateTransitionBatchState } from "../../../src/model/AppliedStateTransitionBatch"; +import { Option } from "../../../src/model/Option"; + +/** + * Creates a RuntimeVerificationKeyAttestation with a mock VK tree. + */ +export async function createMockVerificationKeyAttestation( + verificationKey: VerificationKey +) { + const tree = new VKTree(new InMemoryMerkleTreeStorage()); + const methodId = Field(1); + const configData = new MethodVKConfigData({ + methodId, + vkHash: verificationKey.hash, + }); + tree.setLeaf(BigInt(0), configData.hash()); + const witness = tree.getWitness(BigInt(0)); + return { + attestation: new RuntimeVerificationKeyAttestation({ + verificationKey, + witness, + }), + treeRoot: tree.getRoot().toBigInt(), + }; +} + +/** + * Creates a dummy state transition proof + */ +export async function createDummyStateTransitionProof(): Promise { + const publicInput = new StateTransitionProverPublicInput({ + root: Field(0), + batchesHash: Field(0), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }); + + const publicOutput = new StateTransitionProverPublicOutput({ + root: Field(0), + batchesHash: Field(0), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }); + + const proof = new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput, + publicOutput, + proof: "", + maxProofsVerified: 2, + }); + + return proof; +} + +/** + * Creates a Merkle witness for block tree inclusion + */ +export function createBlockHashWitness(): { + blockWitness: BlockHashMerkleTreeWitness; + blockNumber: Field; + blockHashRoot: Field; +} { + const blockWitness = BlockHashMerkleTreeWitness.dummy(); + const calculatedIndex = blockWitness.calculateIndex(); + const calculatedRoot = blockWitness.calculateRoot(Field(0)); + return { + blockWitness, + blockNumber: calculatedIndex, + blockHashRoot: calculatedRoot, + }; +} + +/** + * Creates a StateTransitionProof with computed witnessedRootsHash. + */ +export async function createStateTransitionProofWithTransitions( + initialRoot: Field, + stProver: StateTransitionProver +): Promise { + const batchSize = 4; + const batch = StateTransitionProvableBatch.fromBatches([ + { + applied: Bool(true), + witnessRoot: Bool(true), + stateTransitions: [ + new ProvableStateTransition({ + path: Field(0), + from: Option.fromValue(Field(0), Field).toProvable(), + to: Option.fromValue(Field(100), Field).toProvable(), + }), + ], + }, + ])[0]; + const witnesses = new MerkleWitnessBatch({ + witnesses: Array.from({ length: batchSize }, () => + LinkedMerkleTree.dummyWitness() + ), + }); + + const currentAppliedBatch = new AppliedStateTransitionBatchState({ + batchHash: Field(0), + root: Field(0), + }); + + const publicInput = new StateTransitionProverPublicInput({ + root: initialRoot, + batchesHash: Field(0), + currentBatchStateHash: Field(0), + witnessedRootsHash: Field(0), + }); + + const publicOutput = await stProver.proveBatch( + publicInput, + batch, + witnesses, + currentAppliedBatch + ); + const proof = new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput, + publicOutput, + proof: "", + maxProofsVerified: 2, + }); + + return proof; +} + +/** + * Creates a runtime transaction with runtime proof + */ +export function createRuntimeTransactionWithProof(options?: { + methodId?: Field; + argsHash?: Field; + networkState?: NetworkState; + isMessage?: boolean; +}): { + runtimeTx: RuntimeTransaction; + runtimeProof: DynamicRuntimeProof; + privateKey: PrivateKey; + signature: Signature; +} { + const methodId = options?.methodId ?? Field(1); + const argsHash = options?.argsHash ?? Field(999); + const networkState = options?.networkState ?? NetworkState.empty(); + const privateKey = PrivateKey.random(); + const publicKey = privateKey.toPublicKey(); + const runtimeTx = options?.isMessage + ? RuntimeTransaction.fromMessage({ + methodId, + argsHash, + }) + : RuntimeTransaction.fromTransaction({ + methodId: methodId, + sender: publicKey, + nonce: UInt64.from(0), + argsHash: argsHash, + }); + + const signatureData = [ + runtimeTx.methodId, + ...runtimeTx.nonce.value.toFields(), + runtimeTx.argsHash, + ]; + const signature = Signature.create(privateKey, signatureData); + + const txHash = runtimeTx.hash(); + const methodPublicOutput = new MethodPublicOutput({ + transactionHash: txHash, + stateTransitionsHash: Field(0), + status: Bool(true), + networkStateHash: networkState.hash(), + isMessage: Bool(options?.isMessage || false), + eventsHash: Field(0), + }); + + const runtimeProof = new DynamicRuntimeProof({ + publicInput: undefined, + publicOutput: methodPublicOutput, + proof: "", + maxProofsVerified: 0, + }); + + return { runtimeTx, runtimeProof, privateKey, signature }; +} + +/** + * Creates a BlockProverPublicInput with defa configuration + */ +export function createBlockProverPublicInput( + overrides: Partial = {} +): BlockProverPublicInput { + const defaults = { + stateRoot: Field(0), + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: NetworkState.empty().hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }; + + return new BlockProverPublicInput({ + ...defaults, + ...overrides, + }); +} + +/** + * Sets up VK attestation and injects tree root into service + */ +export async function setupVerificationKeyAttestation(protocol: any): Promise<{ + verificationKeyAttestation: RuntimeVerificationKeyAttestation; + vk: VerificationKey; +}> { + const vk = await VerificationKey.dummy(); + const { attestation: verificationKeyAttestation, treeRoot } = + await createMockVerificationKeyAttestation(vk); + + const vkService = protocol.dependencyContainer.resolve( + RuntimeVerificationKeyRootService + ); + vkService.setRoot(treeRoot); + + return { verificationKeyAttestation, vk }; +} + +/** + * Sets up state service for transaction execution + */ +export function setupStateService(protocol: any): DummyStateService { + const stateServiceProvider = protocol.stateServiceProvider; + const dummyStateService = new DummyStateService(); + stateServiceProvider.setCurrentStateService(dummyStateService); + return dummyStateService; +} + +/** + * Helper function to create a transaction proof + */ +export function createTransactionProof( + initialStateRoot: Field, + networkStateHash: Field, + pendingSTBatchesHash: Field, + isEmpty: boolean = false +): Proof { + const transactionInput = { + stateRoot: initialStateRoot, + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: networkStateHash, + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }; + const transactionProofPublicInput = new BlockProverPublicInput( + transactionInput + ); + const transactionProofOutput = isEmpty + ? new BlockProverPublicOutput({ ...transactionInput, closed: Bool(false) }) + : new BlockProverPublicOutput({ + stateRoot: initialStateRoot, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + networkStateHash: networkStateHash, + blockNumber: MAX_FIELD, + pendingSTBatchesHash: pendingSTBatchesHash, + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + closed: Bool(false), + }); + return new Proof({ + publicInput: transactionProofPublicInput, + publicOutput: transactionProofOutput, + maxProofsVerified: 2, + proof: "", + }); +} + +/** + * Helper function to prove a block + */ +export async function proveBlock( + protocol: any, + options?: { + isEmptyTransition?: boolean; + deferSTProof?: boolean; + initialStateRoot?: Field; + networkStateHash?: Field; + blockWitness?: BlockHashMerkleTreeWitness; + stProof?: StateTransitionProof; + publicInputOverrides?: Partial; + transactionProofOverride?: Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >; + } +): Promise { + const initialStateRoot = options?.initialStateRoot ?? Field(0); + const networkState = NetworkState.empty(); + const { + blockWitness: defaultBlockWitness, + blockNumber, + blockHashRoot, + } = createBlockHashWitness(); + const blockWitness = options?.blockWitness ?? defaultBlockWitness; + const networkStateHashForInput = + options?.networkStateHash ?? networkState.hash(); + + const blockProofPublicInput = createBlockProverPublicInput({ + stateRoot: initialStateRoot, + networkStateHash: networkStateHashForInput, + blockNumber: blockNumber, + blockHashRoot: blockHashRoot, + ...(options?.publicInputOverrides ?? {}), + }); + const stProver = protocol.resolve("StateTransitionProver"); + const stProof = + options?.stProof ?? + (options?.isEmptyTransition + ? await createDummyStateTransitionProof() + : await createStateTransitionProofWithTransitions( + initialStateRoot, + stProver + )); + const transactionProof = + options?.transactionProofOverride ?? + createTransactionProof( + initialStateRoot, + networkState.hash(), + stProof.publicOutput.batchesHash, + options?.isEmptyTransition + ); + const dummyWitnessRoot = new WitnessedRootWitness({ + witnessedRoot: initialStateRoot, + preimage: Field(0), + }); + const blockProver = protocol.resolve("BlockProver"); + return await blockProver.proveBlock( + blockProofPublicInput, + networkState, + blockWitness, + stProof, + Bool(options?.deferSTProof || false), + dummyWitnessRoot, + transactionProof + ); +} + +/** + * Helper function to prove a transaction + */ +export async function proveTransaction( + protocol: any, + options?: { + initialStateRoot?: Field; + networkState?: NetworkState; + isMessage?: boolean; + methodId?: Field; + argsHash?: Field; + publicInputOverrides?: Partial; + useInvalidVK?: boolean; + badNetworkStateHash?: Field; + } +): Promise { + const initialStateRoot = options?.initialStateRoot ?? Field(0); + const networkState = options?.networkState ?? NetworkState.empty(); + const isMessage = options?.isMessage ?? false; + const methodId = options?.methodId ?? Field(1); + const argsHash = options?.argsHash ?? Field(999); + + const publicInput = createBlockProverPublicInput({ + stateRoot: initialStateRoot, + networkStateHash: options?.badNetworkStateHash ?? networkState.hash(), + ...(options?.publicInputOverrides ?? {}), + }); + + const { runtimeTx, runtimeProof, signature } = + createRuntimeTransactionWithProof({ + methodId, + argsHash, + networkState, + isMessage, + }); + const { verificationKeyAttestation: vk, vk: verificationKey } = + options?.useInvalidVK + ? { + verificationKeyAttestation: RuntimeVerificationKeyAttestation.empty(), + vk: await VerificationKey.dummy(), + } + : await setupVerificationKeyAttestation(protocol); + + setupStateService(protocol); + + const executionData = new BlockProverSingleTransactionExecutionData({ + transaction: new BlockProverTransactionArguments({ + transaction: runtimeTx, + signature, + verificationKeyAttestation: vk, + }), + networkState, + }); + + const blockProver = protocol.resolve("BlockProver"); + return await blockProver.proveTransaction( + publicInput, + runtimeProof, + executionData + ); +} From 0b49c7810ae5e1b7e5d5ad13d16c562df3c43d60 Mon Sep 17 00:00:00 2001 From: stanlou Date: Tue, 25 Nov 2025 15:15:58 +0100 Subject: [PATCH 2/5] refactor: improve code structure --- .../test/prover/block/BlockProver.test.ts | 203 +++--------------- packages/protocol/test/prover/block/utils.ts | 37 ++-- 2 files changed, 48 insertions(+), 192 deletions(-) diff --git a/packages/protocol/test/prover/block/BlockProver.test.ts b/packages/protocol/test/prover/block/BlockProver.test.ts index ae9f1478f..a2233e04b 100644 --- a/packages/protocol/test/prover/block/BlockProver.test.ts +++ b/packages/protocol/test/prover/block/BlockProver.test.ts @@ -13,13 +13,7 @@ /* eslint-enable max-len */ import { MAX_FIELD } from "@proto-kit/common"; -import { - Bool, - Field, - Proof, - Signature, - UInt64, -} from "o1js"; +import { Bool, Field, Proof, Signature, UInt64 } from "o1js"; import "reflect-metadata"; import { @@ -35,7 +29,17 @@ import { } from "../../../src"; import { createAndInitTestingProtocol } from "../../TestingProtocol"; -import { createBlockProverPublicInput, createDummyStateTransitionProof, createRuntimeTransactionWithProof, createStateTransitionProofWithTransitions, proveBlock, proveTransaction, setupStateService, setupVerificationKeyAttestation } from "./utils"; +import { + createBlockProverPublicInput, + createDummyStateTransitionProof, + createRuntimeTransactionWithProof, + createStateTransitionProofWithTransitions, + DEFAULT_TRANSACTION, + proveBlock, + proveTransaction, + setupStateService, + setupVerificationKeyAttestation, +} from "./utils"; describe("BlockProver", () => { const protocol = createAndInitTestingProtocol(); @@ -80,8 +84,6 @@ describe("BlockProver", () => { it("should fail when transaction proof blockHashRoot (publicInput) is not empty", async () => { const errorMsg = `TransactionProof cannot carry the blockHashRoot - publicInput`; - const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const stProof = await createDummyStateTransitionProof(); const badTransactionProof = new Proof< @@ -89,26 +91,11 @@ describe("BlockProver", () => { BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), + ...DEFAULT_TRANSACTION, blockHashRoot: Field(123), }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), + ...DEFAULT_TRANSACTION, closed: Bool(false), }), maxProofsVerified: 2, @@ -126,8 +113,6 @@ describe("BlockProver", () => { it("should fail when blockHashRoot is not empty", async () => { const errorMsg = `TransactionProof cannot carry the blockHashRoot - publicOutput`; - const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const stProof = await createDummyStateTransitionProof(); const badTransactionProof = new Proof< @@ -135,25 +120,10 @@ describe("BlockProver", () => { BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), + ...DEFAULT_TRANSACTION, }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), + ...DEFAULT_TRANSACTION, blockHashRoot: Field(456), closed: Bool(false), }), @@ -172,8 +142,6 @@ describe("BlockProver", () => { it("should fail when transaction proof alter the network state", async () => { const errorMsg = `TransactionProof cannot alter the network state`; - const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const badNetworkState = new NetworkState({ block: { height: UInt64.from(1) }, previous: { rootHash: Field(1) }, @@ -186,26 +154,11 @@ describe("BlockProver", () => { BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), + ...DEFAULT_TRANSACTION, }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), + ...DEFAULT_TRANSACTION, networkStateHash: badNetworkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }), maxProofsVerified: 2, @@ -237,12 +190,8 @@ describe("BlockProver", () => { it("should fail when transaction proof networkStateHash does not match beforeBlock hook result", async () => { const errorMsg = `TransactionProof networkstate hash not matching beforeBlock hook result`; - const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const stProof = await createDummyStateTransitionProof(); - // Create a transaction proof with mismatched networkStateHash - // This should not match the beforeBlock hook result const badNetworkState = new NetworkState({ block: { height: UInt64.from(1) }, previous: { rootHash: Field(1) }, @@ -253,26 +202,13 @@ describe("BlockProver", () => { BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), + ...DEFAULT_TRANSACTION, networkStateHash: badNetworkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), + ...DEFAULT_TRANSACTION, eternalTransactionsHash: Field(123), networkStateHash: badNetworkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }), maxProofsVerified: 2, @@ -283,7 +219,6 @@ describe("BlockProver", () => { await proveBlock(protocol, { stProof, transactionProofOverride: badTransactionProof, - networkStateHash: networkState.hash(), }); }).rejects.toThrow(errorMsg); }); @@ -291,35 +226,16 @@ describe("BlockProver", () => { it("should fail when transaction proof changes the state root", async () => { const errorMsg = `TransactionProofs can't change the state root`; - const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const stProof = await createDummyStateTransitionProof(); const badTransactionProof = new Proof< BlockProverPublicInput, BlockProverPublicOutput >({ - publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), - }), + publicInput: new BlockProverPublicInput(DEFAULT_TRANSACTION), publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, stateRoot: Field(999), - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }), maxProofsVerified: 2, @@ -338,39 +254,24 @@ describe("BlockProver", () => { const errorMsg = `Transaction proof doesn't start their STs after the beforeBlockHook`; const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const stProof = await createStateTransitionProofWithTransitions( initialStateRoot, protocol.resolve("StateTransitionProver") ); - // Create a transaction proof with incorrect pendingSTBatchesHash - // It should match the state after beforeBlock hook, not before const badTransactionProof = new Proof< BlockProverPublicInput, BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, + ...DEFAULT_TRANSACTION, pendingSTBatchesHash: Field(999), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, + ...DEFAULT_TRANSACTION, transactionsHash: Field(123), eternalTransactionsHash: Field(789), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, pendingSTBatchesHash: stProof.publicOutput.batchesHash, - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }), maxProofsVerified: 2, @@ -444,26 +345,14 @@ describe("BlockProver", () => { BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, + ...DEFAULT_TRANSACTION, pendingSTBatchesHash: Field(999), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, + ...DEFAULT_TRANSACTION, transactionsHash: Field(123), eternalTransactionsHash: Field(789), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, pendingSTBatchesHash: Field(999), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }), maxProofsVerified: 2, @@ -509,26 +398,14 @@ describe("BlockProver", () => { BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, + ...DEFAULT_TRANSACTION, pendingSTBatchesHash: Field(999), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, + ...DEFAULT_TRANSACTION, transactionsHash: Field(123), eternalTransactionsHash: Field(789), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, pendingSTBatchesHash: Field(999), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }), maxProofsVerified: 2, @@ -578,7 +455,6 @@ describe("BlockProver", () => { const errorMsg = "from state root not matching"; const initialStateRoot = Field(0); const badStateRoot = Field(999); - const networkState = NetworkState.empty(); const badSTProof = new Proof< StateTransitionProverPublicInput, @@ -604,26 +480,14 @@ describe("BlockProver", () => { BlockProverPublicOutput >({ publicInput: new BlockProverPublicInput({ - stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, + ...DEFAULT_TRANSACTION, pendingSTBatchesHash: Field(999), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), }), publicOutput: new BlockProverPublicOutput({ - stateRoot: initialStateRoot, + ...DEFAULT_TRANSACTION, transactionsHash: Field(123), eternalTransactionsHash: Field(789), - networkStateHash: networkState.hash(), - blockNumber: MAX_FIELD, pendingSTBatchesHash: Field(999), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }), maxProofsVerified: 2, @@ -664,7 +528,7 @@ describe("BlockProver", () => { expect(result.transactionsHash.value).not.toEqual(Field(0).value); expect(result.pendingSTBatchesHash.value).not.toEqual(Field(0).value); }); - it("should prove a message with empty signature", async () => { + it("should prove a message", async () => { const initialStateRoot = Field(0); const networkState = NetworkState.empty(); @@ -742,7 +606,6 @@ describe("BlockProver", () => { const initialStateRoot = Field(0); const networkState = NetworkState.empty(); const methodId = Field(1); - const argsHash = Field(999); const { runtimeProof, signature } = createRuntimeTransactionWithProof(); @@ -751,10 +614,9 @@ describe("BlockProver", () => { setupStateService(protocol); - // Create execution data with a different transaction (different argsHash) const badTransaction = RuntimeTransaction.fromMessage({ methodId, - argsHash: Field(888), // Different argsHash causes different hash + argsHash: Field(888), }); const publicInput = createBlockProverPublicInput({ @@ -798,7 +660,6 @@ describe("BlockProver", () => { setupStateService(protocol); - // Create a bad signature const badSignature = Signature.empty(); const publicInput = createBlockProverPublicInput({ diff --git a/packages/protocol/test/prover/block/utils.ts b/packages/protocol/test/prover/block/utils.ts index 6e581c275..925eb6830 100644 --- a/packages/protocol/test/prover/block/utils.ts +++ b/packages/protocol/test/prover/block/utils.ts @@ -8,7 +8,6 @@ import { UInt64, VerificationKey, } from "o1js"; -import "reflect-metadata"; import { BlockHashMerkleTreeWitness, @@ -260,7 +259,6 @@ export function createBlockProverPublicInput( */ export async function setupVerificationKeyAttestation(protocol: any): Promise<{ verificationKeyAttestation: RuntimeVerificationKeyAttestation; - vk: VerificationKey; }> { const vk = await VerificationKey.dummy(); const { attestation: verificationKeyAttestation, treeRoot } = @@ -271,7 +269,7 @@ export async function setupVerificationKeyAttestation(protocol: any): Promise<{ ); vkService.setRoot(treeRoot); - return { verificationKeyAttestation, vk }; + return { verificationKeyAttestation }; } /** @@ -284,41 +282,40 @@ export function setupStateService(protocol: any): DummyStateService { return dummyStateService; } +export const DEFAULT_TRANSACTION = { + stateRoot: Field(0), + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: NetworkState.empty().hash(), + blockNumber: MAX_FIELD, + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), +}; /** * Helper function to create a transaction proof */ export function createTransactionProof( initialStateRoot: Field, - networkStateHash: Field, pendingSTBatchesHash: Field, isEmpty: boolean = false ): Proof { const transactionInput = { + ...DEFAULT_TRANSACTION, stateRoot: initialStateRoot, - transactionsHash: Field(0), - eternalTransactionsHash: Field(0), - networkStateHash: networkStateHash, - blockNumber: MAX_FIELD, - pendingSTBatchesHash: Field(0), - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), }; + const transactionProofPublicInput = new BlockProverPublicInput( transactionInput ); const transactionProofOutput = isEmpty ? new BlockProverPublicOutput({ ...transactionInput, closed: Bool(false) }) : new BlockProverPublicOutput({ - stateRoot: initialStateRoot, + ...transactionInput, transactionsHash: Field(123), eternalTransactionsHash: Field(789), - networkStateHash: networkStateHash, - blockNumber: MAX_FIELD, pendingSTBatchesHash: pendingSTBatchesHash, - incomingMessagesHash: Field(0), - witnessedRootsHash: Field(0), - blockHashRoot: Field(0), closed: Bool(false), }); return new Proof({ @@ -379,7 +376,6 @@ export async function proveBlock( options?.transactionProofOverride ?? createTransactionProof( initialStateRoot, - networkState.hash(), stProof.publicOutput.batchesHash, options?.isEmptyTransition ); @@ -434,11 +430,10 @@ export async function proveTransaction( networkState, isMessage, }); - const { verificationKeyAttestation: vk, vk: verificationKey } = + const { verificationKeyAttestation: vk } = options?.useInvalidVK ? { verificationKeyAttestation: RuntimeVerificationKeyAttestation.empty(), - vk: await VerificationKey.dummy(), } : await setupVerificationKeyAttestation(protocol); From cb2628e25140df0129bbf788d9f383b2476c6c97 Mon Sep 17 00:00:00 2001 From: stanlou Date: Tue, 25 Nov 2025 16:23:07 +0100 Subject: [PATCH 3/5] fix lint --- .../test/prover/block/BlockProver.test.ts | 38 ++++---- packages/protocol/test/prover/block/utils.ts | 94 +++++++++++++------ 2 files changed, 79 insertions(+), 53 deletions(-) diff --git a/packages/protocol/test/prover/block/BlockProver.test.ts b/packages/protocol/test/prover/block/BlockProver.test.ts index a2233e04b..2080330f9 100644 --- a/packages/protocol/test/prover/block/BlockProver.test.ts +++ b/packages/protocol/test/prover/block/BlockProver.test.ts @@ -27,8 +27,8 @@ import { StateTransitionProverPublicInput, StateTransitionProverPublicOutput, } from "../../../src"; - import { createAndInitTestingProtocol } from "../../TestingProtocol"; + import { createBlockProverPublicInput, createDummyStateTransitionProof, @@ -73,7 +73,7 @@ describe("BlockProver", () => { describe("Assertion Failures", () => { it("should fail when transactionsHash does not start from 0", async () => { - const errorMsg = `Transactionshash has to start at 0`; + const errorMsg = "Transactionshash has to start at 0"; await expect(async () => { await proveBlock(protocol, { publicInputOverrides: { transactionsHash: Field(123) }, @@ -82,10 +82,9 @@ describe("BlockProver", () => { }); it("should fail when transaction proof blockHashRoot (publicInput) is not empty", async () => { - const errorMsg = `TransactionProof cannot carry the blockHashRoot - publicInput`; - + const errorMsg = + "TransactionProof cannot carry the blockHashRoot - publicInput"; const stProof = await createDummyStateTransitionProof(); - const badTransactionProof = new Proof< BlockProverPublicInput, BlockProverPublicOutput @@ -111,10 +110,9 @@ describe("BlockProver", () => { }); it("should fail when blockHashRoot is not empty", async () => { - const errorMsg = `TransactionProof cannot carry the blockHashRoot - publicOutput`; - + const errorMsg = + "TransactionProof cannot carry the blockHashRoot - publicOutput"; const stProof = await createDummyStateTransitionProof(); - const badTransactionProof = new Proof< BlockProverPublicInput, BlockProverPublicOutput @@ -130,7 +128,6 @@ describe("BlockProver", () => { maxProofsVerified: 2, proof: "", }); - await expect(async () => { await proveBlock(protocol, { stProof, @@ -140,7 +137,7 @@ describe("BlockProver", () => { }); it("should fail when transaction proof alter the network state", async () => { - const errorMsg = `TransactionProof cannot alter the network state`; + const errorMsg = "TransactionProof cannot alter the network state"; const badNetworkState = new NetworkState({ block: { height: UInt64.from(1) }, @@ -174,7 +171,8 @@ describe("BlockProver", () => { }); it("should fail when networkStateHash mismatches in proveBlock", async () => { - const errorMsg = `ExecutionData Networkstate doesn't equal public input hash`; + const errorMsg = + "ExecutionData Networkstate doesn't equal public input hash"; const badNetworkStateHash = new NetworkState({ block: { height: UInt64.from(1) }, previous: { rootHash: Field(1) }, @@ -188,7 +186,8 @@ describe("BlockProver", () => { }); it("should fail when transaction proof networkStateHash does not match beforeBlock hook result", async () => { - const errorMsg = `TransactionProof networkstate hash not matching beforeBlock hook result`; + const errorMsg = + "TransactionProof networkstate hash not matching beforeBlock hook result"; const stProof = await createDummyStateTransitionProof(); @@ -224,7 +223,7 @@ describe("BlockProver", () => { }); it("should fail when transaction proof changes the state root", async () => { - const errorMsg = `TransactionProofs can't change the state root`; + const errorMsg = "TransactionProofs can't change the state root"; const stProof = await createDummyStateTransitionProof(); @@ -251,7 +250,8 @@ describe("BlockProver", () => { }); it("should fail when transaction proof does not start STs after beforeBlock hook", async () => { - const errorMsg = `Transaction proof doesn't start their STs after the beforeBlockHook`; + const errorMsg = + "Transaction proof doesn't start their STs after the beforeBlockHook"; const initialStateRoot = Field(0); const stProof = await createStateTransitionProofWithTransitions( @@ -320,7 +320,6 @@ describe("BlockProver", () => { it("should fail when state transition proof currentBatchStateHash is not empty at start", async () => { const errorMsg = "State for STProof has to be empty at the start"; const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const badSTProof = new Proof< StateTransitionProverPublicInput, StateTransitionProverPublicOutput @@ -373,7 +372,6 @@ describe("BlockProver", () => { it("should fail when state transition proof currentBatchStateHash is not empty at end", async () => { const errorMsg = "State for STProof has to be empty at the end"; const initialStateRoot = Field(0); - const networkState = NetworkState.empty(); const badSTProof = new Proof< StateTransitionProverPublicInput, StateTransitionProverPublicOutput @@ -649,18 +647,14 @@ describe("BlockProver", () => { const initialStateRoot = Field(0); const networkState = NetworkState.empty(); - const { - runtimeTx, - runtimeProof, - signature: validSignature, - } = createRuntimeTransactionWithProof(); + const { runtimeTx, runtimeProof } = createRuntimeTransactionWithProof(); const { verificationKeyAttestation: vk } = await setupVerificationKeyAttestation(protocol); setupStateService(protocol); - const badSignature = Signature.empty(); + const badSignature: Signature = Signature.empty() as Signature; const publicInput = createBlockProverPublicInput({ stateRoot: initialStateRoot, diff --git a/packages/protocol/test/prover/block/utils.ts b/packages/protocol/test/prover/block/utils.ts index 925eb6830..b265204ea 100644 --- a/packages/protocol/test/prover/block/utils.ts +++ b/packages/protocol/test/prover/block/utils.ts @@ -1,4 +1,8 @@ -import { LinkedMerkleTree, MAX_FIELD } from "@proto-kit/common"; +import { + LinkedMerkleTree, + MAX_FIELD, + InMemoryMerkleTreeStorage, +} from "@proto-kit/common"; import { Bool, Field, @@ -8,6 +12,7 @@ import { UInt64, VerificationKey, } from "o1js"; +import { DummyStateService } from "@proto-kit/sequencer/src/state/state/DummyStateService"; import { BlockHashMerkleTreeWitness, @@ -26,15 +31,17 @@ import { StateTransitionProverPublicInput, StateTransitionProverPublicOutput, WitnessedRootWitness, + RuntimeVerificationKeyRootService, + AccountStateHook, + BlockHeightHook, + BlockProver, + LastStateRootBlockHook, + Protocol, } from "../../../src"; - -import { InMemoryMerkleTreeStorage } from "@proto-kit/common"; import { VKTree, MethodVKConfigData, } from "../../../src/prover/block/accummulators/RuntimeVerificationKeyTree"; -import { RuntimeVerificationKeyRootService } from "../../../src"; -import { DummyStateService } from "@proto-kit/sequencer/src/state/state/DummyStateService"; import { StateTransitionProvableBatch, MerkleWitnessBatch, @@ -83,7 +90,7 @@ export async function createDummyStateTransitionProof(): Promise({ @@ -92,8 +99,6 @@ export async function createDummyStateTransitionProof(): Promise({ @@ -168,8 +173,6 @@ export async function createStateTransitionProofWithTransitions( proof: "", maxProofsVerified: 2, }); - - return proof; } /** @@ -191,17 +194,18 @@ export function createRuntimeTransactionWithProof(options?: { const networkState = options?.networkState ?? NetworkState.empty(); const privateKey = PrivateKey.random(); const publicKey = privateKey.toPublicKey(); - const runtimeTx = options?.isMessage - ? RuntimeTransaction.fromMessage({ - methodId, - argsHash, - }) - : RuntimeTransaction.fromTransaction({ - methodId: methodId, - sender: publicKey, - nonce: UInt64.from(0), - argsHash: argsHash, - }); + const runtimeTx = + options?.isMessage ?? false + ? RuntimeTransaction.fromMessage({ + methodId, + argsHash, + }) + : RuntimeTransaction.fromTransaction({ + methodId: methodId, + sender: publicKey, + nonce: UInt64.from(0), + argsHash: argsHash, + }); const signatureData = [ runtimeTx.methodId, @@ -216,7 +220,7 @@ export function createRuntimeTransactionWithProof(options?: { stateTransitionsHash: Field(0), status: Bool(true), networkStateHash: networkState.hash(), - isMessage: Bool(options?.isMessage || false), + isMessage: Bool(options?.isMessage ?? false), eventsHash: Field(0), }); @@ -257,7 +261,15 @@ export function createBlockProverPublicInput( /** * Sets up VK attestation and injects tree root into service */ -export async function setupVerificationKeyAttestation(protocol: any): Promise<{ +export async function setupVerificationKeyAttestation( + protocol: Protocol<{ + StateTransitionProver: typeof StateTransitionProver; + BlockProver: typeof BlockProver; + AccountState: typeof AccountStateHook; + BlockHeight: typeof BlockHeightHook; + LastStateRoot: typeof LastStateRootBlockHook; + }> +): Promise<{ verificationKeyAttestation: RuntimeVerificationKeyAttestation; }> { const vk = await VerificationKey.dummy(); @@ -275,8 +287,16 @@ export async function setupVerificationKeyAttestation(protocol: any): Promise<{ /** * Sets up state service for transaction execution */ -export function setupStateService(protocol: any): DummyStateService { - const stateServiceProvider = protocol.stateServiceProvider; +export function setupStateService( + protocol: Protocol<{ + StateTransitionProver: typeof StateTransitionProver; + BlockProver: typeof BlockProver; + AccountState: typeof AccountStateHook; + BlockHeight: typeof BlockHeightHook; + LastStateRoot: typeof LastStateRootBlockHook; + }> +): DummyStateService { + const { stateServiceProvider } = protocol; const dummyStateService = new DummyStateService(); stateServiceProvider.setCurrentStateService(dummyStateService); return dummyStateService; @@ -330,7 +350,13 @@ export function createTransactionProof( * Helper function to prove a block */ export async function proveBlock( - protocol: any, + protocol: Protocol<{ + StateTransitionProver: typeof StateTransitionProver; + BlockProver: typeof BlockProver; + AccountState: typeof AccountStateHook; + BlockHeight: typeof BlockHeightHook; + LastStateRoot: typeof LastStateRootBlockHook; + }>, options?: { isEmptyTransition?: boolean; deferSTProof?: boolean; @@ -366,7 +392,7 @@ export async function proveBlock( const stProver = protocol.resolve("StateTransitionProver"); const stProof = options?.stProof ?? - (options?.isEmptyTransition + (options?.isEmptyTransition ?? false ? await createDummyStateTransitionProof() : await createStateTransitionProofWithTransitions( initialStateRoot, @@ -389,7 +415,7 @@ export async function proveBlock( networkState, blockWitness, stProof, - Bool(options?.deferSTProof || false), + Bool(options?.deferSTProof ?? false), dummyWitnessRoot, transactionProof ); @@ -399,7 +425,13 @@ export async function proveBlock( * Helper function to prove a transaction */ export async function proveTransaction( - protocol: any, + protocol: Protocol<{ + StateTransitionProver: typeof StateTransitionProver; + BlockProver: typeof BlockProver; + AccountState: typeof AccountStateHook; + BlockHeight: typeof BlockHeightHook; + LastStateRoot: typeof LastStateRootBlockHook; + }>, options?: { initialStateRoot?: Field; networkState?: NetworkState; @@ -431,7 +463,7 @@ export async function proveTransaction( isMessage, }); const { verificationKeyAttestation: vk } = - options?.useInvalidVK + options?.useInvalidVK ?? false ? { verificationKeyAttestation: RuntimeVerificationKeyAttestation.empty(), } From d2798544b1165417ff167e98c389744b39566dcb Mon Sep 17 00:00:00 2001 From: stanlou Date: Tue, 25 Nov 2025 19:29:41 +0100 Subject: [PATCH 4/5] fix test --- packages/protocol/test/prover/block/BlockProver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/protocol/test/prover/block/BlockProver.test.ts b/packages/protocol/test/prover/block/BlockProver.test.ts index 2080330f9..39423af5a 100644 --- a/packages/protocol/test/prover/block/BlockProver.test.ts +++ b/packages/protocol/test/prover/block/BlockProver.test.ts @@ -12,9 +12,9 @@ */ /* eslint-enable max-len */ +import "reflect-metadata"; import { MAX_FIELD } from "@proto-kit/common"; import { Bool, Field, Proof, Signature, UInt64 } from "o1js"; -import "reflect-metadata"; import { BlockProverMultiTransactionExecutionData, From 608c1caa18afe92b974a0b40a7bbb146ea00b9a9 Mon Sep 17 00:00:00 2001 From: stanlou Date: Wed, 26 Nov 2025 01:06:25 +0100 Subject: [PATCH 5/5] add merge tests --- .../test/prover/block/BlockProver.test.ts | 283 ++++++++++++++++++ packages/protocol/test/prover/block/utils.ts | 34 +++ 2 files changed, 317 insertions(+) diff --git a/packages/protocol/test/prover/block/BlockProver.test.ts b/packages/protocol/test/prover/block/BlockProver.test.ts index 39423af5a..26202dc35 100644 --- a/packages/protocol/test/prover/block/BlockProver.test.ts +++ b/packages/protocol/test/prover/block/BlockProver.test.ts @@ -30,6 +30,7 @@ import { import { createAndInitTestingProtocol } from "../../TestingProtocol"; import { + createBlockProof, createBlockProverPublicInput, createDummyStateTransitionProof, createRuntimeTransactionWithProof, @@ -743,4 +744,286 @@ describe("BlockProver", () => { ); }); }); + describe("proofs Merging", () => { + it("should merge two closed block proofs", async () => { + const proof1 = createBlockProof({ + publicInput: { blockNumber: Field(1) }, + publicOutput: { blockNumber: Field(2), closed: Bool(true) }, + }); + + const proof2 = createBlockProof({ + publicInput: { blockNumber: Field(2) }, + publicOutput: { blockNumber: Field(3), closed: Bool(true) }, + }); + + const publicInput = createBlockProverPublicInput({ + blockNumber: Field(1), + }); + + const blockProver = protocol.resolve("BlockProver"); + const result = await blockProver.merge(publicInput, proof1, proof2); + + expect(result).toBeDefined(); + expect(result.closed.toBoolean()).toEqual(true); + expect(result.blockNumber).toEqual(Field(3)); + expect(result.stateRoot).toEqual(proof2.publicOutput.stateRoot); + }); + it("should merge two consecutive transaction proofs", async () => { + const proof1 = createBlockProof({ + publicInput: { blockNumber: MAX_FIELD }, + publicOutput: { closed: Bool(false) }, + }); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: MAX_FIELD, closed: Bool(false) }, + }); + const publicInput = createBlockProverPublicInput(); + const blockProver = protocol.resolve("BlockProver"); + const result = await blockProver.merge(publicInput, proof1, proof2); + expect(result).toBeDefined(); + expect(result.stateRoot).toEqual(proof2.publicOutput.stateRoot); + expect(result.closed.toBoolean()).toEqual(false); + expect(result.blockNumber).toEqual(proof2.publicOutput.blockNumber); + }); + describe("Assertion Failures", () => { + it("should fail when state roots do not match", async () => { + const errorMsg = + "StateRoots not matching: publicInput.from -> proof1.from"; + + const proof1 = createBlockProof({ + publicInput: { stateRoot: Field(100) }, + }); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + stateRoot: Field(1), + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when state roots do not match between proofs", async () => { + const errorMsg = "StateRoots not matching: proof1.to -> proof2.from"; + + const proof1 = createBlockProof({ + publicInput: { stateRoot: Field(100) }, + publicOutput: { stateRoot: Field(100) }, + }); + const proof2 = createBlockProof({ + publicInput: { ...proof1.publicOutput, stateRoot: Field(999) }, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + stateRoot: Field(100), + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transactions hash does not match", async () => { + const errorMsg = + "Transactions hash not matching: publicInput.from -> proof1.from"; + + const proof1 = createBlockProof({ + publicInput: { transactionsHash: Field(123), blockNumber: MAX_FIELD }, + publicOutput: { + transactionsHash: Field(123), + blockNumber: MAX_FIELD, + closed: Bool(false), + }, + }); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { closed: Bool(false) }, + }); + + const publicInput = createBlockProverPublicInput({ + transactionsHash: Field(999), + blockNumber: MAX_FIELD, + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when network state hash does not match", async () => { + const errorMsg = + "Network state hash not matching: publicInput.from -> proof1.from"; + + const networkStateHash2 = new NetworkState({ + block: { height: UInt64.from(1) }, + previous: { rootHash: Field(1) }, + }).hash(); + + const proof1 = createBlockProof(); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + networkStateHash: networkStateHash2, + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when block hash root does not match", async () => { + const errorMsg = + "Transactions hash not matching: publicInput.from -> proof1.from"; + + const proof1 = createBlockProof(); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + blockHashRoot: Field(999), + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when eternal transactions hash does not match", async () => { + const errorMsg = + "Transactions hash not matching: publicInput.from -> proof1.from"; + + const proof1 = createBlockProof(); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + eternalTransactionsHash: Field(999), + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when incoming messages hash does not match", async () => { + const errorMsg = + "IncomingMessagesHash not matching: publicInput.from -> proof1.from"; + + const proof1 = createBlockProof(); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + incomingMessagesHash: Field(999), + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when pending ST batches hash does not match", async () => { + const errorMsg = + "Transactions hash not matching: publicInput.from -> proof1.from"; + + const proof1 = createBlockProof(); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + pendingSTBatchesHash: Field(999), + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when witnessed roots hash does not match", async () => { + const errorMsg = + "Transactions hash not matching: publicInput.from -> proof1.from"; + + const proof1 = createBlockProof(); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { blockNumber: Field(2) }, + }); + + const publicInput = createBlockProverPublicInput({ + witnessedRootsHash: Field(999), + }); + + const blockProver = protocol.resolve("BlockProver"); + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when closed indicators do not match", async () => { + const errorMsg = "Closed indicators not matching"; + + const proof1 = createBlockProof({ + publicOutput: { closed: Bool(false) }, + }); + const proof2 = createBlockProof({ + publicInput: proof1.publicOutput, + publicOutput: { closed: Bool(true) }, + }); + + const publicInput = createBlockProverPublicInput(); + const blockProver = protocol.resolve("BlockProver"); + + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when merging with invalid block number progression", async () => { + const errorMsg = "Invalid BlockProof merge"; + + const blockNumber = Field(5); + + const proof1 = createBlockProof({ + publicInput: { blockNumber }, + publicOutput: { blockNumber, closed: Bool(false) }, + }); + + const proof2 = createBlockProof({ + publicInput: { blockNumber }, + publicOutput: { blockNumber, closed: Bool(false) }, + }); + + const publicInput = createBlockProverPublicInput({ blockNumber }); + const blockProver = protocol.resolve("BlockProver"); + + await expect(async () => { + await blockProver.merge(publicInput, proof1, proof2); + }).rejects.toThrow(errorMsg); + }); + }); + }); }); diff --git a/packages/protocol/test/prover/block/utils.ts b/packages/protocol/test/prover/block/utils.ts index b265204ea..b3adec818 100644 --- a/packages/protocol/test/prover/block/utils.ts +++ b/packages/protocol/test/prover/block/utils.ts @@ -487,3 +487,37 @@ export async function proveTransaction( executionData ); } + +/** + * Helper to create a BlockProverProof + */ +export function createBlockProof( + overrides: { + publicInput?: Partial; + publicOutput?: Partial; + } = {} +): Proof { + const defaults = { + stateRoot: Field(0), + transactionsHash: Field(0), + eternalTransactionsHash: Field(0), + networkStateHash: NetworkState.empty().hash(), + blockNumber: Field(0), + pendingSTBatchesHash: Field(0), + incomingMessagesHash: Field(0), + witnessedRootsHash: Field(0), + blockHashRoot: Field(0), + }; + return new Proof({ + publicInput: new BlockProverPublicInput({ + ...defaults, + ...overrides.publicInput, + }), + publicOutput: new BlockProverPublicOutput({ + ...{ ...defaults, closed: Bool(true), blockNumber: Field(1) }, + ...overrides.publicOutput, + }), + maxProofsVerified: 2, + proof: "", + }); +}