diff --git a/packages/common/src/compiling/AtomicCompileHelper.ts b/packages/common/src/compiling/AtomicCompileHelper.ts new file mode 100644 index 00000000..19bdff07 --- /dev/null +++ b/packages/common/src/compiling/AtomicCompileHelper.ts @@ -0,0 +1,62 @@ +import { + AreProofsEnabled, + CompileArtifact, + MOCK_VERIFICATION_KEY, +} from "../zkProgrammable/ZkProgrammable"; +import { isSubtypeOfName } from "../utils"; +import { TypedClass } from "../types"; +import { log } from "../log"; + +export type ArtifactRecord = Record; + +export type CompileTarget = { + name: string; + compile: () => Promise; +}; + +export class AtomicCompileHelper { + public constructor(private readonly areProofsEnabled: AreProofsEnabled) {} + + private compilationPromises: { + [key: string]: Promise; + } = {}; + + public async compileContract( + contract: CompileTarget, + overrideProofsEnabled?: boolean + ): Promise { + let newPromise = false; + const { name } = contract; + if (this.compilationPromises[name] === undefined) { + const proofsEnabled = + overrideProofsEnabled ?? this.areProofsEnabled.areProofsEnabled; + + // We only care about proofs enabled here if it's a contract, because + // in all other cases, ZkProgrammable already handles this switch, and we + // want to preserve the artifact layout (which might be more than one + // entry for ZkProgrammables) + if ( + proofsEnabled || + !isSubtypeOfName( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + contract as unknown as TypedClass, + "SmartContract" + ) + ) { + log.time(`Compiling ${name}`); + this.compilationPromises[name] = contract.compile(); + newPromise = true; + } else { + log.trace(`Compiling ${name} - mock`); + this.compilationPromises[name] = Promise.resolve({ + verificationKey: MOCK_VERIFICATION_KEY, + }); + } + } + const result = await this.compilationPromises[name]; + if (newPromise) { + log.timeEnd.info(`Compiling ${name}`); + } + return result; + } +} diff --git a/packages/common/src/compiling/CompilableModule.ts b/packages/common/src/compiling/CompilableModule.ts new file mode 100644 index 00000000..ba695aeb --- /dev/null +++ b/packages/common/src/compiling/CompilableModule.ts @@ -0,0 +1,6 @@ +import type { CompileRegistry } from "./CompileRegistry"; +import type { ArtifactRecord } from "./AtomicCompileHelper"; + +export interface CompilableModule { + compile(registry: CompileRegistry): Promise; +} diff --git a/packages/common/src/compiling/CompileRegistry.ts b/packages/common/src/compiling/CompileRegistry.ts new file mode 100644 index 00000000..c43c50a9 --- /dev/null +++ b/packages/common/src/compiling/CompileRegistry.ts @@ -0,0 +1,61 @@ +import { inject, injectable, singleton } from "tsyringe"; + +import { AreProofsEnabled } from "../zkProgrammable/ZkProgrammable"; + +import { + ArtifactRecord, + AtomicCompileHelper, + CompileTarget, +} from "./AtomicCompileHelper"; + +/** + * The CompileRegistry compiles "compilable modules" + * (i.e. zkprograms, contracts or contractmodules) + * while making sure they don't get compiled twice in the same process in parallel. + */ +@injectable() +@singleton() +export class CompileRegistry { + public constructor( + @inject("AreProofsEnabled") + private readonly areProofsEnabled: AreProofsEnabled + ) { + this.compiler = new AtomicCompileHelper(this.areProofsEnabled); + } + + private compiler: AtomicCompileHelper; + + private artifacts: ArtifactRecord = {}; + + // TODO Add possibility to force recompilation for non-sideloaded dependencies + + public async compile(target: CompileTarget) { + if (this.artifacts[target.name] === undefined) { + const artifact = await this.compiler.compileContract(target); + this.artifacts[target.name] = artifact; + return artifact; + } + return this.artifacts[target.name]; + } + + public getArtifact(name: string) { + if (this.artifacts[name] === undefined) { + throw new Error( + `Artifact for ${name} not available, did you compile it via the CompileRegistry?` + ); + } + + return this.artifacts[name]; + } + + public addArtifactsRaw(artifacts: ArtifactRecord) { + this.artifacts = { + ...this.artifacts, + ...artifacts, + }; + } + + public getAllArtifacts() { + return this.artifacts; + } +} diff --git a/packages/common/src/compiling/services/ChildVerificationKeyService.ts b/packages/common/src/compiling/services/ChildVerificationKeyService.ts new file mode 100644 index 00000000..1e90738d --- /dev/null +++ b/packages/common/src/compiling/services/ChildVerificationKeyService.ts @@ -0,0 +1,26 @@ +import { injectable, Lifecycle, scoped } from "tsyringe"; + +import { CompileRegistry } from "../CompileRegistry"; + +@injectable() +@scoped(Lifecycle.ContainerScoped) +export class ChildVerificationKeyService { + private compileRegistry?: CompileRegistry; + + public setCompileRegistry(registry: CompileRegistry) { + this.compileRegistry = registry; + } + + public getVerificationKey(name: string) { + if (this.compileRegistry === undefined) { + throw new Error("CompileRegistry hasn't been set yet"); + } + const artifact = this.compileRegistry.getArtifact(name); + if (artifact === undefined) { + throw new Error( + `Verification Key for child program ${name} not found in registry` + ); + } + return artifact.verificationKey; + } +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 42f98934..9b293c8e 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -18,3 +18,7 @@ export * from "./trees/RollupMerkleTree"; export * from "./events/EventEmitterProxy"; export * from "./events/ReplayingSingleUseEventEmitter"; export * from "./trees/MockAsyncMerkleStore"; +export * from "./compiling/AtomicCompileHelper"; +export * from "./compiling/CompileRegistry"; +export * from "./compiling/CompilableModule"; +export * from "./compiling/services/ChildVerificationKeyService"; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index e81f0151..4d25b119 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,5 +1,5 @@ // allows to reference interfaces as 'classes' rather than instances -import { Bool, Field, PublicKey } from "o1js"; +import { Bool, DynamicProof, Field, Proof, ProofBase, PublicKey } from "o1js"; export type TypedClass = new (...args: any[]) => Class; @@ -47,3 +47,12 @@ export const EMPTY_PUBLICKEY = PublicKey.fromObject({ export type OverwriteObjectType = { [Key in keyof Base]: Key extends keyof New ? New[Key] : Base[Key]; } & New; + +export type InferProofBase< + ProofType extends Proof | DynamicProof, +> = + ProofType extends Proof + ? ProofBase + : ProofType extends DynamicProof + ? ProofBase + : undefined; diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index e79169ba..23b351d0 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -6,6 +6,8 @@ import { Proof, } from "o1js"; +import { TypedClass } from "./types"; + export function requireTrue( condition: boolean, errorOrFunction: Error | (() => Error) @@ -56,7 +58,7 @@ export function reduceSequential( array: T[] ) => Promise, initialValue: U -) { +): Promise { return array.reduce>( async (previousPromise, current, index, arr) => { const previous = await previousPromise; @@ -164,3 +166,25 @@ type NonMethodKeys = { [Key in keyof Type]: Type[Key] extends Function ? never : Key; }[keyof Type]; export type NonMethods = Pick>; + +/** + * Returns a boolean indicating whether a given class is a subclass of another class, + * indicated by the name parameter. + */ +// TODO Change to class reference based comparisons +export function isSubtypeOfName( + clas: TypedClass, + name: string +): boolean { + if (clas.name === name) { + return true; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return isSubtypeOfName(Object.getPrototypeOf(clas), name); +} + +// TODO Eventually, replace this by a schema validation library +export function safeParseJson(json: string) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return JSON.parse(json) as T; +} diff --git a/packages/common/src/zkProgrammable/ZkProgrammable.ts b/packages/common/src/zkProgrammable/ZkProgrammable.ts index 0b5f36fc..fc728bf3 100644 --- a/packages/common/src/zkProgrammable/ZkProgrammable.ts +++ b/packages/common/src/zkProgrammable/ZkProgrammable.ts @@ -3,6 +3,8 @@ import { Memoize } from "typescript-memoize"; import { log } from "../log"; import { dummyVerificationKey } from "../dummyVerificationKey"; +import { reduceSequential } from "../utils"; +import type { CompileRegistry } from "../compiling/CompileRegistry"; import { MOCK_PROOF } from "./provableMethod"; @@ -32,6 +34,7 @@ export interface Compile { } export interface PlainZkProgram { + name: string; compile: Compile; verify: Verify; Proof: ReturnType< @@ -72,8 +75,6 @@ export function verifyToMockable( return verified; } - console.log("VerifyMocked"); - return proof.proof === MOCK_PROOF; }; } @@ -125,6 +126,21 @@ export abstract class ZkProgrammable< }; }); } + + public async compile(registry: CompileRegistry) { + return await reduceSequential( + this.zkProgram, + async (acc, program) => { + const result = await registry.compile(program); + return { + ...acc, + [program.name]: result, + }; + }, + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + {} as Record + ); + } } export interface WithZkProgrammable< diff --git a/packages/module/src/runtime/Runtime.ts b/packages/module/src/runtime/Runtime.ts index 32d9050c..264c57e8 100644 --- a/packages/module/src/runtime/Runtime.ts +++ b/packages/module/src/runtime/Runtime.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument */ import { ZkProgram } from "o1js"; -import { DependencyContainer, injectable } from "tsyringe"; +import { container, DependencyContainer, injectable } from "tsyringe"; import { StringKeyOf, ModuleContainer, @@ -11,11 +11,16 @@ import { PlainZkProgram, AreProofsEnabled, ChildContainerProvider, + CompilableModule, + CompileRegistry, } from "@proto-kit/common"; import { MethodPublicOutput, StateServiceProvider, SimpleAsyncStateService, + RuntimeMethodExecutionContext, + RuntimeTransaction, + NetworkState, } from "@proto-kit/protocol"; import { @@ -227,9 +232,10 @@ export class RuntimeZkProgrammable< return buckets; }; - return splitRuntimeMethods().map((bucket) => { + return splitRuntimeMethods().map((bucket, index) => { + const name = `RuntimeProgram-${index}`; const program = ZkProgram({ - name: "RuntimeProgram", + name, publicOutput: MethodPublicOutput, methods: bucket, }); @@ -245,6 +251,7 @@ export class RuntimeZkProgrammable< ); return { + name, compile: program.compile.bind(program), verify: program.verify.bind(program), analyzeMethods: program.analyzeMethods.bind(program), @@ -262,7 +269,7 @@ export class RuntimeZkProgrammable< @injectable() export class Runtime extends ModuleContainer - implements RuntimeEnvironment + implements RuntimeEnvironment, CompilableModule { public static from( definition: RuntimeDefinition @@ -375,5 +382,14 @@ export class Runtime public get runtimeModuleNames() { return Object.keys(this.definition.modules); } + + public async compile(registry: CompileRegistry) { + const context = container.resolve(RuntimeMethodExecutionContext); + context.setup({ + transaction: RuntimeTransaction.dummyTransaction(), + networkState: NetworkState.empty(), + }); + return await this.zkProgrammable.compile(registry); + } } /* eslint-enable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument */ diff --git a/packages/persistance/test-integration/SequencerRestart.test.ts b/packages/persistance/test-integration/SequencerRestart.test.ts index dfb7b998..b3e701a8 100644 --- a/packages/persistance/test-integration/SequencerRestart.test.ts +++ b/packages/persistance/test-integration/SequencerRestart.test.ts @@ -32,7 +32,7 @@ describe("sequencer restart", () => { }, }); - await appChain.start(container.createChildContainer()); + await appChain.start(false, container.createChildContainer()); }; const teardown = async () => { diff --git a/packages/protocol/src/prover/block/BlockProvable.ts b/packages/protocol/src/prover/block/BlockProvable.ts index 74c88a34..1556adea 100644 --- a/packages/protocol/src/prover/block/BlockProvable.ts +++ b/packages/protocol/src/prover/block/BlockProvable.ts @@ -7,7 +7,7 @@ import { Struct, Void, } from "o1js"; -import { WithZkProgrammable } from "@proto-kit/common"; +import { WithZkProgrammable, CompilableModule } from "@proto-kit/common"; import { StateTransitionProof } from "../statetransition/StateTransitionProvable"; import { MethodPublicOutput } from "../../model/MethodPublicOutput"; @@ -77,7 +77,8 @@ export class DynamicRuntimeProof extends DynamicProof< } export interface BlockProvable - extends WithZkProgrammable { + extends WithZkProgrammable, + CompilableModule { proveTransaction: ( publicInput: BlockProverPublicInput, stateProof: StateTransitionProof, diff --git a/packages/protocol/src/prover/block/BlockProver.ts b/packages/protocol/src/prover/block/BlockProver.ts index 3432a577..344dad06 100644 --- a/packages/protocol/src/prover/block/BlockProver.ts +++ b/packages/protocol/src/prover/block/BlockProver.ts @@ -12,6 +12,9 @@ import { import { container, inject, injectable, injectAll } from "tsyringe"; import { AreProofsEnabled, + CompilableModule, + CompileArtifact, + CompileRegistry, PlainZkProgram, provableMethod, WithZkProgrammable, @@ -23,6 +26,7 @@ import { MethodPublicOutput } from "../../model/MethodPublicOutput"; import { ProtocolModule } from "../../protocol/ProtocolModule"; import { StateTransitionProof, + StateTransitionProvable, StateTransitionProverPublicInput, StateTransitionProverPublicOutput, } from "../statetransition/StateTransitionProvable"; @@ -149,6 +153,8 @@ export class BlockProverProgrammable extends ZkProgrammable< super(); } + name = "BlockProver"; + public get areProofsEnabled(): AreProofsEnabled | undefined { return this.prover.areProofsEnabled; } @@ -872,6 +878,7 @@ export class BlockProverProgrammable extends ZkProgrammable< return [ { + name: program.name, compile: program.compile.bind(program), verify: program.verify.bind(program), analyzeMethods: program.analyzeMethods.bind(program), @@ -888,7 +895,10 @@ export class BlockProverProgrammable extends ZkProgrammable< * then be merged to be committed to the base-layer contract */ @injectable() -export class BlockProver extends ProtocolModule implements BlockProvable { +export class BlockProver + extends ProtocolModule + implements BlockProvable, CompilableModule +{ public zkProgrammable: BlockProverProgrammable; public constructor( @@ -896,9 +906,11 @@ export class BlockProver extends ProtocolModule implements BlockProvable { public readonly stateTransitionProver: WithZkProgrammable< StateTransitionProverPublicInput, StateTransitionProverPublicOutput - >, + > & + StateTransitionProvable, @inject("Runtime") - public readonly runtime: WithZkProgrammable, + public readonly runtime: WithZkProgrammable & + CompilableModule, @injectAll("ProvableTransactionHook") transactionHooks: ProvableTransactionHook[], @injectAll("ProvableBlockHook") @@ -916,6 +928,15 @@ export class BlockProver extends ProtocolModule implements BlockProvable { ); } + public async compile( + registry: CompileRegistry + ): Promise | undefined> { + await this.stateTransitionProver.compile(registry); + await this.runtime.compile(registry); + + return await this.zkProgrammable.compile(registry); + } + public proveTransaction( publicInput: BlockProverPublicInput, stateProof: StateTransitionProof, diff --git a/packages/protocol/src/prover/statetransition/StateTransitionProvable.ts b/packages/protocol/src/prover/statetransition/StateTransitionProvable.ts index d6fc39c2..0b61e5ce 100644 --- a/packages/protocol/src/prover/statetransition/StateTransitionProvable.ts +++ b/packages/protocol/src/prover/statetransition/StateTransitionProvable.ts @@ -1,5 +1,5 @@ import { Field, Proof, Struct } from "o1js"; -import { WithZkProgrammable } from "@proto-kit/common"; +import { WithZkProgrammable, CompilableModule } from "@proto-kit/common"; import { StateTransitionProvableBatch } from "../../model/StateTransitionProvableBatch"; @@ -26,9 +26,10 @@ export type StateTransitionProof = Proof< export interface StateTransitionProvable extends WithZkProgrammable< - StateTransitionProverPublicInput, - StateTransitionProverPublicOutput - > { + StateTransitionProverPublicInput, + StateTransitionProverPublicOutput + >, + CompilableModule { witnessProviderReference: StateTransitionWitnessProviderReference; runBatch: ( diff --git a/packages/protocol/src/prover/statetransition/StateTransitionProver.ts b/packages/protocol/src/prover/statetransition/StateTransitionProver.ts index 2fcf143b..93a6fd7a 100644 --- a/packages/protocol/src/prover/statetransition/StateTransitionProver.ts +++ b/packages/protocol/src/prover/statetransition/StateTransitionProver.ts @@ -4,6 +4,9 @@ import { provableMethod, RollupMerkleTreeWitness, ZkProgrammable, + CompilableModule, + type ArtifactRecord, + type CompileRegistry, } from "@proto-kit/common"; import { Field, Provable, SelfProof, ZkProgram } from "o1js"; import { injectable } from "tsyringe"; @@ -134,6 +137,7 @@ export class StateTransitionProverProgrammable extends ZkProgrammable< return [ { + name: program.name, compile: program.compile.bind(program), verify: program.verify.bind(program), analyzeMethods: program.analyzeMethods.bind(program), @@ -345,7 +349,10 @@ export class StateTransitionProverProgrammable extends ZkProgrammable< @injectable() export class StateTransitionProver extends ProtocolModule - implements StateTransitionProvable, StateTransitionProverType + implements + StateTransitionProvable, + StateTransitionProverType, + CompilableModule { public zkProgrammable: StateTransitionProverProgrammable; @@ -360,6 +367,12 @@ export class StateTransitionProver ); } + public async compile( + registry: CompileRegistry + ): Promise { + return await this.zkProgrammable.compile(registry); + } + public runBatch( publicInput: StateTransitionProverPublicInput, batch: StateTransitionProvableBatch diff --git a/packages/protocol/src/settlement/ContractModule.ts b/packages/protocol/src/settlement/ContractModule.ts index a1116b25..29533337 100644 --- a/packages/protocol/src/settlement/ContractModule.ts +++ b/packages/protocol/src/settlement/ContractModule.ts @@ -1,5 +1,7 @@ import { - CompileArtifact, + ArtifactRecord, + type CompilableModule, + CompileRegistry, ConfigurableModule, NoConfig, TypedClass, @@ -17,11 +19,13 @@ export type SmartContractClassFromInterface = typeof SmartContract & * of SmartContract and implements a certain interface as specified by the * ContractType generic. */ -export abstract class ContractModule< - ContractType, - Config = NoConfig, -> extends ConfigurableModule { +export abstract class ContractModule + extends ConfigurableModule + implements CompilableModule +{ public abstract contractFactory(): SmartContractClassFromInterface; - public abstract compile(): Promise>; + public abstract compile( + registry: CompileRegistry + ): Promise; } diff --git a/packages/protocol/src/settlement/SettlementContractModule.ts b/packages/protocol/src/settlement/SettlementContractModule.ts index d832f2cb..931c1667 100644 --- a/packages/protocol/src/settlement/SettlementContractModule.ts +++ b/packages/protocol/src/settlement/SettlementContractModule.ts @@ -5,6 +5,7 @@ import { ModulesRecord, TypedClass, noop, + StringKeyOf, } from "@proto-kit/common"; import { Field, PublicKey, SmartContract } from "o1js"; import { injectable } from "tsyringe"; @@ -12,10 +13,7 @@ import { injectable } from "tsyringe"; import { ProtocolEnvironment } from "../protocol/ProtocolEnvironment"; import { ProtocolModule } from "../protocol/ProtocolModule"; -import { - ContractModule, - SmartContractClassFromInterface, -} from "./ContractModule"; +import { ContractModule } from "./ContractModule"; import { DispatchContractProtocolModule } from "./contracts/DispatchContractProtocolModule"; import { DispatchContractType } from "./contracts/DispatchSmartContract"; import { @@ -28,6 +26,7 @@ import { BridgeContractConfig, BridgeContractProtocolModule, } from "./contracts/BridgeContractProtocolModule"; +import { GetContracts } from "./modularity/types"; export type SettlementModulesRecord = ModulesRecord< TypedClass> @@ -107,32 +106,14 @@ export class SettlementContractModule< noop(); } - public getContractClasses(): { - settlement: SmartContractClassFromInterface; - dispatch: SmartContractClassFromInterface; - bridge: SmartContractClassFromInterface; - } { - // TODO Make that dynamic - const settlementContractKey = "SettlementContract"; - const dispatchContractKey = "DispatchContract"; - const bridgeContractKey = "BridgeContract"; - this.assertIsValidModuleName(settlementContractKey); - this.assertIsValidModuleName(dispatchContractKey); - this.assertIsValidModuleName(bridgeContractKey); - - const settlementModule = this.resolve(settlementContractKey); - const dispatchModule = this.resolve(dispatchContractKey); - const bridgeModule = this.resolve(bridgeContractKey); - - const dispatch = dispatchModule.contractFactory(); - const bridge = bridgeModule.contractFactory(); - const settlement = settlementModule.contractFactory(); - - return { - settlement, - dispatch, - bridge, - }; + public getContractClasses(): GetContracts { + const contracts = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + (this.moduleNames as StringKeyOf[]).map((name) => { + const module = this.resolve(name); + return [name, module.contractFactory()]; + }); + return Object.fromEntries(contracts); } public createContracts(addresses: { @@ -142,12 +123,10 @@ export class SettlementContractModule< settlement: SettlementContractType & SmartContract; dispatch: DispatchContractType & SmartContract; } { - const { dispatch, settlement } = this.getContractClasses(); + const { DispatchContract, SettlementContract } = this.getContractClasses(); - // eslint-disable-next-line new-cap - const dispatchInstance = new dispatch(addresses.dispatch); - // eslint-disable-next-line new-cap - const settlementInstance = new settlement(addresses.settlement); + const dispatchInstance = new DispatchContract(addresses.dispatch); + const settlementInstance = new SettlementContract(addresses.settlement); return { dispatch: dispatchInstance, @@ -159,9 +138,8 @@ export class SettlementContractModule< address: PublicKey, tokenId?: Field ): BridgeContractType & SmartContract { - const { bridge } = this.getContractClasses(); + const { BridgeContract } = this.getContractClasses(); - // eslint-disable-next-line new-cap - return new bridge(address, tokenId); + return new BridgeContract(address, tokenId); } } diff --git a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts index 0f76593a..00dd2de4 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts @@ -1,4 +1,5 @@ import { injectable } from "tsyringe"; +import { CompileRegistry } from "@proto-kit/common"; import { ContractModule } from "../ContractModule"; @@ -28,16 +29,15 @@ export class BridgeContractProtocolModule extends ContractModule< BridgeContractBase.args = { withdrawalStatePath: withdrawalStatePathSplit, - SettlementContract: undefined, + SettlementContract: BridgeContractBase.args?.SettlementContract, }; return BridgeContract; } - public async compile() { - const bridgeVK = await BridgeContract.compile(); + public async compile(registry: CompileRegistry) { return { - BridgeContract: bridgeVK, + BridgeContract: await registry.compile(BridgeContract), }; } } diff --git a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts index 32abac3b..2b7dc32f 100644 --- a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts @@ -1,7 +1,8 @@ import { inject, injectable } from "tsyringe"; import { PublicKey } from "o1js"; +import { CompileRegistry } from "@proto-kit/common"; -import { RuntimeLike } from "../../model/RuntimeLike"; +import { RuntimeLike, RuntimeMethodIdMapping } from "../../model/RuntimeLike"; import { ContractModule, SmartContractClassFromInterface, @@ -31,22 +32,42 @@ export class DispatchContractProtocolModule extends ContractModule< .events; } + private checkConfigIntegrity( + incomingMessagesMethods: Record, + runtimeMethodIds: RuntimeMethodIdMapping + ) { + const missing = Object.values(incomingMessagesMethods).filter( + (method) => runtimeMethodIds[method] === undefined + ); + if (missing.length > 0) { + throw new Error( + `Incoming messages config references a unknown methods: [${missing}]` + ); + } + } + public contractFactory(): SmartContractClassFromInterface { const { incomingMessagesMethods } = this.config; const methodIdMappings = this.runtime.methodIdResolver.methodIdMap(); + this.checkConfigIntegrity(incomingMessagesMethods, methodIdMappings); + DispatchSmartContractBase.args = { incomingMessagesPaths: incomingMessagesMethods, methodIdMappings, + settlementContractClass: + DispatchSmartContractBase.args?.settlementContractClass, }; return DispatchSmartContract; } - public async compile() { - const contractVk = await DispatchSmartContract.compile(); + public async compile(registry: CompileRegistry) { + if (DispatchSmartContractBase.args.settlementContractClass === undefined) { + throw new Error("Reference to Settlement Contract not set"); + } return { - DispatchSmartContract: contractVk, + DispatchSmartContract: await registry.compile(DispatchSmartContract), }; } } diff --git a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts index 3b4a7c17..b4480808 100644 --- a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts @@ -1,5 +1,10 @@ -import { CompileArtifact } from "@proto-kit/common"; import { inject, injectable, injectAll } from "tsyringe"; +import { + ArtifactRecord, + ChildVerificationKeyService, + CompileRegistry, + log, +} from "@proto-kit/common"; import { BlockProvable } from "../../prover/block/BlockProvable"; import { @@ -10,7 +15,6 @@ import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; import { DispatchSmartContractBase } from "./DispatchSmartContract"; import { - LazyBlockProof, SettlementContractType, SettlementSmartContract, SettlementSmartContractBase, @@ -35,13 +39,13 @@ export class SettlementContractProtocolModule extends ContractModule< @injectAll("ProvableSettlementHook") private readonly hooks: ProvableSettlementHook[], @inject("BlockProver") - blockProver: BlockProvable, + private readonly blockProver: BlockProvable, @inject("DispatchContract") private readonly dispatchContractModule: DispatchContractProtocolModule, @inject("BridgeContract") - private readonly bridgeContractModule: BridgeContractProtocolModule + private readonly bridgeContractModule: BridgeContractProtocolModule, + private readonly childVerificationKeyService: ChildVerificationKeyService ) { - LazyBlockProof.tag = blockProver.zkProgrammable.zkProgram[0].Proof.tag; super(); } @@ -62,6 +66,7 @@ export class SettlementContractProtocolModule extends ContractModule< BridgeContractVerificationKey: args?.BridgeContractVerificationKey, BridgeContractPermissions: args?.BridgeContractPermissions, signedSettlements: args?.signedSettlements, + ChildVerificationKeyService: this.childVerificationKeyService, }; // Ideally we don't want to have this cyclic dependency, but we have it in the protocol, @@ -74,10 +79,30 @@ export class SettlementContractProtocolModule extends ContractModule< return SettlementSmartContract; } - public async compile(): Promise> { - const settlementVK = await SettlementSmartContract.compile(); + public async compile( + registry: CompileRegistry + ): Promise { + // Dependencies + const bridgeArtifact = await this.bridgeContractModule.compile(registry); + + await this.blockProver.compile(registry); + + // Init params + SettlementSmartContractBase.args.BridgeContractVerificationKey = + bridgeArtifact.BridgeContract.verificationKey; + + if (SettlementSmartContractBase.args.signedSettlements === undefined) { + throw new Error( + "Args not fully initialized - make sure to also include the SettlementModule in the sequencer" + ); + } + + log.debug("Compiling Settlement Contract"); + + const artifact = await registry.compile(SettlementSmartContract); + return { - SettlementContract: settlementVK, + SettlementSmartContract: artifact, }; } } diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts index cf6cb276..3f1fe412 100644 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts @@ -3,13 +3,13 @@ import { RollupMerkleTree, TypedClass, mapSequential, + ChildVerificationKeyService, } from "@proto-kit/common"; import { AccountUpdate, Bool, Field, method, - Proof, PublicKey, Signature, SmartContract, @@ -24,8 +24,8 @@ import { Struct, Provable, TokenId, + DynamicProof, } from "o1js"; -import { singleton } from "tsyringe"; import { NetworkState } from "../../model/network/NetworkState"; import { BlockHashMerkleTree } from "../../prover/block/accummulators/BlockHashMerkleTree"; @@ -46,7 +46,7 @@ import { UpdateMessagesHashAuth } from "./authorizations/UpdateMessagesHashAuth" /* eslint-disable @typescript-eslint/lines-between-class-members */ -export class LazyBlockProof extends Proof< +export class DynamicBlockProof extends DynamicProof< BlockProverPublicInput, BlockProverPublicOutput > { @@ -54,9 +54,7 @@ export class LazyBlockProof extends Proof< public static publicOutputType = BlockProverPublicOutput; - public static tag: () => { name: string } = () => { - throw new Error("Tag not initialized yet"); - }; + public static maxProofsVerified = 2 as const; } export class TokenMapping extends Struct({ @@ -75,7 +73,7 @@ export interface SettlementContractType { ) => Promise; assertStateRoot: (root: Field) => AccountUpdate; settle: ( - blockProof: LazyBlockProof, + blockProof: DynamicBlockProof, signature: Signature, dispatchContractAddress: PublicKey, publicKey: PublicKey, @@ -93,19 +91,19 @@ export interface SettlementContractType { // Some random prefix for the sequencer signature export const BATCH_SIGNATURE_PREFIX = prefixToField("pk-batchSignature"); -@singleton() -export class SettlementSmartContractStaticArgs { - public args?: { - DispatchContract: TypedClass; - hooks: ProvableSettlementHook[]; - escapeHatchSlotsInterval: number; - BridgeContract: TypedClass & typeof SmartContract; - // Lazily initialized - BridgeContractVerificationKey: VerificationKey | undefined; - BridgeContractPermissions: Permissions | undefined; - signedSettlements: boolean | undefined; - }; -} +// @singleton() +// export class SettlementSmartContractStaticArgs { +// public args?: { +// DispatchContract: TypedClass; +// hooks: ProvableSettlementHook[]; +// escapeHatchSlotsInterval: number; +// BridgeContract: TypedClass & typeof SmartContract; +// // Lazily initialized +// BridgeContractVerificationKey: VerificationKey | undefined; +// BridgeContractPermissions: Permissions | undefined; +// signedSettlements: boolean | undefined; +// }; +// } export abstract class SettlementSmartContractBase extends TokenContractV2 { // This pattern of injecting args into a smartcontract is currently the only @@ -120,10 +118,10 @@ export abstract class SettlementSmartContractBase extends TokenContractV2 { BridgeContractVerificationKey: VerificationKey | undefined; BridgeContractPermissions: Permissions | undefined; signedSettlements: boolean | undefined; + ChildVerificationKeyService: ChildVerificationKeyService; }; events = { - "announce-private-key": PrivateKey, "token-bridge-deployed": TokenMapping, }; @@ -177,6 +175,28 @@ export abstract class SettlementSmartContractBase extends TokenContractV2 { const BridgeContractClass = args.BridgeContract; const bridgeContract = new BridgeContractClass(address, tokenId); + const { + BridgeContractVerificationKey, + signedSettlements, + BridgeContractPermissions, + } = args; + + if ( + signedSettlements === undefined || + BridgeContractPermissions === undefined + ) { + throw new Error( + "Static arguments for SettlementSmartContract not initialized" + ); + } + + if ( + BridgeContractVerificationKey !== undefined && + !BridgeContractVerificationKey.hash.isConstant() + ) { + throw new Error("Bridge contract verification key has to be constants"); + } + // This function is not a zkapps method, therefore it will be part of this methods execution // The returning account update (owner.self) is therefore part of this circuit and is assertable const deploymentAccountUpdate = await bridgeContract.deployProvable( @@ -254,11 +274,10 @@ export abstract class SettlementSmartContractBase extends TokenContractV2 { ); contractKey.toPublicKey().assertEquals(this.address); - this.emitEvent("announce-private-key", contractKey); } protected async settleBase( - blockProof: LazyBlockProof, + blockProof: DynamicBlockProof, signature: Signature, dispatchContractAddress: PublicKey, publicKey: PublicKey, @@ -266,8 +285,17 @@ export abstract class SettlementSmartContractBase extends TokenContractV2 { outputNetworkState: NetworkState, newPromisedMessagesHash: Field ) { + // Brought in as a constant + const blockProofVk = + SettlementSmartContractBase.args.ChildVerificationKeyService.getVerificationKey( + "BlockProver" + ); + if (!blockProofVk.hash.isConstant()) { + throw new Error("Sanity check - vk hash has to be constant"); + } + // Verify the blockproof - blockProof.verify(); + blockProof.verify(blockProofVk); // Get and assert on-chain values const stateRoot = this.stateRoot.getAndRequireEquals(); @@ -447,7 +475,7 @@ export class SettlementSmartContract @method public async settle( - blockProof: LazyBlockProof, + blockProof: DynamicBlockProof, signature: Signature, dispatchContractAddress: PublicKey, publicKey: PublicKey, diff --git a/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts b/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts index 1a579ea3..ecb11712 100644 --- a/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts +++ b/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts @@ -1,10 +1,13 @@ import { Field, PublicKey, UInt32 } from "o1js"; +import { InferProofBase } from "@proto-kit/common"; import { ProtocolModule } from "../../protocol/ProtocolModule"; import { NetworkState } from "../../model/network/NetworkState"; import type { BlockProof } from "../../prover/block/BlockProver"; import type { SettlementSmartContractBase } from "../contracts/SettlementSmartContract"; +export type InputBlockProof = InferProofBase; + export type SettlementStateRecord = { sequencerKey: PublicKey; lastSettlementL1BlockHeight: UInt32; @@ -15,7 +18,7 @@ export type SettlementStateRecord = { }; export type SettlementHookInputs = { - blockProof: BlockProof; + blockProof: InputBlockProof; fromNetworkState: NetworkState; toNetworkState: NetworkState; newPromisedMessagesHash: Field; diff --git a/packages/protocol/src/settlement/modularity/types.ts b/packages/protocol/src/settlement/modularity/types.ts new file mode 100644 index 00000000..8f7c706e --- /dev/null +++ b/packages/protocol/src/settlement/modularity/types.ts @@ -0,0 +1,22 @@ +import { TypedClass } from "@proto-kit/common"; + +import { + ContractModule, + SmartContractClassFromInterface, +} from "../ContractModule"; +import type { SettlementModulesRecord } from "../SettlementContractModule"; + +export type InferContractType< + Module extends TypedClass>, +> = + Module extends TypedClass + ? ConcreteModule extends ContractModule + ? Contract + : never + : never; + +export type GetContracts = { + [Key in keyof SettlementModules]: SmartContractClassFromInterface< + InferContractType + >; +}; diff --git a/packages/protocol/test/compiling/types.ts b/packages/protocol/test/compiling/types.ts new file mode 100644 index 00000000..fe1f231a --- /dev/null +++ b/packages/protocol/test/compiling/types.ts @@ -0,0 +1,28 @@ +import { Field, VerificationKey } from "o1js"; + +import { CompileRegistry, InferDependencyArtifacts } from "../../src"; +/* eslint-disable @typescript-eslint/no-unused-vars */ + +type TestModule = { + compile( + registry: CompileRegistry + ): Promise<{ bar: { verificationKey: VerificationKey } }>; +}; + +type Inferred = InferDependencyArtifacts<{ foo: TestModule }>; +const typeAssignmentTest: Inferred = { + foo: { + bar: { + verificationKey: { + data: "", + hash: Field(1), + }, + }, + }, +}; + +const typePropertyTest: Inferred["foo"]["bar"]["verificationKey"] extends VerificationKey + ? true + : false = true; + +/* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/packages/protocol/test/modularity/types.ts b/packages/protocol/test/modularity/types.ts new file mode 100644 index 00000000..233214b1 --- /dev/null +++ b/packages/protocol/test/modularity/types.ts @@ -0,0 +1,35 @@ +import { SmartContract } from "o1js"; +import { TypedClass } from "@proto-kit/common"; + +import { GetContracts } from "../../src/settlement/modularity/types"; +import { + BridgeContractType, + MandatorySettlementModulesRecord, + SettlementContractType, +} from "../../src"; +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// Goal of this "test" is that it compiles. By compiling this file checks that +// certain types are inferred correctly + +type Inferred = GetContracts; + +// Get inferred Bridge Type +type Bridge = Inferred["BridgeContract"]; +// Get inferred Settlement Contract Type +type Settlement = Inferred["SettlementContract"]; + +// Check that the Bridge type is of the correct type +const bridgeSuccessful: Bridge extends TypedClass< + SmartContract & BridgeContractType +> + ? true + : false = true; + +const settlementSuccessful: Settlement extends TypedClass< + SmartContract & SettlementContractType +> + ? true + : false = true; + +/* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/packages/sdk/test/networkstate/NetworkState.test.ts b/packages/sdk/test/networkstate/NetworkState.test.ts index 36548f53..53d8182d 100644 --- a/packages/sdk/test/networkstate/NetworkState.test.ts +++ b/packages/sdk/test/networkstate/NetworkState.test.ts @@ -60,7 +60,7 @@ describe.skip("block production", () => { }); // Start AppChain - await app.start(container.createChildContainer()); + await app.start(false, container.createChildContainer()); appchain = app; ({ runtime, protocol } = app); diff --git a/packages/sequencer/src/index.ts b/packages/sequencer/src/index.ts index 2f9d4514..d06f4c8a 100644 --- a/packages/sequencer/src/index.ts +++ b/packages/sequencer/src/index.ts @@ -22,7 +22,6 @@ export * from "./protocol/baselayer/NoopBaseLayer"; export * from "./protocol/production/helpers/UntypedOption"; export * from "./protocol/production/helpers/UntypedStateTransition"; export * from "./protocol/production/tasks/BlockProvingTask"; -export * from "./protocol/production/helpers/CompileRegistry"; export * from "./protocol/production/tasks/RuntimeProvingTask"; export * from "./protocol/production/tasks/RuntimeTaskParameters"; export * from "./protocol/production/tasks/StateTransitionTask"; diff --git a/packages/sequencer/src/protocol/production/helpers/CompileRegistry.ts b/packages/sequencer/src/protocol/production/helpers/CompileRegistry.ts deleted file mode 100644 index 3b21fd00..00000000 --- a/packages/sequencer/src/protocol/production/helpers/CompileRegistry.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { injectable, singleton } from "tsyringe"; -import { CompileArtifact, log } from "@proto-kit/common"; - -export type ContractCompileArtifact = Record; - -/** - * The CompileRegistry compiles "compilable modules" - * (i.e. zkprograms, contracts or contractmodules) - * while making sure they don't get compiled twice in the same process in parallel. - */ -@injectable() -@singleton() -export class CompileRegistry { - private compilationPromises: { - [key: string]: Promise; - } = {}; - - // Use only the compile interface here, to avoid type issues - public async compile( - name: string, - zkProgram: { compile: () => Promise } - ) { - let newPromise = false; - if (this.compilationPromises[name] === undefined) { - log.time(`Compiling ${name}`); - this.compilationPromises[name] = zkProgram.compile(); - newPromise = true; - } - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const result = (await this.compilationPromises[name]) as CompileArtifact; - if (newPromise) { - log.timeEnd.info(`Compiling ${name}`); - } - return result; - } - - public async compileSmartContract( - name: string, - contract: { - compile: () => Promise; - }, - proofsEnabled: boolean = true - ) { - let newPromise = false; - if (this.compilationPromises[name] === undefined) { - if (proofsEnabled) { - log.time(`Compiling ${name}`); - this.compilationPromises[name] = contract.compile(); - } else { - this.compilationPromises[name] = Promise.resolve({}); - } - } - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const result = (await this.compilationPromises[ - name - ]) as ContractCompileArtifact; - if (newPromise) { - log.timeEnd.info(`Compiling ${name}`); - } - return result; - } -} diff --git a/packages/sequencer/src/protocol/production/tasks/BlockProvingTask.ts b/packages/sequencer/src/protocol/production/tasks/BlockProvingTask.ts index 7b09c6fc..f7da13a9 100644 --- a/packages/sequencer/src/protocol/production/tasks/BlockProvingTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/BlockProvingTask.ts @@ -18,7 +18,10 @@ import { import { Field, Proof } from "o1js"; import { Runtime } from "@proto-kit/module"; import { inject, injectable, Lifecycle, scoped } from "tsyringe"; -import { ProvableMethodExecutionContext } from "@proto-kit/common"; +import { + ProvableMethodExecutionContext, + CompileRegistry, +} from "@proto-kit/common"; import { PairProofTaskSerializer, @@ -32,7 +35,6 @@ import { TaskWorkerModule } from "../../../worker/worker/TaskWorkerModule"; import { TaskStateRecord } from "../TransactionTraceService"; import { VerificationKeyService } from "../../runtime/RuntimeVerificationKeyService"; import { VerificationKeySerializer } from "../helpers/VerificationKeySerializer"; -import { CompileRegistry } from "../helpers/CompileRegistry"; import { JSONEncodableState } from "./RuntimeTaskParameters"; @@ -134,10 +136,7 @@ export class BlockReductionTask } public async prepare(): Promise { - await this.compileRegistry.compile( - "BlockProver", - this.blockProver.zkProgrammable.zkProgram[0] - ); + await this.blockProver.compile(this.compileRegistry); } } @@ -302,12 +301,8 @@ export class BlockProvingTask ); } - // eslint-disable-next-line sonarjs/no-identical-functions public async prepare(): Promise { // Compile - await this.compileRegistry.compile( - "BlockProver", - this.blockProver.zkProgrammable.zkProgram[0] - ); + await this.blockProver.compile(this.compileRegistry); } } diff --git a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts index d63a9e08..5d02c5e8 100644 --- a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts @@ -1,60 +1,63 @@ import { inject, injectable, Lifecycle, scoped } from "tsyringe"; import { Runtime } from "@proto-kit/module"; -import { VerificationKey } from "o1js"; -import { log, mapSequential } from "@proto-kit/common"; +import { + log, + mapSequential, + StringKeyOf, + ArtifactRecord, + CompileRegistry, + CompilableModule, + safeParseJson, +} from "@proto-kit/common"; import { MandatorySettlementModulesRecord, - NetworkState, Protocol, - RuntimeMethodExecutionContext, - RuntimeTransaction, SettlementContractModule, + RuntimeVerificationKeyRootService, } from "@proto-kit/protocol"; import { TaskSerializer } from "../../../worker/flow/Task"; -import { VKRecord } from "../../runtime/RuntimeVerificationKeyService"; import { UnpreparingTask } from "../../../worker/flow/UnpreparingTask"; import { VerificationKeySerializer } from "../helpers/VerificationKeySerializer"; -export type CompiledCircuitsRecord = { - protocolCircuits: VKRecord; - runtimeCircuits: VKRecord; +export type CompilerTaskParams = { + existingArtifacts: ArtifactRecord; + targets: string[]; + runtimeVKRoot?: string; }; -type VKRecordLite = Record; - -export class UndefinedSerializer implements TaskSerializer { - public toJSON(parameters: undefined): string { - return ""; - } +export type SerializedArtifactRecord = Record< + string, + { verificationKey: { hash: string; data: string } } +>; - public fromJSON(json: string): undefined { - return undefined; - } -} - -export class VKResultSerializer { - public toJSON(input: VKRecord): VKRecordLite { - const temp: VKRecordLite = Object.keys(input).reduce( - (accum, key) => { - return { - ...accum, - [key]: { - vk: VerificationKeySerializer.toJSON(input[key].vk), - }, - }; - }, - {} - ); +export class ArtifactRecordSerializer { + public toJSON(input: ArtifactRecord): SerializedArtifactRecord { + const temp: SerializedArtifactRecord = Object.keys( + input + ).reduce((accum, key) => { + return { + ...accum, + [key]: { + verificationKey: VerificationKeySerializer.toJSON( + input[key].verificationKey + ), + }, + }; + }, {}); return temp; } - public fromJSON(json: VKRecordLite): VKRecord { - return Object.keys(json).reduce((accum, key) => { + public fromJSON(json: SerializedArtifactRecord): ArtifactRecord { + if (json === undefined || json === null) return {}; + + return Object.keys(json).reduce((accum, key) => { return { ...accum, [key]: { - vk: VerificationKeySerializer.fromJSON(json[key].vk), + verificationKey: VerificationKeySerializer.fromJSON( + json[key].verificationKey + ), }, }; }, {}); @@ -64,121 +67,118 @@ export class VKResultSerializer { @injectable() @scoped(Lifecycle.ContainerScoped) export class CircuitCompilerTask extends UnpreparingTask< - undefined, - CompiledCircuitsRecord + CompilerTaskParams, + ArtifactRecord > { public name = "compiledCircuit"; public constructor( @inject("Runtime") protected readonly runtime: Runtime, - @inject("Protocol") protected readonly protocol: Protocol + @inject("Protocol") protected readonly protocol: Protocol, + private readonly compileRegistry: CompileRegistry ) { super(); } - public inputSerializer(): TaskSerializer { - return new UndefinedSerializer(); - } + public inputSerializer(): TaskSerializer { + type CompilerTaskParamsJSON = { + targets: string[]; + runtimeVKRoot?: string; + existingArtifacts: SerializedArtifactRecord; + }; - public resultSerializer(): TaskSerializer { - const vkRecordSerializer = new VKResultSerializer(); + const serializer = new ArtifactRecordSerializer(); return { - fromJSON: (json) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const temp: { - runtimeCircuits: VKRecordLite; - protocolCircuits: VKRecordLite; - } = JSON.parse(json); + toJSON: (input) => + JSON.stringify({ + targets: input.targets, + runtimeVKRoot: input.runtimeVKRoot, + existingArtifacts: serializer.toJSON(input.existingArtifacts), + } satisfies CompilerTaskParamsJSON), + fromJSON: (input) => { + const json = safeParseJson(input); return { - runtimeCircuits: vkRecordSerializer.fromJSON(temp.runtimeCircuits), - protocolCircuits: vkRecordSerializer.fromJSON(temp.protocolCircuits), + targets: json.targets, + runtimeVKRoot: json.runtimeVKRoot, + existingArtifacts: serializer.fromJSON(json.existingArtifacts), }; }, - toJSON: (input) => { - return JSON.stringify({ - runtimeCircuits: vkRecordSerializer.toJSON(input.runtimeCircuits), - protocolCircuits: vkRecordSerializer.toJSON(input.protocolCircuits), - }); - }, }; } - public async compileRuntimeMethods() { - log.time("Compiling runtime circuits"); - - const context = this.runtime.dependencyContainer.resolve( - RuntimeMethodExecutionContext - ); - context.setup({ - transaction: RuntimeTransaction.dummyTransaction(), - networkState: NetworkState.empty(), - }); - - const result = await mapSequential( - this.runtime.zkProgrammable.zkProgram, - async (program) => { - const vk = (await program.compile()).verificationKey; - - return Object.keys(program.methods).map((combinedMethodName) => { - const [moduleName, methodName] = combinedMethodName.split("."); - const methodId = this.runtime.methodIdResolver.getMethodId( - moduleName, - methodName - ); - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return [methodId.toString(), new VerificationKey(vk)] as [ - string, - VerificationKey, - ]; - }); - } - ); - log.timeEnd.info("Compiling runtime circuits"); - return result; + public resultSerializer(): TaskSerializer { + const serializer = new ArtifactRecordSerializer(); + return { + toJSON: (input) => JSON.stringify(serializer.toJSON(input)), + fromJSON: (input) => + serializer.fromJSON(safeParseJson(input)), + }; } - public async compileProtocolCircuits(): Promise< - [string, VerificationKey][][] - > { - // We only care about the BridgeContract for now - later with cachine, + public getSettlementTargets(): Record { + // We only care about the BridgeContract for now - later with caching, // we might want to expand that to all protocol circuits const container = this.protocol.dependencyContainer; if (container.isRegistered("SettlementContractModule")) { - log.time("Compiling protocol circuits"); - const settlementModule = container.resolve< SettlementContractModule >("SettlementContractModule"); - const BridgeClass = settlementModule.getContractClasses().bridge; - const artifact = await BridgeClass.compile(); + // Needed so that all contractFactory functions are called, because + // they set static args on the contracts + settlementModule.getContractClasses(); + + const moduleNames = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + settlementModule.moduleNames as StringKeyOf[]; - log.timeEnd.info("Compiling protocol circuits"); + const modules = moduleNames.map((name) => [ + `Settlement.${name}`, + settlementModule.resolve(name), + ]); - return [[["BridgeContract", artifact.verificationKey]]]; + return Object.fromEntries(modules); } - return [[]]; + return {}; } - public collectRecord(tuples: [string, VerificationKey][][]): VKRecord { - return tuples.flat().reduce((acc, step) => { - acc[step[0]] = { vk: step[1] }; - return acc; - }, {}); - } + public async compute(input: CompilerTaskParams): Promise { + this.compileRegistry.addArtifactsRaw(input.existingArtifacts); + + // We need to initialize the VK tree root if we have it, so that + // the BlockProver can bake in that root + if (input.runtimeVKRoot !== undefined) { + this.protocol.dependencyContainer + .resolve(RuntimeVerificationKeyRootService) + .setRoot(BigInt(input.runtimeVKRoot)); + } - public async compute(): Promise { log.info("Computing VKs"); - const runtimeTuples = await this.compileRuntimeMethods(); - const runtimeRecord = this.collectRecord(runtimeTuples); + // TODO make adaptive + const targets: Record = { + runtime: this.runtime, + protocol: this.protocol.blockProver, + ...this.getSettlementTargets(), + }; - const protocolTuples = await this.compileProtocolCircuits(); - const protocolRecord = this.collectRecord(protocolTuples); + const msg = `Compiling targets [${input.targets}]`; + log.time(msg); + await mapSequential(input.targets, async (target) => { + if (target in targets) { + await targets[target].compile(this.compileRegistry); + } else { + log.info( + // TODO Is that right? Or should we check that the bridge exists on the sequencer side? + `Compile target ${target} not found, skipping` + ); + } + }); + log.timeEnd.info(msg); - return { - runtimeCircuits: runtimeRecord, - protocolCircuits: protocolRecord, - }; + const newEntries = Object.entries( + this.compileRegistry.getAllArtifacts() + ).filter(([key]) => !(key in input.existingArtifacts)); + return Object.fromEntries(newEntries); } } diff --git a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts index d2963855..b58b04f2 100644 --- a/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/NewBlockTask.ts @@ -12,7 +12,10 @@ import { MandatoryProtocolModulesRecord, } from "@proto-kit/protocol"; import { Proof } from "o1js"; -import { ProvableMethodExecutionContext } from "@proto-kit/common"; +import { + ProvableMethodExecutionContext, + CompileRegistry, +} from "@proto-kit/common"; import { Task, TaskSerializer } from "../../../worker/flow/Task"; import { ProofTaskSerializer } from "../../../helpers/utils"; @@ -20,7 +23,6 @@ import { PreFilledStateService } from "../../../state/prefilled/PreFilledStateSe import { TaskWorkerModule } from "../../../worker/worker/TaskWorkerModule"; import { PairingDerivedInput } from "../flow/ReductionTaskFlow"; import { TaskStateRecord } from "../TransactionTraceService"; -import { CompileRegistry } from "../helpers/CompileRegistry"; import { JSONEncodableState } from "./RuntimeTaskParameters"; import { DecodedStateSerializer } from "./BlockProvingTask"; @@ -187,9 +189,6 @@ export class NewBlockTask public async prepare(): Promise { // Compile - await this.compileRegistry.compile( - "BlockProver", - this.blockProver.zkProgrammable.zkProgram[0] - ); + await this.blockProver.compile(this.compileRegistry); } } diff --git a/packages/sequencer/src/protocol/production/tasks/RuntimeProvingTask.ts b/packages/sequencer/src/protocol/production/tasks/RuntimeProvingTask.ts index 600a548f..e74f3451 100644 --- a/packages/sequencer/src/protocol/production/tasks/RuntimeProvingTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/RuntimeProvingTask.ts @@ -9,6 +9,7 @@ import { RuntimeMethodExecutionContext, } from "@proto-kit/protocol"; import { Proof } from "o1js"; +import { CompileRegistry } from "@proto-kit/common"; import { Task, TaskSerializer } from "../../../worker/flow/Task"; import { ProofTaskSerializer } from "../../../helpers/utils"; @@ -35,7 +36,8 @@ export class RuntimeProvingTask public constructor( @inject("Runtime") protected readonly runtime: Runtime, - private readonly executionContext: RuntimeMethodExecutionContext + private readonly executionContext: RuntimeMethodExecutionContext, + private readonly compileRegistry: CompileRegistry ) { super(); } @@ -95,9 +97,6 @@ export class RuntimeProvingTask } public async prepare(): Promise { - for (const zkProgram of this.runtimeZkProgrammable) { - // eslint-disable-next-line no-await-in-loop - await zkProgram.compile(); - } + await this.runtime.compile(this.compileRegistry); } } diff --git a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts index c024e84d..eacbecf8 100644 --- a/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/StateTransitionTask.ts @@ -9,7 +9,11 @@ import { StateTransitionProverPublicInput, StateTransitionProverPublicOutput, } from "@proto-kit/protocol"; -import { log, ProvableMethodExecutionContext } from "@proto-kit/common"; +import { + log, + ProvableMethodExecutionContext, + CompileRegistry, +} from "@proto-kit/common"; import { Task, TaskSerializer } from "../../../worker/flow/Task"; import { @@ -19,7 +23,6 @@ import { } from "../../../helpers/utils"; import { TaskWorkerModule } from "../../../worker/worker/TaskWorkerModule"; import { PreFilledWitnessProvider } from "../../../state/prefilled/PreFilledWitnessProvider"; -import { CompileRegistry } from "../helpers/CompileRegistry"; import { StateTransitionParametersSerializer, @@ -89,10 +92,7 @@ export class StateTransitionTask } public async prepare(): Promise { - await this.compileRegistry.compile( - "StateTransitionProver", - this.stateTransitionProver.zkProgrammable.zkProgram[0] - ); + await this.stateTransitionProver.compile(this.compileRegistry); } } @@ -141,11 +141,7 @@ export class StateTransitionReductionTask .result.prove(); } - // eslint-disable-next-line sonarjs/no-identical-functions public async prepare(): Promise { - await this.compileRegistry.compile( - "StateTransitionProver", - this.stateTransitionProver.zkProgrammable.zkProgram[0] - ); + await this.stateTransitionProver.compile(this.compileRegistry); } } diff --git a/packages/sequencer/src/protocol/runtime/RuntimeVerificationKeyService.ts b/packages/sequencer/src/protocol/runtime/RuntimeVerificationKeyService.ts index 43d722e7..5445d6e6 100644 --- a/packages/sequencer/src/protocol/runtime/RuntimeVerificationKeyService.ts +++ b/packages/sequencer/src/protocol/runtime/RuntimeVerificationKeyService.ts @@ -1,7 +1,9 @@ import { Field, VerificationKey } from "o1js"; import { + CompileArtifact, ConfigurableModule, InMemoryMerkleTreeStorage, + mapSequential, ZkProgrammable, } from "@proto-kit/common"; import { inject, injectable, Lifecycle, scoped } from "tsyringe"; @@ -53,7 +55,45 @@ export class VerificationKeyService extends ConfigurableModule<{}> { [methodId: string]: VerificationKey; }; - public async initializeVKTree(verificationKeys: VKRecord) { + public collectRecord(tuples: [string, VerificationKey][][]): VKRecord { + return tuples.flat().reduce((acc, step) => { + acc[step[0]] = { vk: step[1] }; + return acc; + }, {}); + } + + public async initializeVKTree(artifacts: Record) { + const mappings = await mapSequential( + this.runtime.zkProgrammable.zkProgram, + async (program) => { + const artifact = artifacts[program.name]; + + if (artifact === undefined) { + throw new Error( + `Compiled artifact for runtime program ${program.name} not found` + ); + } + + return Object.keys(program.methods).map((combinedMethodName) => { + const [moduleName, methodName] = combinedMethodName.split("."); + const methodId = this.runtime.methodIdResolver.getMethodId( + moduleName, + methodName + ); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return [ + methodId.toString(), + new VerificationKey(artifact.verificationKey), + ] as [string, VerificationKey]; + }); + } + ); + return await this.initializeVKTreeFromMethodMappings( + this.collectRecord(mappings) + ); + } + + private async initializeVKTreeFromMethodMappings(verificationKeys: VKRecord) { const tree = new VKTree(new InMemoryMerkleTreeStorage()); const valuesVK: Record = {}; const indexes: VKIndexes = {}; diff --git a/packages/sequencer/src/sequencer/SequencerStartupModule.ts b/packages/sequencer/src/sequencer/SequencerStartupModule.ts index 402e12ad..0c9b4a9c 100644 --- a/packages/sequencer/src/sequencer/SequencerStartupModule.ts +++ b/packages/sequencer/src/sequencer/SequencerStartupModule.ts @@ -5,13 +5,18 @@ import { RuntimeVerificationKeyRootService, SettlementSmartContractBase, } from "@proto-kit/protocol"; -import { log } from "@proto-kit/common"; +import { + log, + ArtifactRecord, + ChildVerificationKeyService, + CompileRegistry, +} from "@proto-kit/common"; -import { FlowCreator } from "../worker/flow/Flow"; +import { Flow, FlowCreator } from "../worker/flow/Flow"; import { WorkerRegistrationFlow } from "../worker/worker/startup/WorkerRegistrationFlow"; import { CircuitCompilerTask, - CompiledCircuitsRecord, + CompilerTaskParams, } from "../protocol/production/tasks/CircuitCompilerTask"; import { VerificationKeyService } from "../protocol/runtime/RuntimeVerificationKeyService"; @@ -25,28 +30,32 @@ export class SequencerStartupModule extends SequencerModule { private readonly protocol: Protocol, private readonly compileTask: CircuitCompilerTask, private readonly verificationKeyService: VerificationKeyService, - private readonly registrationFlow: WorkerRegistrationFlow + private readonly registrationFlow: WorkerRegistrationFlow, + private readonly compileRegistry: CompileRegistry ) { super(); } - public async start() { - const flow = this.flowCreator.createFlow("compile-circuits", {}); - - log.info("Compiling Protocol circuits, this can take a few minutes"); - - const vks = await flow.withFlow( - async (res, rej) => { - await flow.pushTask(this.compileTask, undefined, async (result) => { - res(result); - }); - } - ); + private async pushCompileTask( + flow: Flow<{}>, + payload: CompilerTaskParams + ): Promise { + return await flow.withFlow(async (res, rej) => { + await flow.pushTask(this.compileTask, payload, async (result) => { + res(result); + }); + }); + } - log.info("Protocol circuits compiled"); + public async compileRuntime(flow: Flow<{}>) { + const artifacts = await this.pushCompileTask(flow, { + existingArtifacts: {}, + targets: ["runtime"], + runtimeVKRoot: undefined, + }); // Init runtime VK tree - await this.verificationKeyService.initializeVKTree(vks.runtimeCircuits); + await this.verificationKeyService.initializeVKTree(artifacts); const root = this.verificationKeyService.getRoot(); @@ -54,16 +63,93 @@ export class SequencerStartupModule extends SequencerModule { .resolve(RuntimeVerificationKeyRootService) .setRoot(root); + this.compileRegistry.addArtifactsRaw(artifacts); + + return root; + } + + private async compileProtocolAndBridge(flow: Flow<{}>) { + // Can happen in parallel + type ParallelResult = { + protocol?: ArtifactRecord; + bridge?: ArtifactRecord; + }; + const result = await flow.withFlow(async (res, rej) => { + const results: ParallelResult = {}; + + const resolveIfPossible = () => { + const { bridge, protocol } = results; + if (bridge !== undefined && protocol !== undefined) { + res({ ...protocol, ...bridge }); + } + }; + + await flow.pushTask( + this.compileTask, + { + existingArtifacts: {}, + targets: ["protocol"], + runtimeVKRoot: undefined, + }, + async (protocolResult) => { + results.protocol = protocolResult; + resolveIfPossible(); + } + ); + + await flow.pushTask( + this.compileTask, + { + existingArtifacts: {}, + targets: ["Settlement.BridgeContract"], + runtimeVKRoot: undefined, + }, + async (bridgeResult) => { + results.bridge = bridgeResult; + resolveIfPossible(); + } + ); + }); + this.compileRegistry.addArtifactsRaw(result); + return result; + } + + public async start() { + const flow = this.flowCreator.createFlow("compile-circuits", {}); + + this.protocol.dependencyContainer + .resolve(ChildVerificationKeyService) + .setCompileRegistry(this.compileRegistry); + + log.info("Compiling Protocol circuits, this can take a few minutes"); + + const root = await this.compileRuntime(flow); + + const protocolBridgeArtifacts = await this.compileProtocolAndBridge(flow); + + log.info("Protocol circuits compiled"); + // Init BridgeContract vk for settlement contract - const bridgeVk = vks.protocolCircuits.BridgeContract; + const bridgeVk = protocolBridgeArtifacts.BridgeContract; if (bridgeVk !== undefined) { SettlementSmartContractBase.args.BridgeContractVerificationKey = - bridgeVk.vk; + bridgeVk.verificationKey; } + const record = await this.pushCompileTask(flow, { + existingArtifacts: this.compileRegistry.getAllArtifacts(), + targets: ["Settlement.SettlementContract"], + }); + + this.compileRegistry.addArtifactsRaw(record); + + // TODO Compile all contracts and retrieve artifacts to enable crafting of + // the deployments - edit: can also be done on-demand with the CompileTask + await this.registrationFlow.start({ runtimeVerificationKeyRoot: root, - bridgeContractVerificationKey: bridgeVk?.vk, + bridgeContractVerificationKey: bridgeVk?.verificationKey, + compiledArtifacts: this.compileRegistry.getAllArtifacts(), }); log.info("Protocol circuits compiled successfully, commencing startup"); diff --git a/packages/sequencer/src/settlement/SettlementModule.ts b/packages/sequencer/src/settlement/SettlementModule.ts index 6484bcfd..1dd61dd3 100644 --- a/packages/sequencer/src/settlement/SettlementModule.ts +++ b/packages/sequencer/src/settlement/SettlementModule.ts @@ -8,6 +8,7 @@ import { MandatoryProtocolModulesRecord, BlockProverPublicOutput, SettlementSmartContractBase, + DynamicBlockProof, } from "@proto-kit/protocol"; import { AccountUpdate, @@ -26,6 +27,7 @@ import { log, AreProofsEnabled, DependencyFactory, + CompileRegistry, } from "@proto-kit/common"; import truncate from "lodash/truncate"; @@ -94,7 +96,8 @@ export class SettlementModule private readonly transactionSender: MinaTransactionSender, @inject("AreProofsEnabled") areProofsEnabled: AreProofsEnabled, @inject("FeeStrategy") - private readonly feeStrategy: FeeStrategy + private readonly feeStrategy: FeeStrategy, + private readonly compileRegistry: CompileRegistry ) { super(); this.utils = new SettlementUtils(areProofsEnabled, baseLayer); @@ -201,6 +204,8 @@ export class SettlementModule .getBlockProofSerializer() .fromJSONProof(batch.proof); + const dynamicBlockProof = DynamicBlockProof.fromProof(blockProof); + const tx = await Mina.transaction( { sender: feepayer.toPublicKey(), @@ -210,7 +215,7 @@ export class SettlementModule }, async () => { await settlement.settle( - blockProof, + dynamicBlockProof, signature, dispatch.address, feepayer.toPublicKey(), @@ -248,6 +253,8 @@ export class SettlementModule const nonce = options?.nonce ?? 0; + // const verificationKey: + const sm = this.protocol.dependencyContainer.resolve< SettlementContractModule >("SettlementContractModule"); @@ -378,8 +385,6 @@ export class SettlementModule public async start(): Promise { const contractArgs = SettlementSmartContractBase.args; - // const dummyVk = MOCK_VERIFICATION_KEY; - SettlementSmartContractBase.args = { ...contractArgs, signedSettlements: this.utils.isSignedSettlement(), @@ -388,11 +393,6 @@ export class SettlementModule ? new SignedSettlementPermissions() : new ProvenSettlementPermissions() ).bridgeContractMina(), - // BridgeContractVerificationKey: this.utils.isSignedSettlement() - // ? undefined - // : dummyVk, }; - - // TODO Add task to compute verification key } } diff --git a/packages/sequencer/src/settlement/tasks/SettlementProvingTask.ts b/packages/sequencer/src/settlement/tasks/SettlementProvingTask.ts index 95cda66a..38782a60 100644 --- a/packages/sequencer/src/settlement/tasks/SettlementProvingTask.ts +++ b/packages/sequencer/src/settlement/tasks/SettlementProvingTask.ts @@ -3,9 +3,9 @@ import { MOCK_PROOF, AreProofsEnabled, log, + CompileRegistry, } from "@proto-kit/common"; import { - ContractModule, MandatoryProtocolModulesRecord, MandatorySettlementModulesRecord, Protocol, @@ -30,7 +30,6 @@ import { ProofTaskSerializer, DynamicProofTaskSerializer, } from "../../helpers/utils"; -import { CompileRegistry } from "../../protocol/production/helpers/CompileRegistry"; import { Task, TaskSerializer } from "../../worker/flow/Task"; import { TaskWorkerModule } from "../../worker/worker/TaskWorkerModule"; @@ -368,30 +367,37 @@ export class SettlementProvingTask } public async prepare(): Promise { + const { settlementContractModule } = this; // Guard in case the task is configured but settlement is not - if (this.settlementContractModule === undefined) { - return; + if (settlementContractModule === undefined) { + throw new Error( + "Settlement task is configured, but Settlement Contracts aren't" + ); } - const { areProofsEnabled } = this.areProofsEnabled; - const contractClasses: Record = {}; - for (const key of this.settlementContractModule.moduleNames) { - const module: ContractModule = - this.settlementContractModule.resolve( - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - key as keyof MandatorySettlementModulesRecord - ); - + const modules = settlementContractModule.moduleNames.map( + (key) => + [ + key, + settlementContractModule.resolve( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + key as keyof MandatorySettlementModulesRecord + ), + ] as const + ); + + // First, create all contract classes (with static args), then compile them + for (const [key, module] of modules) { contractClasses[key] = module.contractFactory(); + } + + for (const [key, module] of modules) { + log.debug(`Compiling Settlement Module ${key}`); // eslint-disable-next-line no-await-in-loop - await this.compileRegistry.compileSmartContract( - key, - module, - areProofsEnabled - ); + await module.compile(this.compileRegistry); } this.contractRegistry = new ContractRegistry(contractClasses); diff --git a/packages/sequencer/src/worker/worker/FlowTaskWorker.ts b/packages/sequencer/src/worker/worker/FlowTaskWorker.ts index 704bd5e4..8db8a0c2 100644 --- a/packages/sequencer/src/worker/worker/FlowTaskWorker.ts +++ b/packages/sequencer/src/worker/worker/FlowTaskWorker.ts @@ -91,6 +91,7 @@ export class FlowTaskWorker[]> log.debug(`Preparing task ${task.constructor.name}`); // eslint-disable-next-line no-await-in-loop await task.prepare(); + log.trace(`${task.constructor.name} prepared`); } const newWorkers = Object.fromEntries( diff --git a/packages/sequencer/src/worker/worker/WorkerReadyModule.ts b/packages/sequencer/src/worker/worker/WorkerReadyModule.ts index 1fd2516d..fe9d060d 100644 --- a/packages/sequencer/src/worker/worker/WorkerReadyModule.ts +++ b/packages/sequencer/src/worker/worker/WorkerReadyModule.ts @@ -1,5 +1,6 @@ import { injectable } from "tsyringe"; import { injectOptional } from "@proto-kit/common"; + import { LocalTaskWorkerModule } from "./LocalTaskWorkerModule"; /** @@ -15,10 +16,11 @@ export class WorkerReadyModule { | undefined ) {} - public async waitForReady(): Promise { + // eslint-disable-next-line consistent-return + public async waitForReady() { if (this.localTaskWorkerModule !== undefined) { const module = this.localTaskWorkerModule; - return new Promise((res, rej) => { + return await new Promise((res, rej) => { module.containerEvents.on("ready", (ready) => { if (ready) { res(); diff --git a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts index f1cf7467..da085883 100644 --- a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts +++ b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts @@ -1,4 +1,11 @@ -import { log, noop } from "@proto-kit/common"; +import { + log, + noop, + ArtifactRecord, + ChildVerificationKeyService, + CompileRegistry, + safeParseJson, +} from "@proto-kit/common"; import { inject, injectable } from "tsyringe"; import { Protocol, @@ -9,7 +16,14 @@ import { VerificationKey } from "o1js"; import { Task } from "../../flow/Task"; import { AbstractStartupTask } from "../../flow/AbstractStartupTask"; -import { VerificationKeySerializer } from "../../../protocol/production/helpers/VerificationKeySerializer"; +import { + VerificationKeyJSON, + VerificationKeySerializer, +} from "../../../protocol/production/helpers/VerificationKeySerializer"; +import { + ArtifactRecordSerializer, + SerializedArtifactRecord, +} from "../../../protocol/production/tasks/CircuitCompilerTask"; import { CloseWorkerError } from "./CloseWorkerError"; @@ -17,6 +31,7 @@ export type WorkerStartupPayload = { runtimeVerificationKeyRoot: bigint; // This has to be nullable, since bridgeContractVerificationKey?: VerificationKey; + compiledArtifacts: ArtifactRecord; }; @injectable() @@ -28,7 +43,8 @@ export class WorkerRegistrationTask private done = false; public constructor( - @inject("Protocol") private readonly protocol: Protocol + @inject("Protocol") private readonly protocol: Protocol, + private readonly compileRegistry: CompileRegistry ) { super(); } @@ -55,6 +71,11 @@ export class WorkerRegistrationTask input.bridgeContractVerificationKey; } + this.compileRegistry.addArtifactsRaw(input.compiledArtifacts); + this.protocol.dependencyContainer + .resolve(ChildVerificationKeyService) + .setCompileRegistry(this.compileRegistry); + this.events.emit("startup-task-finished"); this.done = true; @@ -62,6 +83,13 @@ export class WorkerRegistrationTask } public inputSerializer() { + type WorkerStartupPayloadJSON = { + runtimeVerificationKeyRoot: string; + bridgeContractVerificationKey: VerificationKeyJSON | undefined; + compiledArtifacts: SerializedArtifactRecord; + }; + + const artifactSerializer = new ArtifactRecordSerializer(); return { toJSON: (payload: WorkerStartupPayload) => { return JSON.stringify({ @@ -73,24 +101,27 @@ export class WorkerRegistrationTask payload.bridgeContractVerificationKey ) : undefined, - }); + compiledArtifacts: artifactSerializer.toJSON( + payload.compiledArtifacts + ), + } satisfies WorkerStartupPayloadJSON); }, fromJSON: (payload: string) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const jsonObject = JSON.parse(payload); + const jsonObject = safeParseJson(payload); return { runtimeVerificationKeyRoot: BigInt( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument jsonObject.runtimeVerificationKeyRoot ), bridgeContractVerificationKey: jsonObject.bridgeContractVerificationKey !== undefined ? VerificationKeySerializer.fromJSON( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument jsonObject.bridgeContractVerificationKey ) : undefined, + compiledArtifacts: artifactSerializer.fromJSON( + jsonObject.compiledArtifacts + ), }; }, }; diff --git a/packages/sequencer/test/integration/BlockProductionSize.test.ts b/packages/sequencer/test/integration/BlockProductionSize.test.ts index 7ca9fd89..19217559 100644 --- a/packages/sequencer/test/integration/BlockProductionSize.test.ts +++ b/packages/sequencer/test/integration/BlockProductionSize.test.ts @@ -97,7 +97,7 @@ describe("block limit", () => { }); // Start AppChain - await app.start(container.createChildContainer()); + await app.start(false, container.createChildContainer()); ({ runtime, sequencer } = app); diff --git a/packages/sequencer/test/integration/Proven.test.ts b/packages/sequencer/test/integration/Proven.test.ts index 64bf1654..4f0195b7 100644 --- a/packages/sequencer/test/integration/Proven.test.ts +++ b/packages/sequencer/test/integration/Proven.test.ts @@ -1,13 +1,33 @@ import "reflect-metadata"; -import { expectDefined, log } from "@proto-kit/common"; +import { + expectDefined, + log, + MOCK_VERIFICATION_KEY, + ChildVerificationKeyService, + CompileRegistry, +} from "@proto-kit/common"; import { Runtime } from "@proto-kit/module"; -import { Protocol } from "@proto-kit/protocol"; +import { + BridgeContract, + DispatchSmartContract, + Protocol, + SettlementContractModule, + SettlementSmartContract, + SettlementSmartContractBase, +} from "@proto-kit/protocol"; import { VanillaProtocolModules } from "@proto-kit/library"; -import { AppChain } from "@proto-kit/sdk"; +import { AppChain, InMemoryAreProofsEnabled } from "@proto-kit/sdk"; import { container } from "tsyringe"; import { PrivateKey, UInt64 } from "o1js"; import { testingSequencerFromModules } from "../TestingSequencer"; +import { + MinaBaseLayer, + ProvenSettlementPermissions, + SettlementModule, + SettlementProvingTask, + WithdrawalQueue, +} from "../../src"; import { ProtocolStateTestHook } from "./mocks/ProtocolStateTestHook"; import { BlockTestService } from "./services/BlockTestService"; @@ -24,22 +44,37 @@ describe("Proven", () => { log.setLevel(log.levels.DEBUG); const runtimeClass = Runtime.from({ modules: { - Balance: ProvenBalance, + Balances: ProvenBalance, }, config: { - Balance: {}, + Balances: {}, }, }); - const sequencerClass = testingSequencerFromModules({}); + const sequencerClass = testingSequencerFromModules( + { + BaseLayer: MinaBaseLayer, + SettlementModule, + OutgoingMessageQueue: WithdrawalQueue, + }, + { + SettlementProvingTask, + } + ); // TODO Analyze how we can get rid of the library import for mandatory modules const protocolClass = Protocol.from({ - modules: VanillaProtocolModules.mandatoryModules({ - ProtocolStateTestHook, - // ProtocolStateTestHook2, - }), + modules: { + ...VanillaProtocolModules.mandatoryModules({ + ProtocolStateTestHook, + // ProtocolStateTestHook2, + }), + SettlementContractModule: SettlementContractModule.with({ + // FungibleToken: FungibleTokenContractModule, + // FungibleTokenAdmin: FungibleTokenAdminContractModule, + }), + }, // modules: VanillaProtocolModules.with({}), }); @@ -58,13 +93,19 @@ describe("Proven", () => { BatchProducerModule: {}, BlockProducerModule: {}, LocalTaskWorkerModule: {}, - BaseLayer: {}, TaskQueue: {}, FeeStrategy: {}, SequencerStartupModule: {}, + BaseLayer: { + network: { + type: "local", + }, + }, + SettlementModule: {}, + OutgoingMessageQueue: {}, }, Runtime: { - Balance: {}, + Balances: {}, }, Protocol: { AccountState: {}, @@ -73,6 +114,18 @@ describe("Proven", () => { BlockHeight: {}, LastStateRoot: {}, ProtocolStateTestHook: {}, + SettlementContractModule: { + SettlementContract: {}, + BridgeContract: { + withdrawalStatePath: "Withdrawals.withdrawals", + withdrawalEventName: "withdrawal", + }, + DispatchContract: { + incomingMessagesMethods: { + deposit: "Balances.deposit", + }, + }, + }, // ProtocolStateTestHook2: {}, }, }); @@ -91,6 +144,36 @@ describe("Proven", () => { timeout ); + it.skip("Hello", async () => { + try { + const vkService = new ChildVerificationKeyService(); + const proofs = new InMemoryAreProofsEnabled(); + proofs.setProofsEnabled(true); + const registry = new CompileRegistry(proofs); + registry.addArtifactsRaw({ + BlockProver: { + verificationKey: MOCK_VERIFICATION_KEY, + }, + }); + vkService.setCompileRegistry(registry); + SettlementSmartContractBase.args = { + DispatchContract: DispatchSmartContract, + ChildVerificationKeyService: vkService, + BridgeContractVerificationKey: MOCK_VERIFICATION_KEY, + signedSettlements: false, + BridgeContract: BridgeContract, + hooks: [], + BridgeContractPermissions: + new ProvenSettlementPermissions().bridgeContractMina(), + escapeHatchSlotsInterval: 1000, + }; + const vk = await SettlementSmartContract.compile(); + console.log(vk.verificationKey); + } catch (e) { + console.error(e); + } + }, 500000); + it( "should produce simple block", async () => { @@ -101,7 +184,7 @@ describe("Proven", () => { const privateKey = PrivateKey.random(); await test.addTransaction({ - method: ["Balance", "addBalance"], + method: ["Balances", "addBalance"], privateKey, args: [PrivateKey.random().toPublicKey(), UInt64.from(100)], }); diff --git a/packages/sequencer/test/integration/mocks/ProvenBalance.ts b/packages/sequencer/test/integration/mocks/ProvenBalance.ts index ff6cbbec..f579993f 100644 --- a/packages/sequencer/test/integration/mocks/ProvenBalance.ts +++ b/packages/sequencer/test/integration/mocks/ProvenBalance.ts @@ -1,4 +1,5 @@ import { + runtimeMessage, runtimeMethod, runtimeModule, RuntimeModule, @@ -7,7 +8,7 @@ import { import { log, Presets } from "@proto-kit/common"; import { PublicKey, UInt64 } from "o1js"; import { Admin } from "@proto-kit/module/test/modules/Admin"; -import { State, StateMap } from "@proto-kit/protocol"; +import { Deposit, State, StateMap } from "@proto-kit/protocol"; @runtimeModule() export class ProvenBalance extends RuntimeModule { @@ -36,4 +37,9 @@ export class ProvenBalance extends RuntimeModule { const newBalance = balance.orElse(UInt64.zero).add(value); await this.balances.set(address, newBalance); } + + @runtimeMessage() + public async deposit(deposit: Deposit) { + await this.addBalance(deposit.address, deposit.amount); + } }