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/module/src/messages/OutgoingMessages.ts b/packages/module/src/messages/OutgoingMessages.ts index 1790c89a3..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"; @@ -112,7 +113,8 @@ export class OutgoingMessages< const counter = counterOption.orElse(Field(0)); const messageKey = { index: counter, tokenId }; - const messageType = prefixToField(key); + // TODO Salt/prefix + 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 f56f8ec95..356919f69 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,16 +61,10 @@ export class OutgoingMessageKey extends Struct({ tokenId: Field, }) {} -@injectable() -@singleton() -export class BridgeContractContext { - public data: { - messageInputs: any[][]; - } = { messageInputs: [] }; -} - export interface BridgeContractArgs { - SettlementContract: TypedClass & + SettlementContract: TypedClass< + Pick + > & typeof SmartContract; messageProcessors: OutgoingMessageProcessor[]; batchSize?: number; @@ -171,40 +165,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, }); + const result = processor.processMessage(value, { + bridgeContract: { + publicKey: this.address, + tokenId: this.tokenId, + }, + }); return { - 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, @@ -290,7 +288,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( @@ -302,6 +300,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/protocol/src/settlement/messages/OutgoingMessageArgument.ts b/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts index eebdf1e6a..e36e750f0 100644 --- a/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts +++ b/packages/protocol/src/settlement/messages/OutgoingMessageArgument.ts @@ -1,4 +1,11 @@ -import { Bool, Field, FlexibleProvablePure, Provable, Struct } from "o1js"; +import { + Bool, + Field, + FlexibleProvablePure, + Provable, + Struct, + Unconstrained, +} from "o1js"; import { LinkedMerkleTree, LinkedMerkleTreeReadWitness, @@ -19,11 +26,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 new file mode 100644 index 000000000..cc186d6f8 --- /dev/null +++ b/packages/sdk/test/bridgeContract/bridge-contract.test.ts @@ -0,0 +1,199 @@ +import "reflect-metadata"; +import { + BridgeContract, + BridgeContractArgs, + BridgingSettlementContractType, + ContractArgsRegistry, + createMessageStruct, + OutgoingMessageArgument, + OutgoingMessageArgumentBatch, + OutgoingMessageKey, + Path, + PROTOKIT_FIELD_PREFIXES, +} from "@proto-kit/protocol"; +import { + AccountUpdate, + Mina, + PrivateKey, + SmartContract, + state, + State, + Permissions, + TokenId, + Field, + UInt64, + Poseidon, + Provable, + method, + Unconstrained, + VerificationKey, +} from "o1js"; +import { container } from "tsyringe"; +import { Withdrawal, WithdrawalMessageProcessor } from "@proto-kit/library"; +import { + InMemoryLinkedLeafStore, + InMemoryMerkleTreeStorage, + LinkedMerkleTree, + noop, +} from "@proto-kit/common"; +import { beforeAll } from "@jest/globals"; + +class MockSettlementContract + extends SmartContract + implements Pick +{ + @state(Field) root = State(Field(100)); + + public assertStateRoot(root: Field): AccountUpdate { + return this.self; + } + + @method + public async test() { + noop(); + } +} + +const proofsEnabled = false; + +const timeout = proofsEnabled ? 180_000 : 150_000; + +describe("bridging contract", () => { + 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 vks: { verificationKey: VerificationKey }[] = []; + + it( + "compile", + async () => { + const vkSettlement = await MockSettlementContract.compile(); + const vk = await BridgeContract.compile(); + vks.push(vkSettlement, vk); + }, + 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); + + 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, + }); + }); + + 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)), + }), + ]) + ); + + 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/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index a575c5e3a..6e00c29bf 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().equals(message.messageType).toBoolean() + ); + 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,