diff --git a/packages/protocol/test/prover/block/BlockProver.test.ts b/packages/protocol/test/prover/block/BlockProver.test.ts index 852f5833c..26202dc35 100644 --- a/packages/protocol/test/prover/block/BlockProver.test.ts +++ b/packages/protocol/test/prover/block/BlockProver.test.ts @@ -12,7 +12,1018 @@ */ /* eslint-enable max-len */ +import "reflect-metadata"; +import { MAX_FIELD } from "@proto-kit/common"; +import { Bool, Field, Proof, Signature, UInt64 } from "o1js"; -it("dummy", () => { - expect(1).toBe(1); +import { + BlockProverMultiTransactionExecutionData, + BlockProverPublicInput, + BlockProverPublicOutput, + BlockProverSingleTransactionExecutionData, + BlockProverTransactionArguments, + NetworkState, + RuntimeTransaction, + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput, +} from "../../../src"; +import { createAndInitTestingProtocol } from "../../TestingProtocol"; + +import { + createBlockProof, + createBlockProverPublicInput, + createDummyStateTransitionProof, + createRuntimeTransactionWithProof, + createStateTransitionProofWithTransitions, + DEFAULT_TRANSACTION, + 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 stProof = await createDummyStateTransitionProof(); + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + ...DEFAULT_TRANSACTION, + blockHashRoot: Field(123), + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + 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 stProof = await createDummyStateTransitionProof(); + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + ...DEFAULT_TRANSACTION, + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + 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 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({ + ...DEFAULT_TRANSACTION, + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + networkStateHash: badNetworkState.hash(), + 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 stProof = await createDummyStateTransitionProof(); + + const badNetworkState = new NetworkState({ + block: { height: UInt64.from(1) }, + previous: { rootHash: Field(1) }, + }); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + ...DEFAULT_TRANSACTION, + networkStateHash: badNetworkState.hash(), + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + eternalTransactionsHash: Field(123), + networkStateHash: badNetworkState.hash(), + closed: Bool(false), + }), + maxProofsVerified: 2, + proof: "", + }); + + await expect(async () => { + await proveBlock(protocol, { + stProof, + transactionProofOverride: badTransactionProof, + }); + }).rejects.toThrow(errorMsg); + }); + + it("should fail when transaction proof changes the state root", async () => { + const errorMsg = "TransactionProofs can't change the state root"; + + const stProof = await createDummyStateTransitionProof(); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput(DEFAULT_TRANSACTION), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + stateRoot: Field(999), + 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 stProof = await createStateTransitionProofWithTransitions( + initialStateRoot, + protocol.resolve("StateTransitionProver") + ); + + const badTransactionProof = new Proof< + BlockProverPublicInput, + BlockProverPublicOutput + >({ + publicInput: new BlockProverPublicInput({ + ...DEFAULT_TRANSACTION, + pendingSTBatchesHash: Field(999), + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + pendingSTBatchesHash: stProof.publicOutput.batchesHash, + 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 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({ + ...DEFAULT_TRANSACTION, + pendingSTBatchesHash: Field(999), + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + pendingSTBatchesHash: Field(999), + 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 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({ + ...DEFAULT_TRANSACTION, + pendingSTBatchesHash: Field(999), + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + pendingSTBatchesHash: Field(999), + 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 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({ + ...DEFAULT_TRANSACTION, + pendingSTBatchesHash: Field(999), + }), + publicOutput: new BlockProverPublicOutput({ + ...DEFAULT_TRANSACTION, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + pendingSTBatchesHash: Field(999), + 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", 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 { runtimeProof, signature } = createRuntimeTransactionWithProof(); + + const { verificationKeyAttestation: vk } = + await setupVerificationKeyAttestation(protocol); + + setupStateService(protocol); + + const badTransaction = RuntimeTransaction.fromMessage({ + methodId, + argsHash: Field(888), + }); + + 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 } = createRuntimeTransactionWithProof(); + + const { verificationKeyAttestation: vk } = + await setupVerificationKeyAttestation(protocol); + + setupStateService(protocol); + + const badSignature: Signature = Signature.empty() as Signature; + + 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 + ); + }); + }); + 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 new file mode 100644 index 000000000..b3adec818 --- /dev/null +++ b/packages/protocol/test/prover/block/utils.ts @@ -0,0 +1,523 @@ +import { + LinkedMerkleTree, + MAX_FIELD, + InMemoryMerkleTreeStorage, +} from "@proto-kit/common"; +import { + Bool, + Field, + PrivateKey, + Proof, + Signature, + UInt64, + VerificationKey, +} from "o1js"; +import { DummyStateService } from "@proto-kit/sequencer/src/state/state/DummyStateService"; + +import { + BlockHashMerkleTreeWitness, + BlockProverPublicInput, + BlockProverPublicOutput, + BlockProverSingleTransactionExecutionData, + BlockProverTransactionArguments, + DynamicRuntimeProof, + MethodPublicOutput, + NetworkState, + ProvableStateTransition, + RuntimeTransaction, + RuntimeVerificationKeyAttestation, + StateTransitionProof, + StateTransitionProver, + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput, + WitnessedRootWitness, + RuntimeVerificationKeyRootService, + AccountStateHook, + BlockHeightHook, + BlockProver, + LastStateRootBlockHook, + Protocol, +} from "../../../src"; +import { + VKTree, + MethodVKConfigData, +} from "../../../src/prover/block/accummulators/RuntimeVerificationKeyTree"; +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), + }); + + return new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput, + publicOutput, + proof: "", + maxProofsVerified: 2, + }); +} + +/** + * 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 + ); + return new Proof< + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >({ + publicInput, + publicOutput, + proof: "", + maxProofsVerified: 2, + }); +} + +/** + * 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 ?? false + ? 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: Protocol<{ + StateTransitionProver: typeof StateTransitionProver; + BlockProver: typeof BlockProver; + AccountState: typeof AccountStateHook; + BlockHeight: typeof BlockHeightHook; + LastStateRoot: typeof LastStateRootBlockHook; + }> +): Promise<{ + verificationKeyAttestation: RuntimeVerificationKeyAttestation; +}> { + const vk = await VerificationKey.dummy(); + const { attestation: verificationKeyAttestation, treeRoot } = + await createMockVerificationKeyAttestation(vk); + + const vkService = protocol.dependencyContainer.resolve( + RuntimeVerificationKeyRootService + ); + vkService.setRoot(treeRoot); + + return { verificationKeyAttestation }; +} + +/** + * Sets up state service for transaction execution + */ +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; +} + +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, + pendingSTBatchesHash: Field, + isEmpty: boolean = false +): Proof { + const transactionInput = { + ...DEFAULT_TRANSACTION, + stateRoot: initialStateRoot, + }; + + const transactionProofPublicInput = new BlockProverPublicInput( + transactionInput + ); + const transactionProofOutput = isEmpty + ? new BlockProverPublicOutput({ ...transactionInput, closed: Bool(false) }) + : new BlockProverPublicOutput({ + ...transactionInput, + transactionsHash: Field(123), + eternalTransactionsHash: Field(789), + pendingSTBatchesHash: pendingSTBatchesHash, + closed: Bool(false), + }); + return new Proof({ + publicInput: transactionProofPublicInput, + publicOutput: transactionProofOutput, + maxProofsVerified: 2, + proof: "", + }); +} + +/** + * Helper function to prove a block + */ +export async function proveBlock( + protocol: Protocol<{ + StateTransitionProver: typeof StateTransitionProver; + BlockProver: typeof BlockProver; + AccountState: typeof AccountStateHook; + BlockHeight: typeof BlockHeightHook; + LastStateRoot: typeof LastStateRootBlockHook; + }>, + 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 ?? false + ? await createDummyStateTransitionProof() + : await createStateTransitionProofWithTransitions( + initialStateRoot, + stProver + )); + const transactionProof = + options?.transactionProofOverride ?? + createTransactionProof( + initialStateRoot, + 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: Protocol<{ + StateTransitionProver: typeof StateTransitionProver; + BlockProver: typeof BlockProver; + AccountState: typeof AccountStateHook; + BlockHeight: typeof BlockHeightHook; + LastStateRoot: typeof LastStateRootBlockHook; + }>, + 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 } = + options?.useInvalidVK ?? false + ? { + verificationKeyAttestation: RuntimeVerificationKeyAttestation.empty(), + } + : 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 + ); +} + +/** + * 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: "", + }); +}