From aba7f339243725bf0143751049156ec4f19d9ee5 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Tue, 27 Jan 2026 15:29:25 +0100 Subject: [PATCH 1/6] Added bridge-contract test --- packages/library/src/index.ts | 1 + .../protocol/src/prover/block/BlockProver.ts | 1 - .../settlement/contracts/BridgeContract.ts | 10 +- .../bridgeContract/bridge-contract.test.ts | 178 ++++++++++++++++++ 4 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/test/bridgeContract/bridge-contract.test.ts diff --git a/packages/library/src/index.ts b/packages/library/src/index.ts index 98d833022..83ad42af0 100644 --- a/packages/library/src/index.ts +++ b/packages/library/src/index.ts @@ -4,6 +4,7 @@ export * from "./math/UInt64"; export * from "./math/UInt112"; export * from "./math/UInt224"; export * from "./protocol/VanillaProtocolModules"; +export * from "./protocol/WithdrawalMessageProcessor"; export * from "./runtime/Balances"; export * from "./runtime/VanillaRuntimeModules"; export * from "./runtime/Withdrawals"; diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 4bb709c2b..38bc988e9 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -446,7 +446,6 @@ export class BlockProverProgrammable extends ZkProgrammable< const finalizeBlockProof = deferTransactionProof.or(deferSTProof).not(); // .or() // .or(state.bundleList.isEmpty().and(state.pendingSTBatches.isEmpty())); - // TODO This finalizes immediately if nothing happened - which we don't account for in tracer currently return this.computeOutput(publicInput, state, finalizeBlockProof); } diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index f56f8ec95..764a6f94c 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -70,7 +70,9 @@ export class BridgeContractContext { } export interface BridgeContractArgs { - SettlementContract: TypedClass & + SettlementContract: TypedClass< + Pick + > & typeof SmartContract; messageProcessors: OutgoingMessageProcessor[]; batchSize?: number; @@ -186,7 +188,9 @@ export abstract class BridgeContractBase messageType: args.messageType, value, }); + Provable.log("h", Poseidon.hash(MessageType.toFields(message))); return { + // TODO This doesn't make any sense tbh... messageType: args.messageType, result: processor.processMessage(value, { bridgeContract: { @@ -302,6 +306,10 @@ export abstract class BridgeContractBase } ); + Provable.log(message.hash); + Provable.log(path); + Provable.log(stateRoot); + args.witness .checkMembership(stateRoot, path, message.hash) .or(isDummy) diff --git a/packages/sdk/test/bridgeContract/bridge-contract.test.ts b/packages/sdk/test/bridgeContract/bridge-contract.test.ts new file mode 100644 index 000000000..505bfdfea --- /dev/null +++ b/packages/sdk/test/bridgeContract/bridge-contract.test.ts @@ -0,0 +1,178 @@ +import "reflect-metadata"; +import { + BridgeContract, + BridgeContractArgs, + BridgeContractContext, + BridgingSettlementContractType, + ContractArgsRegistry, + createMessageStruct, + OutgoingMessageArgument, + OutgoingMessageArgumentBatch, + OutgoingMessageKey, + Path, + PROTOKIT_FIELD_PREFIXES, +} from "@proto-kit/protocol"; +import { + AccountUpdate, + Mina, + PrivateKey, + SmartContract, + state, + State, + Permissions, + VerificationKey, + TokenId, + Field, + UInt64, + Poseidon, + Provable, +} from "o1js"; +import { container } from "tsyringe"; +import { Withdrawal, WithdrawalMessageProcessor } from "@proto-kit/library"; +import { + InMemoryLinkedLeafStore, + InMemoryMerkleTreeStorage, + LinkedMerkleTree, +} from "@proto-kit/common"; + +class MockSettlementContract + extends SmartContract + implements Pick +{ + @state(Field) root = State(Field(100)); + + public assertStateRoot(root: Field): AccountUpdate { + // this.root.requireNothing() + return this.self; + } +} + +const proofsEnabled = false; + +describe("bridging contract", () => { + it("setup", async () => { + container + .resolve(ContractArgsRegistry) + .addArgs("BridgeContract", { + SettlementContract: MockSettlementContract, + messageProcessors: [new WithdrawalMessageProcessor() as any], + }); + + const key1 = PrivateKey.random(); + const key2 = PrivateKey.random(); + + const settlement = new MockSettlementContract(key1.toPublicKey()); + const contract = new BridgeContract(key2.toPublicKey()); + + const chain = await Mina.LocalBlockchain({ proofsEnabled }); + Mina.setActiveInstance(chain); + const tx = await Mina.transaction(chain.testAccounts[0], async () => { + AccountUpdate.fundNewAccount(chain.testAccounts[0], 2); + + await settlement.deploy(); + + const accountUpdate = await contract.deployProvable( + VerificationKey.dummySync(), + false, + Permissions.default(), + key1.toPublicKey() + ); + accountUpdate.requireSignature(); + AccountUpdate.attachToTransaction(accountUpdate); + + AccountUpdate.createSigned(chain.testAccounts[0]).send({ + to: key1.toPublicKey(), + amount: 1e9, + }); + }); + + const proven = await tx + .sign([chain.testAccounts[0].key, key1, key2]) + .prove(); + const txId = await proven.send(); + await txId.wait(); + + const tree = new LinkedMerkleTree( + new InMemoryMerkleTreeStorage(), + new InMemoryLinkedLeafStore() + ); + const path = Path.fromKey( + PROTOKIT_FIELD_PREFIXES.OUTGOING_MESSAGE_BASE_PATH, + OutgoingMessageKey, + { + index: Field(0), + tokenId: contract.tokenId, + } + ); + const message = new Withdrawal({ + tokenId: TokenId.default, + amount: UInt64.from(1e9), + address: chain.testAccounts[1].key.toPublicKey(), + }); + const MessageType = createMessageStruct(Withdrawal); + tree.setLeaf( + path.toBigInt(), + Poseidon.hash( + MessageType.toFields({ messageType: Field(0), value: message }) + ).toBigInt() + ); + Provable.log( + "tree hash", + Poseidon.hash( + MessageType.toFields({ messageType: Field(0), value: message }) + ).toBigInt() + ); + + const tx2 = await Mina.transaction(chain.testAccounts[0], async () => { + await contract.updateStateRoot(tree.getRoot()); + }); + const proven2 = await tx2 + .sign([chain.testAccounts[0].key, key1, key2]) + .prove(); + const txId2 = await proven2.send(); + await txId2.wait(); + + container.resolve(BridgeContractContext).data = { + messageInputs: [[message]], + }; + + const treeWitness = tree.getReadWitness(path.toBigInt()); + // expect( + // treeWitness + // .checkMembership( + // tree.getRoot(), + // path, + // Poseidon.hash(Withdrawal.toFields(message)) + // ) + // .toBoolean() + // ).toBe(true); + + Provable.log(Poseidon.hash(Withdrawal.toFields(message))); + Provable.log(path); + Provable.log(tree.getRoot()); + + const tx3 = await Mina.transaction(chain.testAccounts[0], async () => { + const funded = await contract.rollupOutgoingMessages( + OutgoingMessageArgumentBatch.fromMessages([ + new OutgoingMessageArgument({ + messageType: Field(0), + witness: treeWitness, + }), + ]) + ); + + let numNewAccountsNumber = 0; + Provable.asProver(() => { + numNewAccountsNumber = parseInt(funded.toString(), 10); + }); + + // Pay account creation fees for internal token accounts + AccountUpdate.fundNewAccount(chain.testAccounts[0], numNewAccountsNumber); + }); + const proven3 = await tx3 + .sign([chain.testAccounts[0].key, key1, key2]) + .prove(); + const txId3 = await proven3.send(); + await txId3.wait(); + }); +}); From 7bdb601a07b91fc540d49512d2457462fae1778a Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Tue, 27 Jan 2026 18:21:06 +0100 Subject: [PATCH 2/6] Added compilation to bridge-contract test --- .../bridgeContract/bridge-contract.test.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/sdk/test/bridgeContract/bridge-contract.test.ts b/packages/sdk/test/bridgeContract/bridge-contract.test.ts index 505bfdfea..4918b91f7 100644 --- a/packages/sdk/test/bridgeContract/bridge-contract.test.ts +++ b/packages/sdk/test/bridgeContract/bridge-contract.test.ts @@ -20,12 +20,12 @@ import { state, State, Permissions, - VerificationKey, TokenId, Field, UInt64, Poseidon, Provable, + method, } from "o1js"; import { container } from "tsyringe"; import { Withdrawal, WithdrawalMessageProcessor } from "@proto-kit/library"; @@ -33,6 +33,7 @@ import { InMemoryLinkedLeafStore, InMemoryMerkleTreeStorage, LinkedMerkleTree, + noop, } from "@proto-kit/common"; class MockSettlementContract @@ -42,9 +43,13 @@ class MockSettlementContract @state(Field) root = State(Field(100)); public assertStateRoot(root: Field): AccountUpdate { - // this.root.requireNothing() return this.self; } + + @method + public async test() { + noop(); + } } const proofsEnabled = false; @@ -61,18 +66,22 @@ describe("bridging contract", () => { const key1 = PrivateKey.random(); const key2 = PrivateKey.random(); + const chain = await Mina.LocalBlockchain({ proofsEnabled }); + Mina.setActiveInstance(chain); + + const vkSettlement = await MockSettlementContract.compile(); + const vk = await BridgeContract.compile(); + const settlement = new MockSettlementContract(key1.toPublicKey()); const contract = new BridgeContract(key2.toPublicKey()); - const chain = await Mina.LocalBlockchain({ proofsEnabled }); - Mina.setActiveInstance(chain); const tx = await Mina.transaction(chain.testAccounts[0], async () => { AccountUpdate.fundNewAccount(chain.testAccounts[0], 2); - await settlement.deploy(); + await settlement.deploy(vkSettlement); const accountUpdate = await contract.deployProvable( - VerificationKey.dummySync(), + vk.verificationKey, false, Permissions.default(), key1.toPublicKey() @@ -137,19 +146,6 @@ describe("bridging contract", () => { }; const treeWitness = tree.getReadWitness(path.toBigInt()); - // expect( - // treeWitness - // .checkMembership( - // tree.getRoot(), - // path, - // Poseidon.hash(Withdrawal.toFields(message)) - // ) - // .toBoolean() - // ).toBe(true); - - Provable.log(Poseidon.hash(Withdrawal.toFields(message))); - Provable.log(path); - Provable.log(tree.getRoot()); const tx3 = await Mina.transaction(chain.testAccounts[0], async () => { const funded = await contract.rollupOutgoingMessages( @@ -174,5 +170,7 @@ describe("bridging contract", () => { .prove(); const txId3 = await proven3.send(); await txId3.wait(); - }); + + console.log(proven3.proofs.map((p) => p?.toJSON())); + }, 300000); }); From 52ae772a89f33a1e58a93c3bb1726350c68071fb Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Mon, 2 Feb 2026 17:06:06 +0100 Subject: [PATCH 3/6] Refactored message arguments in BridgeContract to remove context usage --- .../module/src/messages/OutgoingMessages.ts | 1 + .../settlement/contracts/BridgeContract.ts | 66 ++++++++++--------- .../messages/OutgoingMessageArgument.ts | 16 ++++- .../modularity/OutgoingMessageProcessor.ts | 17 ++++- .../bridgeContract/bridge-contract.test.ts | 62 ++++++++++------- .../src/settlement/BridgingModule.ts | 32 +++++---- 6 files changed, 119 insertions(+), 75 deletions(-) diff --git a/packages/module/src/messages/OutgoingMessages.ts b/packages/module/src/messages/OutgoingMessages.ts index 1790c89a3..45d8b3805 100644 --- a/packages/module/src/messages/OutgoingMessages.ts +++ b/packages/module/src/messages/OutgoingMessages.ts @@ -112,6 +112,7 @@ export class OutgoingMessages< const counter = counterOption.orElse(Field(0)); const messageKey = { index: counter, tokenId }; + // TODO Salt/prefix const messageType = prefixToField(key); await counterState.set(tokenId, counter.add(1)); diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index 764a6f94c..0b5aa78f6 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -1,7 +1,6 @@ import { AccountUpdate, Bool, - Experimental, Field, method, Permissions, @@ -14,10 +13,11 @@ import { Struct, TokenContract, TokenId, + Unconstrained, VerificationKey, } from "o1js"; import { noop, range, TypedClass } from "@proto-kit/common"; -import { container, injectable, singleton } from "tsyringe"; +import { container } from "tsyringe"; import { OUTGOING_MESSAGE_BATCH_SIZE, @@ -61,13 +61,13 @@ export class OutgoingMessageKey extends Struct({ tokenId: Field, }) {} -@injectable() -@singleton() -export class BridgeContractContext { - public data: { - messageInputs: any[][]; - } = { messageInputs: [] }; -} +// @injectable() +// @singleton() +// export class BridgeContractContext { +// public data: { +// messageInputs: any[][]; +// } = { messageInputs: [] }; +// } export interface BridgeContractArgs { SettlementContract: TypedClass< @@ -173,42 +173,44 @@ export abstract class BridgeContractBase ); } - private executeProcessors(batchIndex: number, args: OutgoingMessageArgument) { + private executeProcessors(args: OutgoingMessageArgument) { const { messageProcessors } = this.getInitializationArgs(); return messageProcessors.map((processor, j) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const value = Experimental.memoizeWitness(processor.type, () => { - return container.resolve(BridgeContractContext).data.messageInputs[ - batchIndex - ][j]; + const messageType = processor.getMessageType(); + + // Create the message struct from unconstrained message argument Field[] + const value = Provable.witness(processor.type, () => { + if (args.messageType.toString() === messageType.toString()) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const fieldData = (args.data as Unconstrained).get(); + return processor.type.fromFields(fieldData); + } else { + return processor.dummy(); + } }); const MessageType = createMessageStruct(processor.type); const message = new MessageType({ - messageType: args.messageType, + messageType, value, }); - Provable.log("h", Poseidon.hash(MessageType.toFields(message))); + const result = processor.processMessage(value, { + bridgeContract: { + publicKey: this.address, + tokenId: this.tokenId, + }, + }); return { - // TODO This doesn't make any sense tbh... - messageType: args.messageType, - result: processor.processMessage(value, { - bridgeContract: { - publicKey: this.address, - tokenId: this.tokenId, - }, - }), + // messageType is authenticated via the hash, which is checked again + messageType, + result, hash: Poseidon.hash(MessageType.toFields(message)), }; }); } - public processMessage( - batchIndex: number, - args: OutgoingMessageArgument, - isDummy: Bool - ) { - const results = this.executeProcessors(batchIndex, args); + public processMessage(args: OutgoingMessageArgument, isDummy: Bool) { + const results = this.executeProcessors(args); const maxAccountUpdates = Math.max( 0, @@ -294,7 +296,7 @@ export abstract class BridgeContractBase const isDummy = batch.isDummys[i]; - const message = this.processMessage(i, args, isDummy); + const message = this.processMessage(args, isDummy); // Check witness const path = Path.fromKey( diff --git a/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts b/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts index eebdf1e6a..685a498ab 100644 --- a/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts +++ b/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts @@ -1,7 +1,15 @@ -import { Bool, Field, FlexibleProvablePure, Provable, Struct } from "o1js"; +import { + Bool, + Field, + FlexibleProvablePure, + Provable, + Struct, + Unconstrained, +} from "o1js"; import { LinkedMerkleTree, LinkedMerkleTreeReadWitness, + createUnknownLengthUnion, } from "@proto-kit/common"; import { OutgoingMessage } from "./OutgoingMessage"; @@ -9,6 +17,10 @@ import { OutgoingMessage } from "./OutgoingMessage"; // TODO Make that dynamic based on processors configured export const OUTGOING_MESSAGE_BATCH_SIZE = 1; +export class BridgeUnknownLengthArgument extends createUnknownLengthUnion( + 100 +) {} + export function createMessageStruct(type: FlexibleProvablePure) { return class MessageStruct extends Struct({ value: type, @@ -19,11 +31,13 @@ export function createMessageStruct(type: FlexibleProvablePure) { export class OutgoingMessageArgument extends Struct({ witness: LinkedMerkleTreeReadWitness, messageType: Field, + data: Unconstrained, }) { public static dummy(): OutgoingMessageArgument { return new OutgoingMessageArgument({ witness: LinkedMerkleTree.dummyReadWitness(), messageType: Field(0), + data: Unconstrained.from([]), }); } } diff --git a/packages/protocol/src/settlement/modularity/OutgoingMessageProcessor.ts b/packages/protocol/src/settlement/modularity/OutgoingMessageProcessor.ts index dd8de0940..531e8a7d7 100644 --- a/packages/protocol/src/settlement/modularity/OutgoingMessageProcessor.ts +++ b/packages/protocol/src/settlement/modularity/OutgoingMessageProcessor.ts @@ -3,9 +3,10 @@ import { Bool, Field, FlexibleProvablePure, + Poseidon, PublicKey, } from "o1js"; -import { implement, NoConfig } from "@proto-kit/common"; +import { implement, NoConfig, prefixToField } from "@proto-kit/common"; import { ProtocolModule } from "../../protocol/ProtocolModule"; @@ -55,7 +56,19 @@ export abstract class OutgoingMessageProcessor< }; } - abstract type: FlexibleProvablePure; + public getMessageType(): Field { + // TODO static salt/prefix + // This executes the bigint poseidon behind the scenes, therefore creates a constant + const messageType = Poseidon.hash([prefixToField(this.messageType)]); + if (!messageType.isConstant()) { + throw new Error( + "Underlying poseidon implementation has changed and doesn't create a constant anymore" + ); + } + return messageType; + } + + abstract type: FlexibleProvablePure & { name: string }; abstract messageType: string; diff --git a/packages/sdk/test/bridgeContract/bridge-contract.test.ts b/packages/sdk/test/bridgeContract/bridge-contract.test.ts index 4918b91f7..e3b27496e 100644 --- a/packages/sdk/test/bridgeContract/bridge-contract.test.ts +++ b/packages/sdk/test/bridgeContract/bridge-contract.test.ts @@ -2,7 +2,6 @@ import "reflect-metadata"; import { BridgeContract, BridgeContractArgs, - BridgeContractContext, BridgingSettlementContractType, ContractArgsRegistry, createMessageStruct, @@ -26,6 +25,8 @@ import { Poseidon, Provable, method, + Unconstrained, + VerificationKey, } from "o1js"; import { container } from "tsyringe"; import { Withdrawal, WithdrawalMessageProcessor } from "@proto-kit/library"; @@ -35,6 +36,7 @@ import { LinkedMerkleTree, noop, } from "@proto-kit/common"; +import { beforeAll } from "@jest/globals"; class MockSettlementContract extends SmartContract @@ -54,23 +56,40 @@ class MockSettlementContract const proofsEnabled = false; +const timeout = proofsEnabled ? 180_000 : 50_000; + describe("bridging contract", () => { - it("setup", async () => { + let chain: Mina.LocalBlockchain; + + beforeAll(async () => { + chain = await Mina.LocalBlockchain({ proofsEnabled }); + + Mina.setActiveInstance(chain); + container .resolve(ContractArgsRegistry) .addArgs("BridgeContract", { SettlementContract: MockSettlementContract, messageProcessors: [new WithdrawalMessageProcessor() as any], }); + }); - const key1 = PrivateKey.random(); - const key2 = PrivateKey.random(); + const vks: { verificationKey: VerificationKey }[] = []; - const chain = await Mina.LocalBlockchain({ proofsEnabled }); - Mina.setActiveInstance(chain); + it( + "compile", + async () => { + const vkSettlement = await MockSettlementContract.compile(); + const vk = await BridgeContract.compile(); + vks.push(vkSettlement, vk); + }, + timeout + ); - const vkSettlement = await MockSettlementContract.compile(); - const vk = await BridgeContract.compile(); + it("should process withdrawal and mint withdrawal tokens", async () => { + const key1 = PrivateKey.random(); + const key2 = PrivateKey.random(); + const [vkSettlement, vkBridge] = vks; const settlement = new MockSettlementContract(key1.toPublicKey()); const contract = new BridgeContract(key2.toPublicKey()); @@ -81,7 +100,7 @@ describe("bridging contract", () => { await settlement.deploy(vkSettlement); const accountUpdate = await contract.deployProvable( - vk.verificationKey, + vkBridge.verificationKey, false, Permissions.default(), key1.toPublicKey() @@ -115,20 +134,16 @@ describe("bridging contract", () => { ); const message = new Withdrawal({ tokenId: TokenId.default, - amount: UInt64.from(1e9), + amount: UInt64.from(1e8), address: chain.testAccounts[1].key.toPublicKey(), }); const MessageType = createMessageStruct(Withdrawal); + const messageType = new WithdrawalMessageProcessor().getMessageType(); + tree.setLeaf( path.toBigInt(), Poseidon.hash( - MessageType.toFields({ messageType: Field(0), value: message }) - ).toBigInt() - ); - Provable.log( - "tree hash", - Poseidon.hash( - MessageType.toFields({ messageType: Field(0), value: message }) + MessageType.toFields({ messageType, value: message }) ).toBigInt() ); @@ -141,18 +156,15 @@ describe("bridging contract", () => { const txId2 = await proven2.send(); await txId2.wait(); - container.resolve(BridgeContractContext).data = { - messageInputs: [[message]], - }; - const treeWitness = tree.getReadWitness(path.toBigInt()); const tx3 = await Mina.transaction(chain.testAccounts[0], async () => { const funded = await contract.rollupOutgoingMessages( OutgoingMessageArgumentBatch.fromMessages([ new OutgoingMessageArgument({ - messageType: Field(0), + messageType, witness: treeWitness, + data: Unconstrained.from(Withdrawal.toFields(message)), }), ]) ); @@ -171,6 +183,10 @@ describe("bridging contract", () => { const txId3 = await proven3.send(); await txId3.wait(); - console.log(proven3.proofs.map((p) => p?.toJSON())); + const settlementAccount = chain.getAccount( + chain.testAccounts[1].key.toPublicKey(), + contract.deriveTokenId() + ); + expect(settlementAccount.balance.toString()).toBe((1e8).toString()); }, 300000); }); diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index a575c5e3a..29bfb3410 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -1,4 +1,4 @@ -import { container, inject, injectable } from "tsyringe"; +import { inject, injectable } from "tsyringe"; import { BridgeContractConfig, BridgeContractType, @@ -16,7 +16,6 @@ import { OutgoingMessageProcessor, PROTOKIT_FIELD_PREFIXES, OutgoingMessageEvent, - BridgeContractContext, BridgingSettlementModulesRecord, DispatchContractType, BridgingSettlementContractType, @@ -34,13 +33,13 @@ import { TokenId, Transaction, UInt32, + Unconstrained, } from "o1js"; import { DependencyRecord, filterNonUndefined, LinkedMerkleTree, log, - prefixToField, reduceSequential, } from "@proto-kit/common"; import { match, Pattern } from "ts-pattern"; @@ -604,27 +603,26 @@ export class BridgingModule extends SequencerModule { await cachedStore.preloadKeys(keys.map((key) => key.toBigInt())); const transactionParameters = batch.map((message, index) => { + const processor = this.getMessageProcessors().find( + (p) => p.getMessageType() === message.messageType + ); + if (processor === undefined) { + throw new Error( + "Processor not found for message type - looks like your module configuration is faulty" + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const data = processor.type.toFields(message.value); + const witness = tree.getReadWitness(keys[index].toBigInt()); return new OutgoingMessageArgument({ witness, messageType: message.messageType, + data: Unconstrained.from(data), }); }); - const contextData = transactionParameters.map((arg, j) => - this.getMessageProcessors().map((processor) => { - return prefixToField(processor.messageType) - .equals(arg.messageType) - .toBoolean() - ? batch[j].value - : processor.dummy(); - }) - ); - container.resolve(BridgeContractContext).data = { - messageInputs: contextData, - }; - // TODO Somehow make sure this data ends up in the proving task - const tx = await Mina.transaction( { sender: feepayer, From 8670456854573991e79759357067973465fb4c19 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Mon, 2 Feb 2026 17:21:38 +0100 Subject: [PATCH 4/6] Fixed build & lint --- .../src/settlement/messages/OutgoingMessageArgument.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts b/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts index 685a498ab..e36e750f0 100644 --- a/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts +++ b/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts @@ -9,7 +9,6 @@ import { import { LinkedMerkleTree, LinkedMerkleTreeReadWitness, - createUnknownLengthUnion, } from "@proto-kit/common"; import { OutgoingMessage } from "./OutgoingMessage"; @@ -17,10 +16,6 @@ import { OutgoingMessage } from "./OutgoingMessage"; // TODO Make that dynamic based on processors configured export const OUTGOING_MESSAGE_BATCH_SIZE = 1; -export class BridgeUnknownLengthArgument extends createUnknownLengthUnion( - 100 -) {} - export function createMessageStruct(type: FlexibleProvablePure) { return class MessageStruct extends Struct({ value: type, From 7ab546a9293602cc8a7153e7b08f4c06bca7e70d Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Tue, 3 Feb 2026 13:25:44 +0100 Subject: [PATCH 5/6] Fixed wrong hashing of message type --- packages/module/src/messages/OutgoingMessages.ts | 3 ++- .../protocol/src/settlement/contracts/BridgeContract.ts | 8 -------- packages/sequencer/src/settlement/BridgingModule.ts | 4 ++-- packages/sequencer/test/settlement/Settlement.test.ts | 4 ++-- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/module/src/messages/OutgoingMessages.ts b/packages/module/src/messages/OutgoingMessages.ts index 45d8b3805..d00352c04 100644 --- a/packages/module/src/messages/OutgoingMessages.ts +++ b/packages/module/src/messages/OutgoingMessages.ts @@ -2,6 +2,7 @@ import { Field, FlexibleProvablePure, InferProvable, + Poseidon, Struct, TokenId, } from "o1js"; @@ -113,7 +114,7 @@ export class OutgoingMessages< const messageKey = { index: counter, tokenId }; // TODO Salt/prefix - const messageType = prefixToField(key); + const messageType = Poseidon.hash([prefixToField(key)]); await counterState.set(tokenId, counter.add(1)); await stateMap.set(messageKey, { messageType, value }); diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index 0b5aa78f6..356919f69 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -61,14 +61,6 @@ export class OutgoingMessageKey extends Struct({ tokenId: Field, }) {} -// @injectable() -// @singleton() -// export class BridgeContractContext { -// public data: { -// messageInputs: any[][]; -// } = { messageInputs: [] }; -// } - export interface BridgeContractArgs { SettlementContract: TypedClass< Pick diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index 29bfb3410..6e00c29bf 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -603,8 +603,8 @@ export class BridgingModule extends SequencerModule { await cachedStore.preloadKeys(keys.map((key) => key.toBigInt())); const transactionParameters = batch.map((message, index) => { - const processor = this.getMessageProcessors().find( - (p) => p.getMessageType() === message.messageType + const processor = this.getMessageProcessors().find((p) => + p.getMessageType().equals(message.messageType).toBoolean() ); if (processor === undefined) { throw new Error( diff --git a/packages/sequencer/test/settlement/Settlement.test.ts b/packages/sequencer/test/settlement/Settlement.test.ts index b1aa7044a..ab674095a 100644 --- a/packages/sequencer/test/settlement/Settlement.test.ts +++ b/packages/sequencer/test/settlement/Settlement.test.ts @@ -5,7 +5,7 @@ import { MinaBaseLayerConfig } from "../../src"; import { settlementTestFn } from "./Settlement"; import { settlementOnlyTestFn } from "./Settlement-only"; -describe.each(["mock-proofs", "signed"] as const)( +describe.each(["mock-proofs" /*, "signed"*/] as const)( "Settlement contracts: local blockchain - %s", (type) => { const network: MinaBaseLayerConfig = { @@ -14,7 +14,7 @@ describe.each(["mock-proofs", "signed"] as const)( }, }; - describe("Default token", () => { + describe.only("Default token", () => { settlementTestFn(type, network); }); From d4ce1809a66cc2bfd780614d20885718b1bcb9a0 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Tue, 3 Feb 2026 14:31:22 +0100 Subject: [PATCH 6/6] Increased timeout of bridge contract test --- .../bridgeContract/bridge-contract.test.ts | 201 +++++++++--------- .../test/settlement/Settlement.test.ts | 4 +- 2 files changed, 106 insertions(+), 99 deletions(-) diff --git a/packages/sdk/test/bridgeContract/bridge-contract.test.ts b/packages/sdk/test/bridgeContract/bridge-contract.test.ts index e3b27496e..cc186d6f8 100644 --- a/packages/sdk/test/bridgeContract/bridge-contract.test.ts +++ b/packages/sdk/test/bridgeContract/bridge-contract.test.ts @@ -56,7 +56,7 @@ class MockSettlementContract const proofsEnabled = false; -const timeout = proofsEnabled ? 180_000 : 50_000; +const timeout = proofsEnabled ? 180_000 : 150_000; describe("bridging contract", () => { let chain: Mina.LocalBlockchain; @@ -86,107 +86,114 @@ describe("bridging contract", () => { timeout ); - it("should process withdrawal and mint withdrawal tokens", async () => { - const key1 = PrivateKey.random(); - const key2 = PrivateKey.random(); - const [vkSettlement, vkBridge] = vks; - - const settlement = new MockSettlementContract(key1.toPublicKey()); - const contract = new BridgeContract(key2.toPublicKey()); - - const tx = await Mina.transaction(chain.testAccounts[0], async () => { - AccountUpdate.fundNewAccount(chain.testAccounts[0], 2); + it( + "should process withdrawal and mint withdrawal tokens", + async () => { + const key1 = PrivateKey.random(); + const key2 = PrivateKey.random(); + const [vkSettlement, vkBridge] = vks; + + const settlement = new MockSettlementContract(key1.toPublicKey()); + const contract = new BridgeContract(key2.toPublicKey()); + + const tx = await Mina.transaction(chain.testAccounts[0], async () => { + AccountUpdate.fundNewAccount(chain.testAccounts[0], 2); + + await settlement.deploy(vkSettlement); + + const accountUpdate = await contract.deployProvable( + vkBridge.verificationKey, + false, + Permissions.default(), + key1.toPublicKey() + ); + accountUpdate.requireSignature(); + AccountUpdate.attachToTransaction(accountUpdate); + + AccountUpdate.createSigned(chain.testAccounts[0]).send({ + to: key1.toPublicKey(), + amount: 1e9, + }); + }); - await settlement.deploy(vkSettlement); + const proven = await tx + .sign([chain.testAccounts[0].key, key1, key2]) + .prove(); + const txId = await proven.send(); + await txId.wait(); - const accountUpdate = await contract.deployProvable( - vkBridge.verificationKey, - false, - Permissions.default(), - key1.toPublicKey() + const tree = new LinkedMerkleTree( + new InMemoryMerkleTreeStorage(), + new InMemoryLinkedLeafStore() ); - accountUpdate.requireSignature(); - AccountUpdate.attachToTransaction(accountUpdate); - - AccountUpdate.createSigned(chain.testAccounts[0]).send({ - to: key1.toPublicKey(), - amount: 1e9, + const path = Path.fromKey( + PROTOKIT_FIELD_PREFIXES.OUTGOING_MESSAGE_BASE_PATH, + OutgoingMessageKey, + { + index: Field(0), + tokenId: contract.tokenId, + } + ); + const message = new Withdrawal({ + tokenId: TokenId.default, + amount: UInt64.from(1e8), + address: chain.testAccounts[1].key.toPublicKey(), }); - }); - - const proven = await tx - .sign([chain.testAccounts[0].key, key1, key2]) - .prove(); - const txId = await proven.send(); - await txId.wait(); - - const tree = new LinkedMerkleTree( - new InMemoryMerkleTreeStorage(), - new InMemoryLinkedLeafStore() - ); - const path = Path.fromKey( - PROTOKIT_FIELD_PREFIXES.OUTGOING_MESSAGE_BASE_PATH, - OutgoingMessageKey, - { - index: Field(0), - tokenId: contract.tokenId, - } - ); - const message = new Withdrawal({ - tokenId: TokenId.default, - amount: UInt64.from(1e8), - address: chain.testAccounts[1].key.toPublicKey(), - }); - const MessageType = createMessageStruct(Withdrawal); - const messageType = new WithdrawalMessageProcessor().getMessageType(); - - tree.setLeaf( - path.toBigInt(), - Poseidon.hash( - MessageType.toFields({ messageType, value: message }) - ).toBigInt() - ); - - const tx2 = await Mina.transaction(chain.testAccounts[0], async () => { - await contract.updateStateRoot(tree.getRoot()); - }); - const proven2 = await tx2 - .sign([chain.testAccounts[0].key, key1, key2]) - .prove(); - const txId2 = await proven2.send(); - await txId2.wait(); - - const treeWitness = tree.getReadWitness(path.toBigInt()); - - const tx3 = await Mina.transaction(chain.testAccounts[0], async () => { - const funded = await contract.rollupOutgoingMessages( - OutgoingMessageArgumentBatch.fromMessages([ - new OutgoingMessageArgument({ - messageType, - witness: treeWitness, - data: Unconstrained.from(Withdrawal.toFields(message)), - }), - ]) + const MessageType = createMessageStruct(Withdrawal); + const messageType = new WithdrawalMessageProcessor().getMessageType(); + + tree.setLeaf( + path.toBigInt(), + Poseidon.hash( + MessageType.toFields({ messageType, value: message }) + ).toBigInt() ); - let numNewAccountsNumber = 0; - Provable.asProver(() => { - numNewAccountsNumber = parseInt(funded.toString(), 10); + const tx2 = await Mina.transaction(chain.testAccounts[0], async () => { + await contract.updateStateRoot(tree.getRoot()); }); - - // Pay account creation fees for internal token accounts - AccountUpdate.fundNewAccount(chain.testAccounts[0], numNewAccountsNumber); - }); - const proven3 = await tx3 - .sign([chain.testAccounts[0].key, key1, key2]) - .prove(); - const txId3 = await proven3.send(); - await txId3.wait(); - - const settlementAccount = chain.getAccount( - chain.testAccounts[1].key.toPublicKey(), - contract.deriveTokenId() - ); - expect(settlementAccount.balance.toString()).toBe((1e8).toString()); - }, 300000); + const proven2 = await tx2 + .sign([chain.testAccounts[0].key, key1, key2]) + .prove(); + const txId2 = await proven2.send(); + await txId2.wait(); + + const treeWitness = tree.getReadWitness(path.toBigInt()); + + const tx3 = await Mina.transaction(chain.testAccounts[0], async () => { + const funded = await contract.rollupOutgoingMessages( + OutgoingMessageArgumentBatch.fromMessages([ + new OutgoingMessageArgument({ + messageType, + witness: treeWitness, + data: Unconstrained.from(Withdrawal.toFields(message)), + }), + ]) + ); + + let numNewAccountsNumber = 0; + Provable.asProver(() => { + numNewAccountsNumber = parseInt(funded.toString(), 10); + }); + + // Pay account creation fees for internal token accounts + AccountUpdate.fundNewAccount( + chain.testAccounts[0], + numNewAccountsNumber + ); + }); + const proven3 = await tx3 + .sign([chain.testAccounts[0].key, key1, key2]) + .prove(); + const txId3 = await proven3.send(); + await txId3.wait(); + + const settlementAccount = chain.getAccount( + chain.testAccounts[1].key.toPublicKey(), + contract.deriveTokenId() + ); + expect(settlementAccount.balance.toString()).toBe((1e8).toString()); + }, + timeout * 3 + ); }); diff --git a/packages/sequencer/test/settlement/Settlement.test.ts b/packages/sequencer/test/settlement/Settlement.test.ts index ab674095a..b1aa7044a 100644 --- a/packages/sequencer/test/settlement/Settlement.test.ts +++ b/packages/sequencer/test/settlement/Settlement.test.ts @@ -5,7 +5,7 @@ import { MinaBaseLayerConfig } from "../../src"; import { settlementTestFn } from "./Settlement"; import { settlementOnlyTestFn } from "./Settlement-only"; -describe.each(["mock-proofs" /*, "signed"*/] as const)( +describe.each(["mock-proofs", "signed"] as const)( "Settlement contracts: local blockchain - %s", (type) => { const network: MinaBaseLayerConfig = { @@ -14,7 +14,7 @@ describe.each(["mock-proofs" /*, "signed"*/] as const)( }, }; - describe.only("Default token", () => { + describe("Default token", () => { settlementTestFn(type, network); });