diff --git a/packages/common/src/compiling/CompileRegistry.ts b/packages/common/src/compiling/CompileRegistry.ts index e34dbc4b5..a85766ffe 100644 --- a/packages/common/src/compiling/CompileRegistry.ts +++ b/packages/common/src/compiling/CompileRegistry.ts @@ -47,22 +47,17 @@ export class CompileRegistry { return result; } - public async compile(target: CompileTarget, proverNeeded: boolean = true) { - if (this.artifacts[target.name] === undefined || this.inForceProverBlock) { + public async compile(target: CompileTarget, nameOverride?: string) { + const name = nameOverride ?? target.name; + if (this.artifacts[name] === undefined || this.inForceProverBlock) { const artifact = await this.compiler.compileContract(target); - this.artifacts[target.name] = artifact; + this.artifacts[name] = artifact; return artifact; } - return this.artifacts[target.name]; + return this.artifacts[name]; } public getArtifact(name: string): CompileArtifact | undefined { - if (this.artifacts[name] === undefined) { - throw new Error( - `Artifact for ${name} not available, did you compile it via the CompileRegistry?` - ); - } - return this.artifacts[name]; } diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 4d25b1192..f2a0e452e 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,5 +1,13 @@ // allows to reference interfaces as 'classes' rather than instances -import { Bool, DynamicProof, Field, Proof, ProofBase, PublicKey } from "o1js"; +import { + Bool, + DynamicProof, + Field, + Proof, + ProofBase, + PublicKey, + Option, +} from "o1js"; export type TypedClass = new (...args: any[]) => Class; @@ -56,3 +64,5 @@ export type InferProofBase< : ProofType extends DynamicProof ? ProofBase : undefined; + +export class O1PublicKeyOption extends Option(PublicKey) {} diff --git a/packages/protocol/src/hooks/NoopSettlementHook.ts b/packages/protocol/src/hooks/NoopSettlementHook.ts index 166ff678f..99ecbde77 100644 --- a/packages/protocol/src/hooks/NoopSettlementHook.ts +++ b/packages/protocol/src/hooks/NoopSettlementHook.ts @@ -5,14 +5,14 @@ import { ProvableSettlementHook, SettlementHookInputs, } from "../settlement/modularity/ProvableSettlementHook"; -import { SettlementSmartContractBase } from "../settlement/contracts/SettlementSmartContract"; +import { SettlementContractType } from "../settlement/contracts/settlement/SettlementBase"; @injectable() export class NoopSettlementHook extends ProvableSettlementHook< Record > { public async beforeSettlement( - contract: SettlementSmartContractBase, + contract: SettlementContractType, state: SettlementHookInputs ) { noop(); diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index eeb2ba0d5..080dfc646 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -45,8 +45,11 @@ export * from "./state/assert/assert"; export * from "./settlement/contracts/authorizations/ContractAuthorization"; export * from "./settlement/contracts/authorizations/UpdateMessagesHashAuth"; export * from "./settlement/contracts/authorizations/TokenBridgeDeploymentAuth"; -export * from "./settlement/contracts/SettlementSmartContract"; -export * from "./settlement/contracts/SettlementContractProtocolModule"; +export * from "./settlement/contracts/settlement/SettlementBase"; +export * from "./settlement/contracts/settlement/SettlementContract"; +export * from "./settlement/contracts/settlement/BridgingSettlementContract"; +export * from "./settlement/contracts/BridgingSettlementContractModule"; +export * from "./settlement/contracts/SettlementSmartContractModule"; export * from "./settlement/contracts/DispatchSmartContract"; export * from "./settlement/contracts/DispatchContractProtocolModule"; export * from "./settlement/contracts/BridgeContract"; @@ -60,6 +63,7 @@ export * from "./settlement/messages/OutgoingMessageArgument"; export * from "./settlement/messages/OutgoingMessage"; export * from "./settlement/modules/NetworkStateSettlementModule"; export * from "./settlement/messages/Deposit"; +export * from "./settlement/ContractArgsRegistry"; export { constants as ProtocolConstants } from "./Constants"; export * from "./hashing/protokit-prefixes"; export * from "./hashing/mina-prefixes"; diff --git a/packages/protocol/src/settlement/ContractArgsRegistry.ts b/packages/protocol/src/settlement/ContractArgsRegistry.ts new file mode 100644 index 000000000..f9c2f0020 --- /dev/null +++ b/packages/protocol/src/settlement/ContractArgsRegistry.ts @@ -0,0 +1,60 @@ +import { injectable, singleton } from "tsyringe"; +import merge from "lodash/merge"; + +export interface StaticInitializationContract { + getInitializationArgs(): Args; +} + +export type NaiveObjectSchema = { + [Key in keyof Obj]: undefined extends Obj[Key] ? "Optional" : "Required"; +}; + +/* +interface Test { + one: string; + two?: string; +} + +const x: NaiveObjectSchema = { + one: "Required", + two: "Optional", +}; +*/ + +@injectable() +@singleton() +export class ContractArgsRegistry { + args: Record = {}; + + public addArgs(name: string, addition: Partial) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const args: Partial = this.args[name] ?? {}; + this.args[name] = merge(args, addition); + } + + public resetArgs(name: string) { + delete this.args[name]; + } + + public getArgs(name: string, schema: NaiveObjectSchema): Type { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const args = this.args[name] ?? {}; + + const missing = Object.entries<"Optional" | "Required">(schema).filter( + ([key, type]) => { + // We filter only if the key is required and isn't present + return type === "Required" && args[key] === undefined; + } + ); + + if (missing.length > 0) { + const missingKeys = missing.map(([key]) => key); + throw new Error( + `Contract args for ${name} not all present, ${missingKeys} are missing` + ); + } else { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return args as Type; + } + } +} diff --git a/packages/protocol/src/settlement/SettlementContractModule.ts b/packages/protocol/src/settlement/SettlementContractModule.ts index 631842d22..3bfaa778b 100644 --- a/packages/protocol/src/settlement/SettlementContractModule.ts +++ b/packages/protocol/src/settlement/SettlementContractModule.ts @@ -16,17 +16,19 @@ import { ProtocolModule } from "../protocol/ProtocolModule"; import { ContractModule } from "./ContractModule"; import { DispatchContractProtocolModule } from "./contracts/DispatchContractProtocolModule"; import { DispatchContractType } from "./contracts/DispatchSmartContract"; -import { - SettlementContractConfig, - SettlementContractProtocolModule, -} from "./contracts/SettlementContractProtocolModule"; -import { SettlementContractType } from "./contracts/SettlementSmartContract"; +import { BridgingSettlementContractModule } from "./contracts/BridgingSettlementContractModule"; import { BridgeContractType } from "./contracts/BridgeContract"; import { BridgeContractConfig, BridgeContractProtocolModule, } from "./contracts/BridgeContractProtocolModule"; -import { GetContracts } from "./modularity/types"; +import { GetContracts, InferContractType } from "./modularity/types"; +import { BridgingSettlementContractType } from "./contracts/settlement/BridgingSettlementContract"; +import { SettlementContractType } from "./contracts/settlement/SettlementBase"; +import { + SettlementContractConfig, + SettlementSmartContractModule, +} from "./contracts/SettlementSmartContractModule"; export type SettlementModulesRecord = ModulesRecord< TypedClass> @@ -36,6 +38,12 @@ export type MandatorySettlementModulesRecord = { SettlementContract: TypedClass< ContractModule >; +}; + +export type BridgingSettlementModulesRecord = { + SettlementContract: TypedClass< + ContractModule + >; DispatchContract: TypedClass>; BridgeContract: TypedClass< ContractModule @@ -44,8 +52,7 @@ export type MandatorySettlementModulesRecord = { @injectable() export class SettlementContractModule< - SettlementModules extends SettlementModulesRecord & - MandatorySettlementModulesRecord, + SettlementModules extends SettlementModulesRecord, > extends ModuleContainer implements ProtocolModule @@ -54,10 +61,7 @@ export class SettlementContractModule< super(definition); } - public static from< - SettlementModules extends SettlementModulesRecord & - MandatorySettlementModulesRecord, - >( + public static from( modules: SettlementModules ): TypedClass> { return class ScopedSettlementContractModule extends SettlementContractModule { @@ -67,27 +71,18 @@ export class SettlementContractModule< }; } - public static mandatoryModules() { + public static settlementOnly() { return { - SettlementContract: SettlementContractProtocolModule, - DispatchContract: DispatchContractProtocolModule, - BridgeContract: BridgeContractProtocolModule, + SettlementContract: SettlementSmartContractModule, } as const; } - public static fromDefaults() { - return SettlementContractModule.from( - SettlementContractModule.mandatoryModules() - ); - } - - public static with( - additionalModules: AdditionalModules - ) { - return SettlementContractModule.from({ - ...SettlementContractModule.mandatoryModules(), - ...additionalModules, - } as const); + public static settlementAndBridging() { + return { + SettlementContract: BridgingSettlementContractModule, + DispatchContract: DispatchContractProtocolModule, + BridgeContract: BridgeContractProtocolModule, + } as const; } // ** For protocol module @@ -116,30 +111,40 @@ export class SettlementContractModule< return Object.fromEntries(contracts); } - public createContracts(addresses: { - settlement: PublicKey; - dispatch: PublicKey; - }): { - settlement: SettlementContractType & SmartContract; - dispatch: DispatchContractType & SmartContract; - } { - const { DispatchContract, SettlementContract } = this.getContractClasses(); - - const dispatchInstance = new DispatchContract(addresses.dispatch); - const settlementInstance = new SettlementContract(addresses.settlement); - - return { - dispatch: dispatchInstance, - settlement: settlementInstance, - }; - } - - public createBridgeContract( + public createContract>( + contractName: ContractName, address: PublicKey, tokenId?: Field - ): BridgeContractType & SmartContract { - const { BridgeContract } = this.getContractClasses(); + ): InferContractType { + const module = this.resolve(contractName); + const ContractClass = module.contractFactory(); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return new ContractClass(address, tokenId) as InferContractType< + SettlementModules[ContractName] + >; + } - return new BridgeContract(address, tokenId); + public createContracts< + ContractName extends keyof SettlementModules, + >(addresses: { + [Key in ContractName]: PublicKey; + }): { + [Key in ContractName]: SmartContract & + InferContractType; + } { + const classes = this.getContractClasses(); + + const obj: Record = {}; + // eslint-disable-next-line guard-for-in + for (const key in addresses) { + const ContractClass = classes[key]; + obj[key] = new ContractClass(addresses[key]); + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return obj as { + [Key in keyof SettlementModules]: SmartContract & + InferContractType; + }; } } diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index 34917d865..f56f8ec95 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -28,8 +28,13 @@ import { import { Path } from "../../model/Path"; import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor"; import { PROTOKIT_FIELD_PREFIXES } from "../../hashing/protokit-prefixes"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../ContractArgsRegistry"; -import type { SettlementContractType } from "./SettlementSmartContract"; +import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; export type BridgeContractType = { stateRoot: State; @@ -64,19 +69,33 @@ export class BridgeContractContext { } = { messageInputs: [] }; } -export abstract class BridgeContractBase extends TokenContract { - public static args: { - SettlementContract: - | (TypedClass & typeof SmartContract) - | undefined; - messageProcessors: OutgoingMessageProcessor[]; - batchSize?: number; - }; +export interface BridgeContractArgs { + SettlementContract: TypedClass & + typeof SmartContract; + messageProcessors: OutgoingMessageProcessor[]; + batchSize?: number; +} + +export const BridgeContractArgsSchema: NaiveObjectSchema = { + batchSize: "Optional", + SettlementContract: "Required", + messageProcessors: "Required", +}; +export abstract class BridgeContractBase + extends TokenContract + implements StaticInitializationContract +{ public constructor(address: PublicKey, tokenId?: Field) { super(address, tokenId); } + getInitializationArgs(): BridgeContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("BridgeContract", BridgeContractArgsSchema); + } + abstract settlementContractAddress: State; abstract stateRoot: State; @@ -134,14 +153,11 @@ export abstract class BridgeContractBase extends TokenContract { // witness values, not update/insert this.stateRoot.set(root); + const args = this.getInitializationArgs(); + const settlementContractAddress = this.settlementContractAddress.getAndRequireEquals(); - const SettlementContractClass = BridgeContractBase.args.SettlementContract; - if (SettlementContractClass === undefined) { - throw new Error( - "Settlement Contract class hasn't been set yet, something is wrong with your module composition" - ); - } + const SettlementContractClass = args.SettlementContract; const settlementContract = new SettlementContractClass( settlementContractAddress ); @@ -150,11 +166,14 @@ export abstract class BridgeContractBase extends TokenContract { } private batchSize() { - return BridgeContractBase.args.batchSize ?? OUTGOING_MESSAGE_BATCH_SIZE; + return ( + this.getInitializationArgs().batchSize ?? OUTGOING_MESSAGE_BATCH_SIZE + ); } private executeProcessors(batchIndex: number, args: OutgoingMessageArgument) { - return BridgeContractBase.args.messageProcessors.map((processor, j) => { + 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[ diff --git a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts index 1ab75c588..e4537d242 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts @@ -3,10 +3,11 @@ import { CompileRegistry } from "@proto-kit/common"; import { ContractModule } from "../ContractModule"; import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { BridgeContract, - BridgeContractBase, + BridgeContractArgs, BridgeContractType, } from "./BridgeContract"; @@ -21,7 +22,8 @@ export class BridgeContractProtocolModule extends ContractModule< > { public constructor( @injectAll("OutgoingMessageProcessor", { isOptional: true }) - private readonly messageProcessors: OutgoingMessageProcessor[] + private readonly messageProcessors: OutgoingMessageProcessor[], + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -29,11 +31,10 @@ export class BridgeContractProtocolModule extends ContractModule< public contractFactory() { const { config } = this; - BridgeContractBase.args = { - SettlementContract: BridgeContractBase.args?.SettlementContract, + this.contractArgsRegistry.addArgs("BridgeContract", { messageProcessors: this.messageProcessors, batchSize: config.outgoingBatchSize, - }; + }); return BridgeContract; } diff --git a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts similarity index 50% rename from packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts rename to packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts index 6cdf51952..9d1fef1ed 100644 --- a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts @@ -12,27 +12,26 @@ import { SmartContractClassFromInterface, } from "../ContractModule"; import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; -import { DispatchSmartContractBase } from "./DispatchSmartContract"; +import { DispatchContractArgs } from "./DispatchSmartContract"; import { - SettlementContractType, - SettlementSmartContract, - SettlementSmartContractBase, -} from "./SettlementSmartContract"; -import { BridgeContractBase } from "./BridgeContract"; + BridgingSettlementContractType, + BridgingSettlementContract, + BridgingSettlementContractArgs, +} from "./settlement/BridgingSettlementContract"; +import { BridgeContractArgs } from "./BridgeContract"; import { DispatchContractProtocolModule } from "./DispatchContractProtocolModule"; import { BridgeContractProtocolModule } from "./BridgeContractProtocolModule"; - -export type SettlementContractConfig = { - escapeHatchSlotsInterval?: number; -}; - -// 24 hours -const DEFAULT_ESCAPE_HATCH = (60 / 3) * 24; +import { + DEFAULT_ESCAPE_HATCH, + SettlementContractConfig, +} from "./SettlementSmartContractModule"; +import { SettlementContract } from "./settlement/SettlementContract"; @injectable() -export class SettlementContractProtocolModule extends ContractModule< - SettlementContractType, +export class BridgingSettlementContractModule extends ContractModule< + BridgingSettlementContractType, SettlementContractConfig > { public constructor( @@ -44,12 +43,13 @@ export class SettlementContractProtocolModule extends ContractModule< private readonly dispatchContractModule: DispatchContractProtocolModule, @inject("BridgeContract") private readonly bridgeContractModule: BridgeContractProtocolModule, - private readonly childVerificationKeyService: ChildVerificationKeyService + private readonly childVerificationKeyService: ChildVerificationKeyService, + private readonly argsRegistry: ContractArgsRegistry ) { super(); } - public contractFactory(): SmartContractClassFromInterface { + public contractFactory(): SmartContractClassFromInterface { const { hooks, config } = this; const dispatchContract = this.dispatchContractModule.contractFactory(); const bridgeContract = this.bridgeContractModule.contractFactory(); @@ -57,27 +57,27 @@ export class SettlementContractProtocolModule extends ContractModule< const escapeHatchSlotsInterval = config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; - const { args } = SettlementSmartContractBase; - SettlementSmartContractBase.args = { - ...args, - DispatchContract: dispatchContract, - hooks, - escapeHatchSlotsInterval, - BridgeContract: bridgeContract, - 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, - // So its logical that we can't avoid that here - BridgeContractBase.args.SettlementContract = SettlementSmartContract; + this.argsRegistry.addArgs( + "SettlementContract", + { + DispatchContract: dispatchContract, + hooks, + escapeHatchSlotsInterval, + BridgeContract: bridgeContract, + ChildVerificationKeyService: this.childVerificationKeyService, + } + ); - DispatchSmartContractBase.args.settlementContractClass = - SettlementSmartContract; + // Ideally, we don't want to have this cyclic dependency, but we have it in the protocol, + // So it's logical that we can't avoid that here + this.argsRegistry.addArgs("BridgeContract", { + SettlementContract: BridgingSettlementContract, + }); + this.argsRegistry.addArgs("DispatchContract", { + settlementContractClass: BridgingSettlementContract, + }); - return SettlementSmartContract; + return BridgingSettlementContract; } public async compile( @@ -91,19 +91,22 @@ export class SettlementContractProtocolModule extends ContractModule< this.contractFactory(); // 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" - ); - } + this.argsRegistry.addArgs( + "SettlementContract", + { + BridgeContractVerificationKey: + bridgeArtifact.BridgeContract.verificationKey, + } + ); log.debug("Compiling Settlement Contract"); const artifact = await registry.forceProverExists( - async (reg) => await registry.compile(SettlementSmartContract) + async (reg) => + await registry.compile( + BridgingSettlementContract, + SettlementContract.name + ) ); return { diff --git a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts index 5b3663a82..f421d9f1e 100644 --- a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts @@ -7,11 +7,12 @@ import { ContractModule, SmartContractClassFromInterface, } from "../ContractModule"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { DispatchSmartContract, DispatchContractType, - DispatchSmartContractBase, + DispatchContractArgs, } from "./DispatchSmartContract"; export type DispatchContractConfig = { @@ -23,7 +24,10 @@ export class DispatchContractProtocolModule extends ContractModule< DispatchContractType, DispatchContractConfig > { - public constructor(@inject("Runtime") private readonly runtime: RuntimeLike) { + public constructor( + @inject("Runtime") private readonly runtime: RuntimeLike, + private readonly contractArgsRegistry: ContractArgsRegistry + ) { super(); } @@ -52,20 +56,18 @@ export class DispatchContractProtocolModule extends ContractModule< this.checkConfigIntegrity(incomingMessagesMethods, methodIdMappings); - DispatchSmartContractBase.args = { - incomingMessagesPaths: incomingMessagesMethods, - methodIdMappings, - settlementContractClass: - DispatchSmartContractBase.args?.settlementContractClass, - }; + this.contractArgsRegistry.addArgs( + "DispatchContract", + { + incomingMessagesPaths: incomingMessagesMethods, + methodIdMappings, + } + ); return DispatchSmartContract; } public async compile(registry: CompileRegistry) { - if (DispatchSmartContractBase.args.settlementContractClass === undefined) { - throw new Error("Reference to Settlement Contract not set"); - } return { DispatchSmartContract: await registry.forceProverExists( async () => await registry.compile(DispatchSmartContract) diff --git a/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts b/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts index 3e4de1ab1..1767b4ebc 100644 --- a/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts +++ b/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts @@ -17,6 +17,7 @@ import { Permissions, } from "o1js"; import { InMemoryMerkleTreeStorage, TypedClass } from "@proto-kit/common"; +import { container } from "tsyringe"; import { RuntimeMethodIdMapping } from "../../model/RuntimeLike"; import { RuntimeTransaction } from "../../model/transaction/RuntimeTransaction"; @@ -25,8 +26,13 @@ import { MinaEvents, } from "../../utils/MinaPrefixedProvableHashList"; import { Deposit } from "../messages/Deposit"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../ContractArgsRegistry"; -import type { SettlementContractType } from "./SettlementSmartContract"; +import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; import { TokenBridgeDeploymentAuth } from "./authorizations/TokenBridgeDeploymentAuth"; import { UpdateMessagesHashAuth } from "./authorizations/UpdateMessagesHashAuth"; import { @@ -40,6 +46,10 @@ import { export const ACTIONS_EMPTY_HASH = Reducer.initialActionState; export interface DispatchContractType { + events: { + "token-bridge-added": typeof TokenBridgeTreeAddition; + }; + updateMessagesHash: ( executedMessagesHash: Field, newPromisedMessagesHash: Field @@ -63,14 +73,24 @@ const tokenBridgeRoot = new TokenBridgeTree( new InMemoryMerkleTreeStorage() ).getRoot(); -export abstract class DispatchSmartContractBase extends SmartContract { - public static args: { - methodIdMappings: RuntimeMethodIdMapping; - incomingMessagesPaths: Record; - settlementContractClass?: TypedClass & - typeof SmartContract; +export interface DispatchContractArgs { + methodIdMappings: RuntimeMethodIdMapping; + incomingMessagesPaths: Record; + settlementContractClass: TypedClass & + typeof SmartContract; +} + +export const DispatchContractArgsSchema: NaiveObjectSchema = + { + incomingMessagesPaths: "Required", + methodIdMappings: "Required", + settlementContractClass: "Required", }; +export abstract class DispatchSmartContractBase + extends SmartContract + implements StaticInitializationContract +{ events = { "token-bridge-added": TokenBridgeTreeAddition, // We need a placeholder event here, so that o1js internally adds a identifier to the @@ -89,6 +109,12 @@ export abstract class DispatchSmartContractBase extends SmartContract { abstract tokenBridgeCount: State; + getInitializationArgs(): DispatchContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("DispatchContract", DispatchContractArgsSchema); + } + protected updateMessagesHashBase( executedMessagesHash: Field, newPromisedMessagesHash: Field @@ -105,12 +131,12 @@ export abstract class DispatchSmartContractBase extends SmartContract { this.self.account.actionState.requireEquals(newPromisedMessagesHash); this.promisedMessagesHash.set(newPromisedMessagesHash); + const args = this.getInitializationArgs(); const settlementContractAddress = this.settlementContract.getAndRequireEquals(); - const settlementContract = - new DispatchSmartContractBase.args.settlementContractClass!( - settlementContractAddress - ); + const settlementContract = new args.settlementContractClass!( + settlementContractAddress + ); settlementContract.authorizationField.requireEquals( new UpdateMessagesHashAuth({ @@ -175,10 +201,10 @@ export abstract class DispatchSmartContractBase extends SmartContract { // treeWitness: TokenBridgeTreeWitness ) { this.settlementContract.requireEquals(settlementContractAddress); - const settlementContract = - new DispatchSmartContractBase.args.settlementContractClass!( - settlementContractAddress - ); + const args = this.getInitializationArgs(); + const settlementContract = new args.settlementContractClass!( + settlementContractAddress + ); // Append bridge address to the tree // TODO This not concurrent and will fail if multiple users deploy bridges at the same time @@ -318,7 +344,7 @@ export class DispatchSmartContract }); const { methodIdMappings, incomingMessagesPaths } = - DispatchSmartContractBase.args; + this.getInitializationArgs(); const methodId = Field( methodIdMappings[incomingMessagesPaths.deposit].methodId diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts deleted file mode 100644 index 06ee0ce68..000000000 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts +++ /dev/null @@ -1,479 +0,0 @@ -import { - prefixToField, - TypedClass, - mapSequential, - ChildVerificationKeyService, - LinkedMerkleTree, -} from "@proto-kit/common"; -import { - AccountUpdate, - Bool, - Field, - method, - PublicKey, - Signature, - SmartContract, - State, - state, - UInt32, - AccountUpdateForest, - TokenContract, - VerificationKey, - Permissions, - Struct, - Provable, - TokenId, - DynamicProof, - DeployArgs, -} from "o1js"; - -import { NetworkState } from "../../model/network/NetworkState"; -import { BlockHashMerkleTree } from "../../prover/block/accummulators/BlockHashMerkleTree"; -import { - BlockProverPublicInput, - BlockProverPublicOutput, -} from "../../prover/block/BlockProvable"; -import { - ProvableSettlementHook, - SettlementHookInputs, - SettlementStateRecord, -} from "../modularity/ProvableSettlementHook"; - -import { DispatchContractType } from "./DispatchSmartContract"; -import { BridgeContractType } from "./BridgeContract"; -import { TokenBridgeDeploymentAuth } from "./authorizations/TokenBridgeDeploymentAuth"; -import { UpdateMessagesHashAuth } from "./authorizations/UpdateMessagesHashAuth"; - -/* eslint-disable @typescript-eslint/lines-between-class-members */ - -export class DynamicBlockProof extends DynamicProof< - BlockProverPublicInput, - BlockProverPublicOutput -> { - public static publicInputType = BlockProverPublicInput; - - public static publicOutputType = BlockProverPublicOutput; - - public static maxProofsVerified = 2 as const; -} - -export class TokenMapping extends Struct({ - tokenId: Field, - publicKey: PublicKey, -}) {} - -export interface SettlementContractType { - authorizationField: State; - - deployAndInitialize: ( - args: DeployArgs | undefined, - permissions: Permissions, - sequencer: PublicKey, - dispatchContract: PublicKey - ) => Promise; - - assertStateRoot: (root: Field) => AccountUpdate; - settle: ( - blockProof: DynamicBlockProof, - signature: Signature, - dispatchContractAddress: PublicKey, - publicKey: PublicKey, - inputNetworkState: NetworkState, - outputNetworkState: NetworkState, - newPromisedMessagesHash: Field - ) => Promise; - addTokenBridge: ( - tokenId: Field, - address: PublicKey, - dispatchContract: PublicKey - ) => Promise; -} - -// 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; -// }; -// } - -export abstract class SettlementSmartContractBase extends TokenContract { - // This pattern of injecting args into a smartcontract is currently the only - // viable solution that works given the inheritance issues of o1js - // public static args = container.resolve(SettlementSmartContractStaticArgs); - public static args: { - DispatchContract: TypedClass; - hooks: ProvableSettlementHook[]; - escapeHatchSlotsInterval: number; - BridgeContract: TypedClass & typeof SmartContract; - // Lazily initialized - BridgeContractVerificationKey: VerificationKey | undefined; - BridgeContractPermissions: Permissions | undefined; - signedSettlements: boolean | undefined; - ChildVerificationKeyService: ChildVerificationKeyService; - }; - - events = { - "token-bridge-deployed": TokenMapping, - }; - - abstract sequencerKey: State; - abstract lastSettlementL1BlockHeight: State; - abstract stateRoot: State; - abstract networkStateHash: State; - abstract blockHashRoot: State; - abstract dispatchContractAddressX: State; - - abstract authorizationField: State; - - // Not @state - // abstract offchainStateCommitmentsHash: State; - - public assertStateRoot(root: Field): AccountUpdate { - this.stateRoot.requireEquals(root); - return this.self; - } - - // TODO Like these properties, I am too lazy to properly infer the types here - private assertLazyConfigsInitialized() { - const uninitializedProperties: string[] = []; - const { args } = SettlementSmartContractBase; - if (args.BridgeContractPermissions === undefined) { - uninitializedProperties.push("BridgeContractPermissions"); - } - if (args.signedSettlements === undefined) { - uninitializedProperties.push("signedSettlements"); - } - if (uninitializedProperties.length > 0) { - throw new Error( - `Lazy configs of SettlementSmartContract haven't been initialized ${uninitializedProperties.reduce( - (a, b) => `${a},${b}` - )}` - ); - } - } - - protected async deployTokenBridge( - tokenId: Field, - address: PublicKey, - dispatchContractAddress: PublicKey, - dispatchContractPreconditionEnforced = false - ) { - Provable.asProver(() => { - this.assertLazyConfigsInitialized(); - }); - - const { args } = SettlementSmartContractBase; - 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( - args.BridgeContractVerificationKey, - args.signedSettlements!, - args.BridgeContractPermissions!, - this.address - ); - - this.approve(deploymentAccountUpdate); - - this.self.body.mayUseToken = { - // Only set this if we deploy a custom token - parentsOwnToken: tokenId.equals(TokenId.default).not(), - inheritFromParent: Bool(false), - }; - - this.emitEvent( - "token-bridge-deployed", - new TokenMapping({ - tokenId: tokenId, - publicKey: address, - }) - ); - - // We can't set a precondition twice, for the $mina bridge deployment that - // would be the case, so we disable it in this case - if (!dispatchContractPreconditionEnforced) { - this.dispatchContractAddressX.requireEquals(dispatchContractAddress.x); - } - - // Set authorization for the auth callback, that we need - this.authorizationField.set( - new TokenBridgeDeploymentAuth({ - target: dispatchContractAddress, - tokenId, - address, - }).hash() - ); - const dispatchContract = - new SettlementSmartContractBase.args.DispatchContract( - dispatchContractAddress - ); - await dispatchContract.enableTokenDeposits(tokenId, address, this.address); - } - - protected async initializeBase( - sequencer: PublicKey, - dispatchContract: PublicKey - ) { - this.sequencerKey.set(sequencer.x); - this.stateRoot.set(LinkedMerkleTree.EMPTY_ROOT); - this.blockHashRoot.set(Field(BlockHashMerkleTree.EMPTY_ROOT)); - this.networkStateHash.set(NetworkState.empty().hash()); - this.dispatchContractAddressX.set(dispatchContract.x); - } - - protected async settleBase( - blockProof: DynamicBlockProof, - signature: Signature, - dispatchContractAddress: PublicKey, - publicKey: PublicKey, - inputNetworkState: NetworkState, - 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(blockProofVk); - - // Get and assert on-chain values - const stateRoot = this.stateRoot.getAndRequireEquals(); - const networkStateHash = this.networkStateHash.getAndRequireEquals(); - const blockHashRoot = this.blockHashRoot.getAndRequireEquals(); - const sequencerKey = this.sequencerKey.getAndRequireEquals(); - const lastSettlementL1BlockHeight = - this.lastSettlementL1BlockHeight.getAndRequireEquals(); - const onChainDispatchContractAddressX = - this.dispatchContractAddressX.getAndRequireEquals(); - - onChainDispatchContractAddressX.assertEquals( - dispatchContractAddress.x, - "DispatchContract address not provided correctly" - ); - - const { DispatchContract, escapeHatchSlotsInterval, hooks } = - SettlementSmartContractBase.args; - - // Get dispatch contract values - // These values are witnesses but will be checked later on the AU - // call to the dispatch contract via .updateMessagesHash() - const dispatchContract = new DispatchContract(dispatchContractAddress); - const promisedMessagesHash = dispatchContract.promisedMessagesHash.get(); - - // Get block height and use the lower bound for all ops - const minBlockHeightIncluded = this.network.blockchainLength.get(); - this.network.blockchainLength.requireBetween( - minBlockHeightIncluded, - // 5 because that is the length the newPromisedMessagesHash will be valid - minBlockHeightIncluded.add(4) - ); - - // Check signature/escape catch - publicKey.x.assertEquals( - sequencerKey, - "Sequencer public key witness not matching" - ); - const signatureValid = signature.verify(publicKey, [ - BATCH_SIGNATURE_PREFIX, - lastSettlementL1BlockHeight.value, - ]); - const escapeHatchActivated = lastSettlementL1BlockHeight - .add(UInt32.from(escapeHatchSlotsInterval)) - .lessThan(minBlockHeightIncluded); - signatureValid - .or(escapeHatchActivated) - .assertTrue( - "Sequencer signature not valid and escape hatch not activated" - ); - - // Assert correctness of networkState witness - inputNetworkState - .hash() - .assertEquals(networkStateHash, "InputNetworkState witness not valid"); - outputNetworkState - .hash() - .assertEquals( - blockProof.publicOutput.networkStateHash, - "OutputNetworkState witness not valid" - ); - - blockProof.publicOutput.closed.assertEquals( - Bool(true), - "Supplied proof is not a closed BlockProof" - ); - blockProof.publicOutput.pendingSTBatchesHash.assertEquals( - Field(0), - "Supplied proof is has outstanding STs to be proven" - ); - - // Execute onSettlementHooks for additional checks - const stateRecord: SettlementStateRecord = { - blockHashRoot, - stateRoot, - networkStateHash, - lastSettlementL1BlockHeight, - sequencerKey: publicKey, - }; - const inputs: SettlementHookInputs = { - blockProof, - contractState: stateRecord, - newPromisedMessagesHash, - fromNetworkState: inputNetworkState, - toNetworkState: outputNetworkState, - currentL1BlockHeight: minBlockHeightIncluded, - }; - await mapSequential(hooks, async (hook) => { - await hook.beforeSettlement(this, inputs); - }); - - // Apply blockProof - stateRoot.assertEquals( - blockProof.publicInput.stateRoot, - "Input state root not matching" - ); - - networkStateHash.assertEquals( - blockProof.publicInput.networkStateHash, - "Input networkStateHash not matching" - ); - blockHashRoot.assertEquals( - blockProof.publicInput.blockHashRoot, - "Input blockHashRoot not matching" - ); - this.stateRoot.set(blockProof.publicOutput.stateRoot); - this.networkStateHash.set(blockProof.publicOutput.networkStateHash); - this.blockHashRoot.set(blockProof.publicOutput.blockHashRoot); - - // Assert and apply deposit commitments - promisedMessagesHash.assertEquals( - blockProof.publicOutput.incomingMessagesHash, - "Promised messages not honored" - ); - - // Set authorization for the dispatchContract to verify the messages hash update - this.authorizationField.set( - new UpdateMessagesHashAuth({ - target: dispatchContract.address, - executedMessagesHash: promisedMessagesHash, - newPromisedMessagesHash, - }).hash() - ); - - // Call DispatchContract - // This call checks that the promisedMessagesHash, which is already proven - // to be the blockProofs publicoutput, is actually the current on-chain - // promisedMessageHash. It also checks the newPromisedMessagesHash to be - // a current sequencestate value - await dispatchContract.updateMessagesHash( - promisedMessagesHash, - newPromisedMessagesHash - ); - - this.lastSettlementL1BlockHeight.set(minBlockHeightIncluded); - } -} - -export class SettlementSmartContract - extends SettlementSmartContractBase - implements SettlementContractType -{ - @state(Field) public sequencerKey = State(); - @state(UInt32) public lastSettlementL1BlockHeight = State(); - - @state(Field) public stateRoot = State(); - @state(Field) public networkStateHash = State(); - @state(Field) public blockHashRoot = State(); - - @state(Field) public dispatchContractAddressX = State(); - - @state(Field) public authorizationField = State(); - - public async deployAndInitialize( - args: DeployArgs | undefined, - permissions: Permissions, - sequencer: PublicKey, - dispatchContract: PublicKey - ): Promise { - await super.deploy(args); - - this.self.account.permissions.set(permissions); - - await this.initializeBase(sequencer, dispatchContract); - } - - @method async approveBase(forest: AccountUpdateForest) { - this.checkZeroBalanceChange(forest); - } - - @method - public async addTokenBridge( - tokenId: Field, - address: PublicKey, - dispatchContract: PublicKey - ) { - await this.deployTokenBridge(tokenId, address, dispatchContract); - } - - @method - public async settle( - blockProof: DynamicBlockProof, - signature: Signature, - dispatchContractAddress: PublicKey, - publicKey: PublicKey, - inputNetworkState: NetworkState, - outputNetworkState: NetworkState, - newPromisedMessagesHash: Field - ) { - return await this.settleBase( - blockProof, - signature, - dispatchContractAddress, - publicKey, - inputNetworkState, - outputNetworkState, - newPromisedMessagesHash - ); - } -} - -/* eslint-enable @typescript-eslint/lines-between-class-members */ diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts new file mode 100644 index 000000000..c776e5ff5 --- /dev/null +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts @@ -0,0 +1,79 @@ +import { inject, injectable, injectAll } from "tsyringe"; +import { + ArtifactRecord, + ChildVerificationKeyService, + CompileRegistry, + log, +} from "@proto-kit/common"; + +import { BlockProvable } from "../../prover/block/BlockProvable"; +import { + ContractModule, + SmartContractClassFromInterface, +} from "../ContractModule"; +import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; + +import { + SettlementContractArgs, + SettlementContractType, +} from "./settlement/SettlementBase"; +import { SettlementContract } from "./settlement/SettlementContract"; + +export type SettlementContractConfig = { + escapeHatchSlotsInterval?: number; +}; + +// 24 hours +export const DEFAULT_ESCAPE_HATCH = (60 / 3) * 24; + +@injectable() +export class SettlementSmartContractModule extends ContractModule< + SettlementContractType, + SettlementContractConfig +> { + public constructor( + @injectAll("ProvableSettlementHook") + private readonly hooks: ProvableSettlementHook[], + @inject("BlockProver") + private readonly blockProver: BlockProvable, + private readonly childVerificationKeyService: ChildVerificationKeyService, + private readonly argsRegistry: ContractArgsRegistry + ) { + super(); + } + + public contractFactory(): SmartContractClassFromInterface { + const { hooks, config } = this; + + const escapeHatchSlotsInterval = + config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; + + this.argsRegistry.addArgs("SettlementContract", { + hooks, + escapeHatchSlotsInterval, + ChildVerificationKeyService: this.childVerificationKeyService, + }); + + return SettlementContract; + } + + public async compile( + registry: CompileRegistry + ): Promise { + // Dependencies + await this.blockProver.compile(registry); + + this.contractFactory(); + + log.debug("Compiling Settlement Contract"); + + const artifact = await registry.forceProverExists( + async (reg) => await registry.compile(SettlementContract) + ); + + return { + SettlementSmartContract: artifact, + }; + } +} diff --git a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts new file mode 100644 index 000000000..4865e4ec5 --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts @@ -0,0 +1,283 @@ +import { TypedClass, O1PublicKeyOption } from "@proto-kit/common"; +import { + AccountUpdate, + Bool, + Field, + method, + PublicKey, + Signature, + SmartContract, + State, + state, + UInt32, + AccountUpdateForest, + VerificationKey, + Permissions, + Struct, + TokenId, + DeployArgs, +} from "o1js"; +import { container } from "tsyringe"; + +import { NetworkState } from "../../../model/network/NetworkState"; +import { DispatchContractType } from "../DispatchSmartContract"; +import { BridgeContractType } from "../BridgeContract"; +import { TokenBridgeDeploymentAuth } from "../authorizations/TokenBridgeDeploymentAuth"; +import { UpdateMessagesHashAuth } from "../authorizations/UpdateMessagesHashAuth"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../../ContractArgsRegistry"; + +import { + DynamicBlockProof, + SettlementBase, + SettlementContractArgs, + SettlementContractArgsSchema, + SettlementContractType, +} from "./SettlementBase"; + +/* eslint-disable @typescript-eslint/lines-between-class-members */ + +export class TokenMapping extends Struct({ + tokenId: Field, + publicKey: PublicKey, +}) {} + +export interface BridgingSettlementContractType extends SettlementContractType { + authorizationField: State; + + assertStateRoot: (root: Field) => AccountUpdate; + addTokenBridge: (tokenId: Field, address: PublicKey) => Promise; +} + +export interface BridgingSettlementContractArgs extends SettlementContractArgs { + DispatchContract: TypedClass; + BridgeContract: TypedClass & typeof SmartContract; + // Lazily initialized + BridgeContractVerificationKey: VerificationKey | undefined; + BridgeContractPermissions: Permissions; +} + +export const BridgingSettlementContractArgsSchema: NaiveObjectSchema = + { + ...SettlementContractArgsSchema, + DispatchContract: "Required", + BridgeContract: "Required", + BridgeContractVerificationKey: "Optional", + BridgeContractPermissions: "Required", + }; + +export abstract class BridgingSettlementContractBase + extends SettlementBase + implements StaticInitializationContract +{ + public getInitializationArgs(): BridgingSettlementContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("SettlementContract", BridgingSettlementContractArgsSchema); + } + + events = { + "token-bridge-deployed": TokenMapping, + }; + + abstract dispatchContractAddress: State; + + abstract authorizationField: State; + + // Not @state + // abstract offchainStateCommitmentsHash: State; + + public assertStateRoot(root: Field): AccountUpdate { + this.stateRoot.requireEquals(root); + return this.self; + } + + protected async initializeBaseBridging( + sequencer: PublicKey, + dispatchContract: PublicKey + ) { + await super.initializeBase(sequencer); + + this.dispatchContractAddress.set(dispatchContract); + } + + // TODO We should move this to the dispatchcontract eventually - or after mesa + // to the combined settlement & dispatch contract + protected async deployTokenBridge(tokenId: Field, address: PublicKey) { + const { + BridgeContractVerificationKey, + signedSettlements, + BridgeContractPermissions, + BridgeContract: BridgeContractClass, + DispatchContract, + } = this.getInitializationArgs(); + + const bridgeContract = new BridgeContractClass(address, tokenId); + + 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( + BridgeContractVerificationKey, + signedSettlements!, + BridgeContractPermissions!, + this.address + ); + + this.approve(deploymentAccountUpdate); + + this.self.body.mayUseToken = { + // Only set this if we deploy a custom token + parentsOwnToken: tokenId.equals(TokenId.default).not(), + inheritFromParent: Bool(false), + }; + + this.emitEvent( + "token-bridge-deployed", + new TokenMapping({ + tokenId: tokenId, + publicKey: address, + }) + ); + + const dispatchContractAddress = + this.dispatchContractAddress.getAndRequireEquals(); + + // Set authorization for the auth callback, that we need + this.authorizationField.set( + new TokenBridgeDeploymentAuth({ + target: dispatchContractAddress, + tokenId, + address, + }).hash() + ); + const dispatchContract = new DispatchContract(dispatchContractAddress); + await dispatchContract.enableTokenDeposits(tokenId, address, this.address); + } + + protected async settleBaseBridging( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) { + await super.settleBase( + blockProof, + signature, + publicKey, + inputNetworkState, + outputNetworkState, + newPromisedMessagesHash + ); + + const dispatchContractAddress = + this.dispatchContractAddress.getAndRequireEquals(); + + const { DispatchContract } = this.getInitializationArgs(); + + // Get dispatch contract values + // These values are witnesses but will be checked later on the AU + // call to the dispatch contract via .updateMessagesHash() + const dispatchContract = new DispatchContract(dispatchContractAddress); + const promisedMessagesHash = dispatchContract.promisedMessagesHash.get(); + + // Assert and apply deposit commitments + promisedMessagesHash.assertEquals( + blockProof.publicOutput.incomingMessagesHash, + "Promised messages not honored" + ); + + // Set authorization for the dispatchContract to verify the messages hash update + this.authorizationField.set( + new UpdateMessagesHashAuth({ + target: dispatchContract.address, + executedMessagesHash: promisedMessagesHash, + newPromisedMessagesHash, + }).hash() + ); + + // Call DispatchContract + // This call checks that the promisedMessagesHash, which is already proven + // to be the blockProofs publicoutput, is actually the current on-chain + // promisedMessageHash. It also checks the newPromisedMessagesHash to be + // a current sequencestate value + await dispatchContract.updateMessagesHash( + promisedMessagesHash, + newPromisedMessagesHash + ); + } +} + +export class BridgingSettlementContract + extends BridgingSettlementContractBase + implements BridgingSettlementContractType +{ + @state(Field) public sequencerKey = State(); + @state(UInt32) public lastSettlementL1BlockHeight = State(); + + @state(Field) public stateRoot = State(); + @state(Field) public networkStateHash = State(); + @state(Field) public blockHashRoot = State(); + + @state(PublicKey) public dispatchContractAddress = State(); + + @state(Field) public authorizationField = State(); + + public async deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: O1PublicKeyOption + ): Promise { + dispatchContract.assertSome( + "Bridging-enabled settlement contract requires a dispatch contract address" + ); + + await super.deploy(args); + + this.self.account.permissions.set(permissions); + + await this.initializeBaseBridging(sequencer, dispatchContract.value); + } + + @method async approveBase(forest: AccountUpdateForest) { + this.checkZeroBalanceChange(forest); + } + + @method + public async addTokenBridge(tokenId: Field, address: PublicKey) { + await this.deployTokenBridge(tokenId, address); + } + + @method + public async settle( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) { + return await this.settleBaseBridging( + blockProof, + signature, + publicKey, + inputNetworkState, + outputNetworkState, + newPromisedMessagesHash + ); + } +} + +/* eslint-enable @typescript-eslint/lines-between-class-members */ diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts new file mode 100644 index 000000000..cc6401c03 --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -0,0 +1,253 @@ +import { + Bool, + DeployArgs, + DynamicProof, + Field, + Option, + PublicKey, + Signature, + State, + TokenContract, + UInt32, + Permissions, +} from "o1js"; +import { + ChildVerificationKeyService, + LinkedMerkleTree, + mapSequential, + prefixToField, +} from "@proto-kit/common"; +import { container } from "tsyringe"; + +import { BlockHashMerkleTree } from "../../../prover/block/accummulators/BlockHashMerkleTree"; +import { NetworkState } from "../../../model/network/NetworkState"; +import { + ProvableSettlementHook, + SettlementHookInputs, + SettlementStateRecord, +} from "../../modularity/ProvableSettlementHook"; +import { + BlockProverPublicInput, + BlockProverPublicOutput, +} from "../../../prover/block/BlockProvable"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../../ContractArgsRegistry"; + +/* eslint-disable @typescript-eslint/lines-between-class-members */ + +// Some random prefix for the sequencer signature +export const BATCH_SIGNATURE_PREFIX = prefixToField("pk-batchSignature"); + +export class DynamicBlockProof extends DynamicProof< + BlockProverPublicInput, + BlockProverPublicOutput +> { + public static publicInputType = BlockProverPublicInput; + + public static publicOutputType = BlockProverPublicOutput; + + public static maxProofsVerified = 2 as const; +} + +export interface SettlementContractType { + sequencerKey: State; + lastSettlementL1BlockHeight: State; + stateRoot: State; + networkStateHash: State; + blockHashRoot: State; + + deployAndInitialize: ( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: Option + ) => Promise; + + settle: ( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) => Promise; +} + +export interface SettlementContractArgs { + hooks: ProvableSettlementHook[]; + escapeHatchSlotsInterval: number; + signedSettlements: boolean; + ChildVerificationKeyService: ChildVerificationKeyService; +} + +export const SettlementContractArgsSchema: NaiveObjectSchema = + { + ChildVerificationKeyService: "Required", + hooks: "Required", + escapeHatchSlotsInterval: "Required", + signedSettlements: "Required", + }; + +export abstract class SettlementBase + extends TokenContract + implements StaticInitializationContract +{ + getInitializationArgs(): SettlementContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("SettlementContract", SettlementContractArgsSchema); + } + + abstract sequencerKey: State; + abstract lastSettlementL1BlockHeight: State; + abstract stateRoot: State; + abstract networkStateHash: State; + abstract blockHashRoot: State; + + protected async initializeBase(sequencer: PublicKey) { + this.sequencerKey.set(sequencer.x); + this.stateRoot.set(LinkedMerkleTree.EMPTY_ROOT); + this.blockHashRoot.set(Field(BlockHashMerkleTree.EMPTY_ROOT)); + this.networkStateHash.set(NetworkState.empty().hash()); + } + + abstract settle( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ): Promise; + + abstract deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: Option + ): Promise; + + protected async settleBase( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) { + const { + escapeHatchSlotsInterval, + hooks, + ChildVerificationKeyService: childVerificationKeyService, + } = this.getInitializationArgs(); + + // Brought in as a constant + const blockProofVk = + childVerificationKeyService.getVerificationKey("BlockProver"); + if (!blockProofVk.hash.isConstant()) { + throw new Error("Sanity check - vk hash has to be constant"); + } + // Verify the blockproof + + blockProof.verify(blockProofVk); + // Get and assert on-chain values + const stateRoot = this.stateRoot.getAndRequireEquals(); + const networkStateHash = this.networkStateHash.getAndRequireEquals(); + const blockHashRoot = this.blockHashRoot.getAndRequireEquals(); + const sequencerKey = this.sequencerKey.getAndRequireEquals(); + + const lastSettlementL1BlockHeight = + this.lastSettlementL1BlockHeight.getAndRequireEquals(); + + // Get block height and use the lower bound for all ops + const minBlockHeightIncluded = this.network.blockchainLength.get(); + this.network.blockchainLength.requireBetween( + minBlockHeightIncluded, + // 5 because that is the length the newPromisedMessagesHash will be valid + minBlockHeightIncluded.add(4) + ); + + // Check signature/escape catch + publicKey.x.assertEquals( + sequencerKey, + "Sequencer public key witness not matching" + ); + const signatureValid = signature.verify(publicKey, [ + BATCH_SIGNATURE_PREFIX, + lastSettlementL1BlockHeight.value, + ]); + const escapeHatchActivated = lastSettlementL1BlockHeight + .add(UInt32.from(escapeHatchSlotsInterval)) + .lessThan(minBlockHeightIncluded); + signatureValid + .or(escapeHatchActivated) + .assertTrue( + "Sequencer signature not valid and escape hatch not activated" + ); + + // Assert correctness of networkState witness + inputNetworkState + .hash() + .assertEquals(networkStateHash, "InputNetworkState witness not valid"); + outputNetworkState + .hash() + .assertEquals( + blockProof.publicOutput.networkStateHash, + "OutputNetworkState witness not valid" + ); + + blockProof.publicOutput.closed.assertEquals( + Bool(true), + "Supplied proof is not a closed BlockProof" + ); + blockProof.publicOutput.pendingSTBatchesHash.assertEquals( + Field(0), + "Supplied proof is has outstanding STs to be proven" + ); + + // Execute onSettlementHooks for additional checks + const stateRecord: SettlementStateRecord = { + blockHashRoot, + stateRoot, + networkStateHash, + lastSettlementL1BlockHeight, + sequencerKey: publicKey, + }; + const inputs: SettlementHookInputs = { + blockProof, + contractState: stateRecord, + newPromisedMessagesHash, + fromNetworkState: inputNetworkState, + toNetworkState: outputNetworkState, + currentL1BlockHeight: minBlockHeightIncluded, + }; + await mapSequential(hooks, async (hook) => { + await hook.beforeSettlement(this, inputs); + }); + + // Apply blockProof + stateRoot.assertEquals( + blockProof.publicInput.stateRoot, + "Input state root not matching" + ); + + networkStateHash.assertEquals( + blockProof.publicInput.networkStateHash, + "Input networkStateHash not matching" + ); + blockHashRoot.assertEquals( + blockProof.publicInput.blockHashRoot, + "Input blockHashRoot not matching" + ); + this.stateRoot.set(blockProof.publicOutput.stateRoot); + this.networkStateHash.set(blockProof.publicOutput.networkStateHash); + this.blockHashRoot.set(blockProof.publicOutput.blockHashRoot); + + this.lastSettlementL1BlockHeight.set(minBlockHeightIncluded); + } +} + +/* eslint-enable @typescript-eslint/lines-between-class-members */ diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts new file mode 100644 index 000000000..95321684f --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts @@ -0,0 +1,75 @@ +import { + State, + UInt32, + AccountUpdateForest, + state, + method, + PublicKey, + Field, + Signature, + DeployArgs, + Permissions, +} from "o1js"; +import { O1PublicKeyOption } from "@proto-kit/common"; + +import { NetworkState } from "../../../model/network/NetworkState"; + +import { + DynamicBlockProof, + SettlementBase, + SettlementContractType, +} from "./SettlementBase"; + +export class SettlementContract + extends SettlementBase + implements SettlementContractType +{ + @state(Field) sequencerKey = State(); + + @state(UInt32) lastSettlementL1BlockHeight = State(); + + @state(Field) stateRoot = State(); + + @state(Field) networkStateHash = State(); + + @state(Field) blockHashRoot = State(); + + public async deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: O1PublicKeyOption + ): Promise { + dispatchContract.assertNone( + "Non-bridging settlement contract doesn't require a dispatch contract" + ); + + await super.deploy(args); + + this.self.account.permissions.set(permissions); + + await this.initializeBase(sequencer); + } + + @method async approveBase(forest: AccountUpdateForest) { + this.checkZeroBalanceChange(forest); + } + + @method async settle( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ): Promise { + await super.settleBase( + blockProof, + signature, + publicKey, + inputNetworkState, + outputNetworkState, + newPromisedMessagesHash + ); + } +} diff --git a/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts b/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts index ecb117129..65ccf7a12 100644 --- a/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts +++ b/packages/protocol/src/settlement/modularity/ProvableSettlementHook.ts @@ -4,7 +4,7 @@ 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"; +import type { SettlementContractType } from "../contracts/settlement/SettlementBase"; export type InputBlockProof = InferProofBase; @@ -30,7 +30,7 @@ export abstract class ProvableSettlementHook< Config, > extends ProtocolModule { public abstract beforeSettlement( - smartContract: SettlementSmartContractBase, + smartContract: SettlementContractType, inputs: SettlementHookInputs ): Promise; } diff --git a/packages/protocol/src/settlement/modularity/types.ts b/packages/protocol/src/settlement/modularity/types.ts index 8f7c706e6..b05abdf6f 100644 --- a/packages/protocol/src/settlement/modularity/types.ts +++ b/packages/protocol/src/settlement/modularity/types.ts @@ -1,4 +1,5 @@ import { TypedClass } from "@proto-kit/common"; +import { SmartContract } from "o1js"; import { ContractModule, @@ -11,7 +12,7 @@ export type InferContractType< > = Module extends TypedClass ? ConcreteModule extends ContractModule - ? Contract + ? Contract & SmartContract : never : never; diff --git a/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts b/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts index 739ccc27a..169492175 100644 --- a/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts +++ b/packages/protocol/src/settlement/modules/NetworkStateSettlementModule.ts @@ -4,7 +4,7 @@ import { ProvableSettlementHook, SettlementHookInputs, } from "../modularity/ProvableSettlementHook"; -import { SettlementSmartContract } from "../contracts/SettlementSmartContract"; +import { SettlementContractType } from "../contracts/settlement/SettlementBase"; type NetworkStateSettlementModuleConfig = { blocksPerL1Block: UInt64; @@ -13,7 +13,7 @@ type NetworkStateSettlementModuleConfig = { /* eslint-disable @typescript-eslint/no-unused-vars */ export class NetworkStateSettlementModule extends ProvableSettlementHook { public async beforeSettlement( - smartContract: SettlementSmartContract, + smartContract: SettlementContractType, { blockProof, fromNetworkState, diff --git a/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts b/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts index e37019b32..2681a29aa 100644 --- a/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts +++ b/packages/sequencer/src/protocol/baselayer/MinaBaseLayer.ts @@ -2,7 +2,6 @@ import { AreProofsEnabled, DependencyFactory, ModuleContainerLike, - DependencyRecord, } from "@proto-kit/common"; import { Mina } from "o1js"; import { match } from "ts-pattern"; @@ -15,7 +14,6 @@ import { } from "../../sequencer/builder/SequencerModule"; import { MinaTransactionSender } from "../../settlement/transactions/MinaTransactionSender"; import { DefaultOutgoingMessageAdapter } from "../../settlement/messages/outgoing/DefaultOutgoingMessageAdapter"; -import { IncomingMessagesService } from "../../settlement/messages/IncomingMessagesService"; import { BaseLayer } from "./BaseLayer"; import { LocalBlockchainUtils } from "./network-utils/LocalBlockchainUtils"; @@ -64,14 +62,6 @@ export class MinaBaseLayer super(); } - public static dependencies() { - return { - IncomingMessagesService: { - useClass: IncomingMessagesService, - }, - } satisfies DependencyRecord; - } - public dependencies() { const NetworkUtilsClass = match(this.config.network.type) .with("local", () => LocalBlockchainUtils) @@ -151,5 +141,3 @@ export class MinaBaseLayer this.network = Network; } } - -MinaBaseLayer satisfies DependencyFactory; diff --git a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts index f3a06bd72..2e14928d8 100644 --- a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts @@ -15,8 +15,10 @@ import { Protocol, SettlementContractModule, RuntimeVerificationKeyRootService, - SettlementSmartContractBase, MandatoryProtocolModulesRecord, + type SettlementModulesRecord, + BridgingSettlementContractArgs, + ContractArgsRegistry, } from "@proto-kit/protocol"; import { TaskSerializer } from "../../../worker/flow/Task"; @@ -48,7 +50,8 @@ export class CircuitCompilerTask extends UnpreparingTask< @inject("Runtime") protected readonly runtime: Runtime, @inject("Protocol") protected readonly protocol: Protocol, - private readonly compileRegistry: CompileRegistry + private readonly compileRegistry: CompileRegistry, + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -97,7 +100,7 @@ export class CircuitCompilerTask extends UnpreparingTask< const container = this.protocol.dependencyContainer; if (container.isRegistered("SettlementContractModule")) { const settlementModule = container.resolve< - SettlementContractModule + SettlementContractModule >("SettlementContractModule"); // Needed so that all contractFactory functions are called, because @@ -115,9 +118,10 @@ export class CircuitCompilerTask extends UnpreparingTask< const sumModule = { compile: async (registry: CompileRegistry) => { - await reduceSequential( - modules.map(([, module]) => module), - async (record, module) => { + await reduceSequential<[string, CompilableModule], ArtifactRecord>( + modules, + async (record, [moduleName, module]) => { + log.info(`Compiling ${moduleName}`); const artifacts = await module.compile(registry); return { ...record, @@ -129,9 +133,9 @@ export class CircuitCompilerTask extends UnpreparingTask< }, }; - modules.push(["Settlement", sumModule]); + const combinedModules = [...modules, ["Settlement", sumModule]]; - return Object.fromEntries(modules); + return Object.fromEntries(combinedModules); } return {}; } @@ -150,16 +154,17 @@ export class CircuitCompilerTask extends UnpreparingTask< } if (input.isSignedSettlement !== undefined) { - const contractArgs = SettlementSmartContractBase.args; - SettlementSmartContractBase.args = { - ...contractArgs, - signedSettlements: input.isSignedSettlement, - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (input.isSignedSettlement - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { + signedSettlements: input.isSignedSettlement, + // TODO Add distinction between mina and custom tokens + BridgeContractPermissions: (input.isSignedSettlement + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions() + ).bridgeContractMina(), + } + ); } // TODO make adaptive diff --git a/packages/sequencer/src/sequencer/SequencerStartupModule.ts b/packages/sequencer/src/sequencer/SequencerStartupModule.ts index 8d2294de0..603835f7a 100644 --- a/packages/sequencer/src/sequencer/SequencerStartupModule.ts +++ b/packages/sequencer/src/sequencer/SequencerStartupModule.ts @@ -1,9 +1,10 @@ import { inject } from "tsyringe"; import { + BridgingSettlementContractArgs, + ContractArgsRegistry, MandatoryProtocolModulesRecord, Protocol, RuntimeVerificationKeyRootService, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { log, @@ -43,7 +44,8 @@ export class SequencerStartupModule @inject("BaseLayer", { isOptional: true }) private readonly baseLayer: MinaBaseLayer | undefined, @inject("AreProofsEnabled") - private readonly areProofsEnabled: AreProofsEnabled + private readonly areProofsEnabled: AreProofsEnabled, + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -168,8 +170,13 @@ export class SequencerStartupModule // Init BridgeContract vk for settlement contract const bridgeVk = protocolBridgeArtifacts.BridgeContract; if (bridgeVk !== undefined) { - SettlementSmartContractBase.args.BridgeContractVerificationKey = - bridgeVk.verificationKey; + // TODO Inject CompileRegistry directly + this.contractArgsRegistry.addArgs( + "SettlementContract", + { + BridgeContractVerificationKey: bridgeVk.verificationKey, + } + ); } await this.registrationFlow.start({ diff --git a/packages/sequencer/src/sequencer/SettlementStartupModule.ts b/packages/sequencer/src/sequencer/SettlementStartupModule.ts index 8a76e7284..1936fb3c1 100644 --- a/packages/sequencer/src/sequencer/SettlementStartupModule.ts +++ b/packages/sequencer/src/sequencer/SettlementStartupModule.ts @@ -11,6 +11,7 @@ import { CircuitCompilerTask } from "../protocol/production/tasks/CircuitCompile @injectable() export class SettlementStartupModule { + // TODO Why is this a separate module? public constructor( private readonly compileRegistry: CompileRegistry, private readonly flowCreator: FlowCreator, @@ -38,40 +39,38 @@ export class SettlementStartupModule { return artifacts; } - private async getArtifacts(retry: boolean): Promise<{ - SettlementSmartContract: CompileArtifact; - DispatchSmartContract: CompileArtifact; - }> { - const settlementVerificationKey = this.compileRegistry.getArtifact( - "SettlementSmartContract" - ); - const dispatchVerificationKey = this.compileRegistry.getArtifact( - "DispatchSmartContract" + private async getArtifacts>( + contracts: Contracts, + retry: boolean + ): Promise> { + const artifacts = Object.entries(contracts).map( + ([contract]) => + [contract, this.compileRegistry.getArtifact(contract)] as const ); - if ( - settlementVerificationKey === undefined || - dispatchVerificationKey === undefined - ) { + if (artifacts.some((x) => x[1] === undefined)) { if (retry) { log.info( "Settlement Contracts not yet compiled, initializing compilation" ); await this.compile(); - return await this.getArtifacts(false); + return await this.getArtifacts(contracts, false); } throw new Error( "Settlement contract verification keys not available for deployment" ); } - return { - SettlementSmartContract: settlementVerificationKey, - DispatchSmartContract: dispatchVerificationKey, - }; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return Object.fromEntries(artifacts) as Record< + keyof Contracts, + CompileArtifact + >; } - public async retrieveVerificationKeys() { - return await this.getArtifacts(true); + public async retrieveVerificationKeys>( + contracts: Contracts + ) { + return await this.getArtifacts(contracts, true); } } diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index 90bf94282..a575c5e3a 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -3,7 +3,6 @@ import { BridgeContractConfig, BridgeContractType, MandatoryProtocolModulesRecord, - MandatorySettlementModulesRecord, OUTGOING_MESSAGE_BATCH_SIZE, OutgoingMessageArgument, OutgoingMessageArgumentBatch, @@ -18,6 +17,11 @@ import { PROTOKIT_FIELD_PREFIXES, OutgoingMessageEvent, BridgeContractContext, + BridgingSettlementModulesRecord, + DispatchContractType, + BridgingSettlementContractType, + ContractArgsRegistry, + BridgingSettlementContractArgs, } from "@proto-kit/protocol"; import { AccountUpdate, @@ -25,12 +29,14 @@ import { Mina, Provable, PublicKey, + SmartContract, TokenContract, TokenId, Transaction, UInt32, } from "o1js"; import { + DependencyRecord, filterNonUndefined, LinkedMerkleTree, log, @@ -41,12 +47,15 @@ import { match, Pattern } from "ts-pattern"; import { FungibleToken } from "mina-fungible-token"; // eslint-disable-next-line import/no-extraneous-dependencies import groupBy from "lodash/groupBy"; +// eslint-disable-next-line import/no-extraneous-dependencies +import truncate from "lodash/truncate"; import { FeeStrategy } from "../protocol/baselayer/fees/FeeStrategy"; import type { MinaBaseLayer } from "../protocol/baselayer/MinaBaseLayer"; import { AsyncLinkedLeafStore } from "../state/async/AsyncLinkedLeafStore"; import { CachedLinkedLeafStore } from "../state/lmt/CachedLinkedLeafStore"; import { SettleableBatch } from "../storage/model/Batch"; +import { SequencerModule } from "../sequencer/builder/SequencerModule"; import type { SettlementModule } from "./SettlementModule"; import { SettlementUtils } from "./utils/SettlementUtils"; @@ -54,6 +63,10 @@ import { MinaTransactionSender } from "./transactions/MinaTransactionSender"; import { OutgoingMessageCollector } from "./messages/outgoing/OutgoingMessageCollector"; import { ArchiveNode } from "./utils/ArchiveNode"; import { MinaSigner } from "./MinaSigner"; +import { SignedSettlementPermissions } from "./permissions/SignedSettlementPermissions"; +import { ProvenSettlementPermissions } from "./permissions/ProvenSettlementPermissions"; +import { AddressRegistry } from "./interactions/AddressRegistry"; +import { IncomingMessagesService } from "./messages/IncomingMessagesService"; export type SettlementTokenConfig = Record< string, @@ -67,6 +80,12 @@ export type SettlementTokenConfig = Record< } >; +export type BridgingModuleConfig = { + addresses?: { + DispatchContract: PublicKey; + }; +}; + /** * Module that facilitates all transaction creation and monitoring for * bridging related operations. @@ -74,18 +93,18 @@ export type SettlementTokenConfig = Record< * for those as needed */ @injectable() -export class BridgingModule { +export class BridgingModule extends SequencerModule { + // TODO Eventually, we don't want to store this here either, but build a smarter AddressRegistry private seenBridgeDeployments: { latestDeployment: number; - // tokenId => Bridge address - deployments: Record; } = { latestDeployment: -1, - deployments: {}, }; private utils: SettlementUtils; + protected dispatchContract?: DispatchContractType & SmartContract; + public constructor( @inject("Protocol") private readonly protocol: Protocol, @@ -99,18 +118,52 @@ export class BridgingModule { @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, @inject("SettlementSigner") private readonly signer: MinaSigner, @inject("TransactionSender") - private readonly transactionSender: MinaTransactionSender + private readonly transactionSender: MinaTransactionSender, + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + private readonly argsRegistry: ContractArgsRegistry ) { + super(); + this.utils = new SettlementUtils(baseLayer, signer); } + public static dependencies() { + return { + IncomingMessagesService: { + useClass: IncomingMessagesService, + }, + } satisfies DependencyRecord; + } + + public getDispatchContract() { + if (this.dispatchContract === undefined) { + const address = this.getDispatchContractAddress(); + this.dispatchContract = this.settlementContractModule().createContract( + "DispatchContract", + address + ); + } + return this.dispatchContract; + } + + public getDispatchContractAddress(): PublicKey { + const keys = + this.addressRegistry.getContractAddress("DispatchContract") ?? + this.config.addresses?.DispatchContract; + if (keys === undefined) { + throw new Error("Contracts not initialized yet"); + } + return keys; + } + private getMessageProcessors() { return this.protocol.dependencyContainer.resolveAll< OutgoingMessageProcessor >("OutgoingMessageProcessor"); } - protected settlementContractModule(): SettlementContractModule { + protected settlementContractModule(): SettlementContractModule { return this.protocol.dependencyContainer.resolve( "SettlementContractModule" ); @@ -127,10 +180,11 @@ export class BridgingModule { return config; } + // TODO Use AddressRegistry for bridge addresses public async updateBridgeAddresses() { const events = await this.settlementModule - .getContracts() - .settlement.fetchEvents( + .getContract() + .fetchEvents( UInt32.from(this.seenBridgeDeployments.latestDeployment + 1) ); const tuples = events @@ -138,41 +192,112 @@ export class BridgingModule { .map((event) => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const mapping = event.event.data as unknown as TokenMapping; - return [mapping.tokenId.toString(), mapping.publicKey]; + return [mapping.tokenId.toBigInt(), mapping.publicKey] as const; }); - const mergedDeployments = { - ...this.seenBridgeDeployments.deployments, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ...(Object.fromEntries(tuples) as Record), - }; + + tuples.forEach(([tokenId, publicKey]) => { + this.addressRegistry.addContractAddress( + this.addressRegistry.getIdentifier("BridgeContract", tokenId), + publicKey + ); + }); + const latestDeployment = events .map((event) => Number(event.blockHeight.toString())) .reduce((a, b) => (a > b ? a : b), 0); this.seenBridgeDeployments = { - deployments: mergedDeployments, latestDeployment, }; } + public async deployMinaBridge( + contractKey: PublicKey, + options: { + nonce?: number; + } + ) { + return await this.deployTokenBridge(undefined, contractKey, options); + } + + /** + * Deploys a token bridge (BridgeContract) and authorizes it on the DispatchContract + * + * Invariant: The owner has to be specified, unless the bridge is for the mina token + * + * @param owner reference to the token owner contract (used to approve the deployment AUs) + * @param contractKey PublicKey to which the new bridge contract should be deployed to + * @param options + */ + public async deployTokenBridge( + owner: TokenContract | undefined, + contractKey: PublicKey, + options: { + nonce?: number; + } + ) { + const feepayer = this.signer.getFeepayerKey(); + const nonce = options?.nonce ?? undefined; + + const tokenId = owner?.deriveTokenId() ?? TokenId.default; + const settlementContract = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + this.settlementModule.getContract() as BridgingSettlementContractType & + SmartContract; + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce: nonce, + memo: `Deploy token bridge for ${truncate(tokenId.toString(), { length: 6 })}`, + fee: this.feeStrategy.getFee(), + }, + async () => { + AccountUpdate.fundNewAccount(feepayer, 1); + + await settlementContract.addTokenBridge(tokenId, contractKey); + + if (owner !== undefined) { + await owner.approveAccountUpdate(settlementContract.self); + } + } + ); + + // Only ContractKeys and OwnerKey for check. + // Used all in signing process. + const txSigned = this.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + ...this.signer.getContractAddresses(), + ...(owner ? [owner.address] : []), + ], + signingPublicKeys: [contractKey], + }); + + await this.transactionSender.proveAndSendTransaction(txSigned, "included"); + } + public async getBridgeAddress( tokenId: Field ): Promise { - const { deployments } = this.seenBridgeDeployments; + const identifier = this.addressRegistry.getIdentifier( + "BridgeContract", + tokenId.toBigInt() + ); + const deployment = this.addressRegistry.getContractAddress(identifier); - if (Object.keys(deployments).includes(tokenId.toString())) { - return deployments[tokenId.toString()]; + if (deployment !== undefined) { + return deployment; } await this.updateBridgeAddresses(); - return this.seenBridgeDeployments.deployments[tokenId.toString()]; + return this.addressRegistry.getContractAddress(identifier); } public async getDepositContractAttestation(tokenId: Field) { await ArchiveNode.waitOnSync(this.baseLayer.config); - const { dispatch } = this.settlementModule.getContracts(); + const DispatchContract = this.getDispatchContract(); - const tree = await TokenBridgeTree.buildTreeFromEvents(dispatch); + const tree = await TokenBridgeTree.buildTreeFromEvents(DispatchContract); const index = tree.getIndex(tokenId); return new TokenBridgeAttestation({ index: Field(index), @@ -311,7 +436,8 @@ export class BridgingModule { public createBridgeContract(contractAddress: PublicKey, tokenId: Field) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.settlementContractModule().createBridgeContract( + return this.settlementContractModule().createContract( + "BridgeContract", contractAddress, tokenId ) as BridgeContractType & TokenContract; @@ -356,7 +482,7 @@ export class BridgingModule { | { nonceUsed: false } | { nonceUsed: true; tx: Mina.Transaction } > { - const settlementContract = this.settlementModule.getContracts().settlement; + const settlementContract = this.settlementModule.getSettlementContract(); const bridge = await this.getBridgeContract(tokenId); log.debug( @@ -546,5 +672,28 @@ export class BridgingModule { return txs; } + + public async start(): Promise { + this.argsRegistry.addArgs( + "SettlementContract", + { + // TODO Add distinction between mina and custom tokens + BridgeContractPermissions: (this.baseLayer.isSignedSettlement() + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions() + ).bridgeContractMina(), + } + ); + + const dispatchAddress = this.config.addresses?.DispatchContract; + if (dispatchAddress !== undefined) { + this.addressRegistry.addContractAddress( + "DispatchContract", + dispatchAddress + ); + } + } /* eslint-enable no-await-in-loop */ } + +// BridgingModule satisfies DependencyFactory; diff --git a/packages/sequencer/src/settlement/SettlementModule.ts b/packages/sequencer/src/settlement/SettlementModule.ts index 597da53b3..4ab3439d3 100644 --- a/packages/sequencer/src/settlement/SettlementModule.ts +++ b/packages/sequencer/src/settlement/SettlementModule.ts @@ -1,32 +1,22 @@ import { Protocol, SettlementContractModule, - BATCH_SIGNATURE_PREFIX, - DispatchSmartContract, - SettlementSmartContract, MandatorySettlementModulesRecord, MandatoryProtocolModulesRecord, - SettlementSmartContractBase, - DynamicBlockProof, + type SettlementContractType, + ContractArgsRegistry, + SettlementContractArgs, } from "@proto-kit/protocol"; -import { - AccountUpdate, - fetchAccount, - Field, - Mina, - PublicKey, - TokenContract, - TokenId, -} from "o1js"; +import { fetchAccount, Field, Mina, PublicKey, SmartContract } from "o1js"; import { inject } from "tsyringe"; import { EventEmitter, EventEmittingComponent, - log, DependencyFactory, + ModuleContainerLike, + DependencyRecord, + log, } from "@proto-kit/common"; -// eslint-disable-next-line import/no-extraneous-dependencies -import truncate from "lodash/truncate"; import { SequencerModule, @@ -34,18 +24,26 @@ import { } from "../sequencer/builder/SequencerModule"; import type { MinaBaseLayer } from "../protocol/baselayer/MinaBaseLayer"; import { Batch, SettleableBatch } from "../storage/model/Batch"; -import { BlockProofSerializer } from "../protocol/production/tasks/serializers/BlockProofSerializer"; import { Settlement } from "../storage/model/Settlement"; -import { FeeStrategy } from "../protocol/baselayer/fees/FeeStrategy"; -import { SettlementStartupModule } from "../sequencer/SettlementStartupModule"; import { SettlementStorage } from "../storage/repositories/SettlementStorage"; -import { MinaTransactionSender } from "./transactions/MinaTransactionSender"; -import { ProvenSettlementPermissions } from "./permissions/ProvenSettlementPermissions"; -import { SignedSettlementPermissions } from "./permissions/SignedSettlementPermissions"; import { SettlementUtils } from "./utils/SettlementUtils"; -import { BridgingModule } from "./BridgingModule"; +import type { BridgingModule } from "./BridgingModule"; import { MinaSigner } from "./MinaSigner"; +import { BridgingDeployInteraction } from "./interactions/bridging/BridgingDeployInteraction"; +import { VanillaDeployInteraction } from "./interactions/vanilla/VanillaDeployInteraction"; +import { BridgingSettlementInteraction } from "./interactions/bridging/BridgingSettlementInteraction"; +import { VanillaSettlementInteraction } from "./interactions/vanilla/VanillaSettlementInteraction"; +import { + AddressRegistry, + InMemoryAddressRegistry, +} from "./interactions/AddressRegistry"; + +export type SettlementModuleConfig = { + addresses?: { + SettlementContract: PublicKey; + }; +}; export type SettlementModuleEvents = { "settlement-submitted": [Batch]; @@ -53,13 +51,10 @@ export type SettlementModuleEvents = { @sequencerModule() export class SettlementModule - extends SequencerModule - implements EventEmittingComponent, DependencyFactory + extends SequencerModule + implements EventEmittingComponent { - protected contracts?: { - settlement: SettlementSmartContract; - dispatch: DispatchSmartContract; - }; + protected contract?: SettlementContractType & SmartContract; public utils: SettlementUtils; @@ -71,69 +66,77 @@ export class SettlementModule private readonly protocol: Protocol, @inject("SettlementStorage") private readonly settlementStorage: SettlementStorage, - private readonly blockProofSerializer: BlockProofSerializer, - @inject("TransactionSender") - private readonly transactionSender: MinaTransactionSender, @inject("SettlementSigner") private readonly signer: MinaSigner, - @inject("FeeStrategy") - private readonly feeStrategy: FeeStrategy, - private readonly settlementStartupModule: SettlementStartupModule + @inject("Sequencer") + private readonly parentContainer: ModuleContainerLike, + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + private readonly argsRegistry: ContractArgsRegistry ) { super(); this.utils = new SettlementUtils(this.baseLayer, this.signer); } - public dependencies() { + public static dependencies(): DependencyRecord { return { - BridgingModule: { - useClass: BridgingModule, + AddressRegistry: { + useClass: InMemoryAddressRegistry, }, }; } + private bridgingModule(): BridgingModule | undefined { + const container = this.parentContainer.dependencyContainer; + if (container.isRegistered("BridgingModule")) { + return container.resolve("BridgingModule"); + } + return undefined; + } + protected settlementContractModule(): SettlementContractModule { return this.protocol.dependencyContainer.resolve( "SettlementContractModule" ); } - public getAddresses() { - const keysArray = this.signer.getContractAddresses(); - return { - settlement: keysArray[0], - dispatch: keysArray[1], - }; + public getSettlementContractAddress(): PublicKey { + const keys = + this.addressRegistry.getContractAddress("SettlementContract") ?? + this.config.addresses?.SettlementContract; + + if (keys === undefined) { + throw new Error("Contracts not initialized yet"); + } + return keys; } - public getContractAddresses() { - return this.signer.getContractAddresses(); + public getSettlementContract() { + if (this.contract === undefined) { + const address = this.getSettlementContractAddress(); + this.contract = this.settlementContractModule().createContract( + "SettlementContract", + address + ); + } + + return this.contract; } - public getContracts() { - if (this.contracts === undefined) { - const addresses = this.getAddresses(); + public getContract() { + if (this.contract === undefined) { + const address = this.getSettlementContractAddress(); const { protocol } = this; const settlementContractModule = protocol.dependencyContainer.resolve< SettlementContractModule >("SettlementContractModule"); - // TODO Add generic inference of concrete Contract types - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - this.contracts = settlementContractModule.createContracts(addresses) as { - settlement: SettlementSmartContract; - dispatch: DispatchSmartContract; - }; + this.contract = settlementContractModule.createContract( + "SettlementContract", + address + ); } - return this.contracts; - } - - private async fetchContractAccounts() { - const contracts = this.getContracts(); - await this.utils.fetchContractAccounts( - contracts.settlement, - contracts.dispatch - ); + return this.contract; } public async settleBatch( @@ -142,60 +145,18 @@ export class SettlementModule nonce?: number; } = {} ): Promise { - await this.fetchContractAccounts(); - const { settlement: settlementContract, dispatch } = this.getContracts(); - const feepayer = this.signer.getFeepayerKey(); log.debug("Preparing settlement"); - const lastSettlementL1BlockHeight = - settlementContract.lastSettlementL1BlockHeight.get().value; - const signature = this.signer.sign([ - BATCH_SIGNATURE_PREFIX, - lastSettlementL1BlockHeight, - ]); - - const latestSequenceStateHash = dispatch.account.actionState.get(); - - const blockProof = await this.blockProofSerializer - .getBlockProofSerializer() - .fromJSONProof(batch.proof); - - const dynamicBlockProof = DynamicBlockProof.fromProof(blockProof); - - const tx = await Mina.transaction( - { - sender: feepayer, - nonce: options?.nonce, - fee: this.feeStrategy.getFee(), - memo: "Protokit settle", - }, - async () => { - await settlementContract.settle( - dynamicBlockProof, - signature, - dispatch.address, - feepayer, - batch.fromNetworkState, - batch.toNetworkState, - latestSequenceStateHash - ); - } - ); - - this.utils.signTransaction(tx, { - signingWithSignatureCheck: [...this.signer.getContractAddresses()], - }); - - const { hash: transactionHash } = - await this.transactionSender.proveAndSendTransaction(tx, "included"); - - log.info("Settlement transaction sent and included"); - - const settlement = { - batches: [batch.height], - promisedMessagesHash: latestSequenceStateHash.toString(), - transactionHash, - }; + const bridgingModule = this.bridgingModule(); + const interaction = + bridgingModule !== undefined + ? this.parentContainer.dependencyContainer.resolve( + BridgingSettlementInteraction + ) + : this.parentContainer.dependencyContainer.resolve( + VanillaSettlementInteraction + ); + const settlement = await interaction.settle(batch, options); await this.settlementStorage.pushSettlement(settlement); @@ -205,166 +166,61 @@ export class SettlementModule } // Can't do anything for now - initialize() method use settlementKey. + // TODO Rethink that interface - deploy with addresses as args would be pretty nice public async deploy( - settlementKey: PublicKey, - dispatchKey: PublicKey, - minaBridgeKey: PublicKey, + addresses: { + settlementContract: PublicKey; + dispatchContract?: PublicKey; + }, options: { nonce?: number; } = {} ) { - const feepayer = this.signer.getFeepayerKey(); - - const nonce = options?.nonce ?? 0; - - const sm = this.protocol.dependencyContainer.resolve< - SettlementContractModule - >("SettlementContractModule"); - const { settlement, dispatch } = sm.createContracts({ - settlement: settlementKey, - dispatch: dispatchKey, - }); - - const verificationsKeys = - await this.settlementStartupModule.retrieveVerificationKeys(); - - const permissions = this.baseLayer.isSignedSettlement() - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions(); - - const tx = await Mina.transaction( - { - sender: feepayer, - nonce, - fee: this.feeStrategy.getFee(), - memo: "Protokit settlement deploy", - }, - async () => { - AccountUpdate.fundNewAccount(feepayer, 2); - - await dispatch.deployAndInitialize( - { - verificationKey: - verificationsKeys.DispatchSmartContract.verificationKey, - }, - permissions.dispatchContract(), - settlement.address - ); - - await settlement.deployAndInitialize( - { - verificationKey: - verificationsKeys.SettlementSmartContract.verificationKey, - }, - permissions.settlementContract(), - feepayer, - dispatchKey - ); - } - ); - - this.utils.signTransaction(tx, { - signingWithSignatureCheck: [...this.signer.getContractAddresses()], - }); - // Note: We can't use this.signTransaction on the above tx - - // This should already apply the tx result to the - // cached accounts / local blockchain - await this.transactionSender.proveAndSendTransaction(tx, "included"); - - await this.utils.fetchContractAccounts(settlement, dispatch); - - const initTx = await Mina.transaction( - { - sender: feepayer, - nonce: nonce + 1, - fee: this.feeStrategy.getFee(), - memo: "Deploy MINA bridge", - }, - async () => { - AccountUpdate.fundNewAccount(feepayer, 1); - // Deploy bridge contract for $Mina - await settlement.addTokenBridge( - TokenId.default, - minaBridgeKey, - dispatchKey - ); - } - ); - - const initTxSigned = this.utils.signTransaction(initTx, { - signingWithSignatureCheck: [ - ...this.signer.getContractAddresses(), - minaBridgeKey, - ], - }); - - await this.transactionSender.proveAndSendTransaction( - initTxSigned, - "included" - ); + const bridgingModule = this.bridgingModule(); + // TODO Add overwriting in dependency factories and then resolve based on that here + const interaction = + bridgingModule !== undefined + ? this.parentContainer.dependencyContainer.resolve( + BridgingDeployInteraction + ) + : this.parentContainer.dependencyContainer.resolve( + VanillaDeployInteraction + ); + + await interaction.deploy(addresses, options); } - public async deployTokenBridge( - owner: TokenContract, - ownerPublicKey: PublicKey, - contractKey: PublicKey, - options: { - nonce?: number; - } - ) { - const feepayer = this.signer.getFeepayerKey(); - const nonce = options?.nonce ?? undefined; - - const tokenId = owner.deriveTokenId(); - const { settlement, dispatch } = this.getContracts(); - - const tx = await Mina.transaction( - { - sender: feepayer, - nonce: nonce, - memo: `Deploy token bridge for ${truncate(tokenId.toString(), { length: 6 })}`, - fee: this.feeStrategy.getFee(), - }, - async () => { - AccountUpdate.fundNewAccount(feepayer, 1); - await settlement.addTokenBridge(tokenId, contractKey, dispatch.address); - await owner.approveAccountUpdate(settlement.self); - } - ); - - // Only ContractKeys and OwnerKey for check. - // Used all in signing process. - const txSigned = this.utils.signTransaction(tx, { - signingWithSignatureCheck: [ - ...this.signer.getContractAddresses(), - ownerPublicKey, - ], - signingPublicKeys: [contractKey], + public async start(): Promise { + this.argsRegistry.addArgs("SettlementContract", { + signedSettlements: this.baseLayer.isSignedSettlement(), }); - await this.transactionSender.proveAndSendTransaction(txSigned, "included"); - } - - public async start(): Promise { - const contractArgs = SettlementSmartContractBase.args; + const settlementContractAddress = this.config.addresses?.SettlementContract; + if (settlementContractAddress !== undefined) { + this.addressRegistry.addContractAddress( + "SettlementContract", + settlementContractAddress + ); - SettlementSmartContractBase.args = { - ...contractArgs, - signedSettlements: this.baseLayer.isSignedSettlement(), - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (this.baseLayer.isSignedSettlement() - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; + await this.checkDeployment(); + } } public async checkDeployment( tokenBridges?: Array<{ address: PublicKey; tokenId: Field }> ): Promise { + const addresses = [ + this.addressRegistry.getContractAddress("SettlementContract")!, + ]; + + const bridgeContractAddress = + this.bridgingModule()?.config.addresses?.DispatchContract; + if (bridgeContractAddress !== undefined) { + addresses.push(bridgeContractAddress); + } + const contracts: Array<{ address: PublicKey; tokenId?: Field }> = [ - ...this.getContractAddresses().map((addr) => ({ address: addr })), + ...addresses.map((addr) => ({ address: addr })), ...(tokenBridges ?? []), ]; @@ -405,3 +261,5 @@ export class SettlementModule } } } + +SettlementModule satisfies DependencyFactory; diff --git a/packages/sequencer/src/settlement/interactions/AddressRegistry.ts b/packages/sequencer/src/settlement/interactions/AddressRegistry.ts new file mode 100644 index 000000000..449f345cd --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/AddressRegistry.ts @@ -0,0 +1,31 @@ +import { PublicKey } from "o1js"; + +export interface AddressRegistry { + getIdentifier(name: string, tokenId?: bigint): string; + + getContractAddress(identifier: string): PublicKey | undefined; + + addContractAddress(identifier: string, address: PublicKey): void; + + hasContractAddress(identifier: string): boolean; +} + +export class InMemoryAddressRegistry implements AddressRegistry { + addresses: Record = {}; + + getIdentifier(name: string, tokenId?: bigint) { + return `${name}-${tokenId}`; + } + + addContractAddress(identifier: string, address: PublicKey): void { + this.addresses[identifier] = address; + } + + getContractAddress(identifier: string): PublicKey { + return this.addresses[identifier]; + } + + hasContractAddress(identifier: string): boolean { + return this.addresses[identifier] !== undefined; + } +} diff --git a/packages/sequencer/src/settlement/interactions/DeployInteraction.ts b/packages/sequencer/src/settlement/interactions/DeployInteraction.ts new file mode 100644 index 000000000..e2501d866 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/DeployInteraction.ts @@ -0,0 +1,11 @@ +import { PublicKey } from "o1js"; + +export interface DeployInteraction { + deploy( + addresses: { + settlementContract: PublicKey; + dispatchContract: PublicKey; + }, + options?: { nonce?: number } + ): void; +} diff --git a/packages/sequencer/src/settlement/interactions/SettleInteraction.ts b/packages/sequencer/src/settlement/interactions/SettleInteraction.ts new file mode 100644 index 000000000..3291df213 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/SettleInteraction.ts @@ -0,0 +1,11 @@ +import { SettleableBatch } from "../../storage/model/Batch"; +import { Settlement } from "../../storage/model/Settlement"; + +export interface SettleInteraction { + settle( + batch: SettleableBatch, + options: { + nonce?: number; + } + ): Promise; +} diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts new file mode 100644 index 000000000..c1c24d4a4 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts @@ -0,0 +1,129 @@ +import { inject, injectable } from "tsyringe"; +import { AccountUpdate, Mina, PublicKey } from "o1js"; +import { + BridgingSettlementModulesRecord, + MandatoryProtocolModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { O1PublicKeyOption } from "@proto-kit/common"; + +import { DeployInteraction } from "../DeployInteraction"; +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { SettlementStartupModule } from "../../../sequencer/SettlementStartupModule"; +import { SignedSettlementPermissions } from "../../permissions/SignedSettlementPermissions"; +import { ProvenSettlementPermissions } from "../../permissions/ProvenSettlementPermissions"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; + +@injectable() +export class BridgingDeployInteraction implements DeployInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + private readonly settlementStartupModule: SettlementStartupModule, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async deploy( + { + dispatchContract: dispatchKey, + settlementContract: settlementKey, + }: { + settlementContract: PublicKey; + dispatchContract?: PublicKey; + }, + options: { + nonce?: number; + } = {} + ) { + if (dispatchKey === undefined) { + throw new Error("DispatchContract address not provided"); + } + + const feepayer = this.signer.getFeepayerKey(); + + const nonce = options?.nonce ?? 0; + + const sm = this.settlementContractModule(); + const { + SettlementContract: settlementContract, + DispatchContract: dispatchContract, + } = sm.createContracts({ + SettlementContract: settlementKey, + DispatchContract: dispatchKey, + }); + + const verificationsKeys = + await this.settlementStartupModule.retrieveVerificationKeys({ + SettlementContract: true, + DispatchSmartContract: true, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract, dispatchContract); + + const permissions = this.baseLayer.isSignedSettlement() + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions(); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settlement deploy", + }, + async () => { + AccountUpdate.fundNewAccount(feepayer, 2); + + await dispatchContract.deployAndInitialize( + { + verificationKey: + verificationsKeys.DispatchSmartContract.verificationKey, + }, + permissions.dispatchContract(), + settlementContract.address + ); + + await settlementContract.deployAndInitialize( + { + verificationKey: + verificationsKeys.SettlementContract.verificationKey, + }, + permissions.settlementContract(), + feepayer, + O1PublicKeyOption.from(dispatchKey) + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: [...this.signer.getContractAddresses()], + }); + + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + this.addressRegistry.addContractAddress( + "SettlementContract", + settlementKey + ); + this.addressRegistry.addContractAddress("DispatchContract", dispatchKey); + } +} diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts new file mode 100644 index 000000000..ee8b74983 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts @@ -0,0 +1,126 @@ +import { inject, injectable } from "tsyringe"; +import { Mina } from "o1js"; +import { + BATCH_SIGNATURE_PREFIX, + BridgingSettlementModulesRecord, + DynamicBlockProof, + MandatoryProtocolModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { log } from "@proto-kit/common"; + +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; +import { BlockProofSerializer } from "../../../protocol/production/tasks/serializers/BlockProofSerializer"; +import { SettleableBatch } from "../../../storage/model/Batch"; +import { Settlement } from "../../../storage/model/Settlement"; +import { SettleInteraction } from "../SettleInteraction"; + +@injectable() +export class BridgingSettlementInteraction implements SettleInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender, + private readonly blockProofSerializer: BlockProofSerializer + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async settle( + batch: SettleableBatch, + options: { + nonce?: number; + } = {} + ): Promise { + const feepayer = this.signer.getFeepayerKey(); + + const settlementKey = + this.addressRegistry.getContractAddress("SettlementContract"); + const dispatchKey = + this.addressRegistry.getContractAddress("DispatchContract"); + + if (settlementKey === undefined || dispatchKey === undefined) { + throw new Error( + "Settlement and/or DispatchContract addresses haven't been initialized" + ); + } + + const sm = this.settlementContractModule(); + const { + SettlementContract: settlementContract, + DispatchContract: dispatchContract, + } = sm.createContracts({ + SettlementContract: settlementKey, + DispatchContract: dispatchKey, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract, dispatchContract); + + const lastSettlementL1BlockHeight = + settlementContract.lastSettlementL1BlockHeight.get().value; + const signature = this.signer.sign([ + BATCH_SIGNATURE_PREFIX, + lastSettlementL1BlockHeight, + ]); + + const latestSequenceStateHash = dispatchContract.account.actionState.get(); + + const blockProof = await this.blockProofSerializer + .getBlockProofSerializer() + .fromJSONProof(batch.proof); + + const dynamicBlockProof = DynamicBlockProof.fromProof(blockProof); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce: options?.nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settle", + }, + async () => { + await settlementContract.settle( + dynamicBlockProof, + signature, + feepayer, + batch.fromNetworkState, + batch.toNetworkState, + latestSequenceStateHash + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: this.signer.getContractAddresses(), + }); + + const { hash: transactionHash } = + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + log.info("Settlement transaction sent and included"); + + return { + batches: [batch.height], + promisedMessagesHash: latestSequenceStateHash.toString(), + transactionHash, + }; + } +} diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts new file mode 100644 index 000000000..c5fb126fe --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts @@ -0,0 +1,117 @@ +import { inject, injectable } from "tsyringe"; +import { AccountUpdate, Mina, PublicKey } from "o1js"; +import { + MandatoryProtocolModulesRecord, + MandatorySettlementModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { log, O1PublicKeyOption } from "@proto-kit/common"; + +import { DeployInteraction } from "../DeployInteraction"; +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { SettlementStartupModule } from "../../../sequencer/SettlementStartupModule"; +import { SignedSettlementPermissions } from "../../permissions/SignedSettlementPermissions"; +import { ProvenSettlementPermissions } from "../../permissions/ProvenSettlementPermissions"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; + +@injectable() +export class VanillaDeployInteraction implements DeployInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + private readonly settlementStartupModule: SettlementStartupModule, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async deploy( + { + settlementContract: settlementKey, + dispatchContract: dispatchKey, + }: { + settlementContract: PublicKey; + dispatchContract?: PublicKey; + }, + options: { + nonce?: number; + } = {} + ) { + if (dispatchKey !== undefined) { + log.error( + "DispatchContract address provided for deploy(), however the module configuration hints at a " + + "settlement-only deployment, therefore the DispatchContract will not be deployed" + ); + } + + const feepayer = this.signer.getFeepayerKey(); + + const nonce = options?.nonce ?? 0; + + const sm = this.settlementContractModule(); + const { SettlementContract: settlementContract } = sm.createContracts({ + SettlementContract: settlementKey, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract); + + const verificationsKeys = + await this.settlementStartupModule.retrieveVerificationKeys({ + SettlementContract: true, + }); + + const permissions = this.baseLayer.isSignedSettlement() + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions(); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settlement deploy", + }, + async () => { + AccountUpdate.fundNewAccount(feepayer, 1); + + await settlementContract.deployAndInitialize( + { + verificationKey: + verificationsKeys.SettlementContract.verificationKey, + }, + permissions.settlementContract(), + feepayer, + O1PublicKeyOption.none() + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: [...this.signer.getContractAddresses()], + }); + + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + this.addressRegistry.addContractAddress( + "SettlementContract", + settlementKey + ); + } +} diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts new file mode 100644 index 000000000..096438ab5 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts @@ -0,0 +1,118 @@ +import { inject, injectable } from "tsyringe"; +import { Field, Mina } from "o1js"; +import { + BATCH_SIGNATURE_PREFIX, + BridgingSettlementModulesRecord, + DynamicBlockProof, + MandatoryProtocolModulesRecord, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { log } from "@proto-kit/common"; + +import { AddressRegistry } from "../AddressRegistry"; +import { MinaSigner } from "../../MinaSigner"; +import { MinaBaseLayer } from "../../../protocol/baselayer/MinaBaseLayer"; +import { FeeStrategy } from "../../../protocol/baselayer/fees/FeeStrategy"; +import { MinaTransactionSender } from "../../transactions/MinaTransactionSender"; +import { SettlementUtils } from "../../utils/SettlementUtils"; +import { BlockProofSerializer } from "../../../protocol/production/tasks/serializers/BlockProofSerializer"; +import { SettleableBatch } from "../../../storage/model/Batch"; +import { Settlement } from "../../../storage/model/Settlement"; +import { SettleInteraction } from "../SettleInteraction"; + +@injectable() +export class VanillaSettlementInteraction implements SettleInteraction { + public constructor( + @inject("AddressRegistry") + private readonly addressRegistry: AddressRegistry, + @inject("SettlementSigner") private readonly signer: MinaSigner, + @inject("BaseLayer") private readonly baseLayer: MinaBaseLayer, + @inject("Protocol") + private readonly protocol: Protocol, + @inject("FeeStrategy") + private readonly feeStrategy: FeeStrategy, + @inject("TransactionSender") + private readonly transactionSender: MinaTransactionSender, + private readonly blockProofSerializer: BlockProofSerializer + ) {} + + protected settlementContractModule(): SettlementContractModule { + return this.protocol.dependencyContainer.resolve( + "SettlementContractModule" + ); + } + + public async settle( + batch: SettleableBatch, + options: { + nonce?: number; + } = {} + ): Promise { + const feepayer = this.signer.getFeepayerKey(); + + const settlementKey = + this.addressRegistry.getContractAddress("SettlementContract"); + + if (settlementKey === undefined) { + throw new Error("Settlement addresses haven't been initialized"); + } + + const sm = this.settlementContractModule(); + const { SettlementContract: settlementContract } = sm.createContracts({ + SettlementContract: settlementKey, + }); + + const utils = new SettlementUtils(this.baseLayer, this.signer); + await utils.fetchContractAccounts(settlementContract); + + const lastSettlementL1BlockHeight = + settlementContract.lastSettlementL1BlockHeight.get().value; + const signature = this.signer.sign([ + BATCH_SIGNATURE_PREFIX, + lastSettlementL1BlockHeight, + ]); + + const latestSequenceStateHash = Field(0); + + const blockProof = await this.blockProofSerializer + .getBlockProofSerializer() + .fromJSONProof(batch.proof); + + const dynamicBlockProof = DynamicBlockProof.fromProof(blockProof); + + const tx = await Mina.transaction( + { + sender: feepayer, + nonce: options?.nonce, + fee: this.feeStrategy.getFee(), + memo: "Protokit settle", + }, + async () => { + await settlementContract.settle( + dynamicBlockProof, + signature, + feepayer, + batch.fromNetworkState, + batch.toNetworkState, + latestSequenceStateHash + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: this.signer.getContractAddresses(), + }); + + const { hash: transactionHash } = + await this.transactionSender.proveAndSendTransaction(tx, "included"); + + log.info("Settlement transaction sent and included"); + + return { + batches: [batch.height], + promisedMessagesHash: latestSequenceStateHash.toString(), + transactionHash, + }; + } +} diff --git a/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts b/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts index 509500df3..6311cb30a 100644 --- a/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts +++ b/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts @@ -5,7 +5,7 @@ import { SettlementStorage } from "../../storage/repositories/SettlementStorage" import { MessageStorage } from "../../storage/repositories/MessageStorage"; import { BlockStorage } from "../../storage/repositories/BlockStorage"; import { PendingTransaction } from "../../mempool/PendingTransaction"; -import type { SettlementModule } from "../SettlementModule"; +import type { BridgingModule } from "../BridgingModule"; import { IncomingMessageAdapter } from "./IncomingMessageAdapter"; @@ -20,8 +20,8 @@ export class IncomingMessagesService { private readonly messagesAdapter: IncomingMessageAdapter, @inject("BlockStorage") private readonly blockStorage: BlockStorage, - @inject("SettlementModule") - private readonly settlementModule: SettlementModule + @inject("BridgingModule") + private readonly bridgingModule: BridgingModule ) {} private async fetchRemaining( @@ -29,7 +29,7 @@ export class IncomingMessagesService { toMessagesHash: string ) { const dispatchContractAddress = - this.settlementModule.getAddresses().dispatch; + this.bridgingModule.getDispatchContractAddress(); const fetched = await this.messagesAdapter.fetchPendingMessages( dispatchContractAddress, diff --git a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts index 3c53dfd44..30339d0fc 100644 --- a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts +++ b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts @@ -9,9 +9,10 @@ import { } from "@proto-kit/common"; import { inject, injectable } from "tsyringe"; import { + BridgingSettlementContractArgs, + ContractArgsRegistry, RuntimeVerificationKeyRootService, SettlementContractModule, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { VerificationKey } from "o1js"; @@ -48,7 +49,8 @@ export class WorkerRegistrationTask public constructor( @inject("Protocol") private readonly protocol: ModuleContainerLike, - private readonly compileRegistry: CompileRegistry + private readonly compileRegistry: CompileRegistry, + private readonly contractArgsRegistry: ContractArgsRegistry ) { super(); } @@ -78,7 +80,7 @@ export class WorkerRegistrationTask this.protocol.dependencyContainer .resolve< SettlementContractModule< - ReturnType + ReturnType > >("SettlementContractModule") .resolve("SettlementContract") @@ -86,21 +88,24 @@ export class WorkerRegistrationTask } if (input.bridgeContractVerificationKey !== undefined) { - SettlementSmartContractBase.args.BridgeContractVerificationKey = - input.bridgeContractVerificationKey; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { BridgeContractVerificationKey: input.bridgeContractVerificationKey } + ); } if (input.isSignedSettlement !== undefined) { - const contractArgs = SettlementSmartContractBase.args; - SettlementSmartContractBase.args = { - ...contractArgs, - signedSettlements: input.isSignedSettlement, - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (input.isSignedSettlement - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { + signedSettlements: input.isSignedSettlement, + // TODO Add distinction between mina and custom tokens + BridgeContractPermissions: (input.isSignedSettlement + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions() + ).bridgeContractMina(), + } + ); } this.compileRegistry.addArtifactsRaw(input.compiledArtifacts); diff --git a/packages/sequencer/test/integration/Proven.test.ts b/packages/sequencer/test/integration/Proven.test.ts index 6de1a5517..0526d93d2 100644 --- a/packages/sequencer/test/integration/Proven.test.ts +++ b/packages/sequencer/test/integration/Proven.test.ts @@ -9,11 +9,12 @@ import { import { Runtime } from "@proto-kit/module"; import { BridgeContract, + BridgingSettlementContract, + BridgingSettlementContractArgs, + ContractArgsRegistry, DispatchSmartContract, Protocol, SettlementContractModule, - SettlementSmartContract, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { VanillaProtocolModules } from "@proto-kit/library"; import { container } from "tsyringe"; @@ -66,7 +67,8 @@ describe.skip("Proven", () => { ProtocolStateTestHook, // ProtocolStateTestHook2, }), - SettlementContractModule: SettlementContractModule.with({ + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementAndBridging(), // FungibleToken: FungibleTokenContractModule, // FungibleTokenAdmin: FungibleTokenAdminContractModule, }), @@ -103,10 +105,7 @@ describe.skip("Proven", () => { type: "local", }, }, - SettlementModule: { - // TODO - feepayer: PrivateKey.random(), - }, + SettlementModule: {}, }, Runtime: { Balances: {}, @@ -152,12 +151,15 @@ describe.skip("Proven", () => { SettlementStartupModule ); - const vks = await module.retrieveVerificationKeys(); + const vks = await module.retrieveVerificationKeys({ + SettlementContract: true, + DispatchSmartContract: true, + }); console.log(vks); expect(vks.DispatchSmartContract).toBeDefined(); - expect(vks.SettlementSmartContract).toBeDefined(); + expect(vks.SettlementContract).toBeDefined(); }); it.skip("Hello", async () => { @@ -172,18 +174,21 @@ describe.skip("Proven", () => { }, }); 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(); + + container + .resolve(ContractArgsRegistry) + .addArgs("SettlementContract", { + DispatchContract: DispatchSmartContract, + ChildVerificationKeyService: vkService, + BridgeContractVerificationKey: MOCK_VERIFICATION_KEY, + signedSettlements: false, + BridgeContract: BridgeContract, + hooks: [], + BridgeContractPermissions: + new ProvenSettlementPermissions().bridgeContractMina(), + escapeHatchSlotsInterval: 1000, + }); + const vk = await BridgingSettlementContract.compile(); console.log(vk.verificationKey); } catch (e) { console.error(e); diff --git a/packages/sequencer/test/settlement/Settlement-only.ts b/packages/sequencer/test/settlement/Settlement-only.ts new file mode 100644 index 000000000..6fe5d8dad --- /dev/null +++ b/packages/sequencer/test/settlement/Settlement-only.ts @@ -0,0 +1,306 @@ +import "reflect-metadata"; +import { Field, PrivateKey } from "o1js"; +import { Runtime } from "@proto-kit/module"; +import { + BlockStorageNetworkStateModule, + ClientAppChain, + InMemoryBlockExplorer, + InMemorySigner, + InMemoryTransactionSender, + StateServiceQueryModule, +} from "@proto-kit/sdk"; +import { + BlockProverPublicInput, + ContractArgsRegistry, + Protocol, + SettlementContractModule, +} from "@proto-kit/protocol"; +import { UInt64, VanillaProtocolModules } from "@proto-kit/library"; +import { + expectDefined, + LinkedMerkleTree, + mapSequential, +} from "@proto-kit/common"; +import { container } from "tsyringe"; +import { afterAll } from "@jest/globals"; + +import { createTransaction } from "../integration/utils"; +import { testingSequencerModules } from "../TestingSequencer"; +import { + BlockQueue, + InMemoryMinaSigner, + ManualBlockTrigger, + MinaBaseLayer, + MinaBaseLayerConfig, + MinaNetworkUtils, + PendingTransaction, + PrivateMempool, + Sequencer, + SettlementModule, + SettlementProvingTask, + VanillaTaskWorkerModules, +} from "../../src"; + +import { Withdrawals } from "./mocks/Withdrawals"; +import { Balances } from "./mocks/Balances"; + +// Most of this code is copied from test/Settlement.ts and thinned down to +// settlement-only - eventually we should consolidate and/or make the API nicer +// to require less code +export const settlementOnlyTestFn = ( + settlementType: "signed" | "mock-proofs" | "proven", + baseLayerConfig: MinaBaseLayerConfig, + timeout: number = 120_000 +) => { + let testAccounts: PrivateKey[] = []; + + const sequencerKey = PrivateKey.random(); + const settlementKey = PrivateKey.random(); + + let trigger: ManualBlockTrigger; + let settlementModule: SettlementModule; + let blockQueue: BlockQueue; + + function setupAppChain() { + const runtime = Runtime.from({ + Balances, + Withdrawals, + }); + + // eslint-disable-next-line @typescript-eslint/dot-notation + MinaBaseLayer.prototype["isSignedSettlement"] = () => + settlementType === "signed"; + + const sequencer = Sequencer.from( + testingSequencerModules( + { + BaseLayer: MinaBaseLayer, + SettlementModule: SettlementModule, + SettlementSigner: InMemoryMinaSigner, + }, + { + SettlementProvingTask, + } + ) + ); + + const appchain = ClientAppChain.from({ + Runtime: runtime, + Sequencer: sequencer, + + Protocol: Protocol.from({ + ...VanillaProtocolModules.mandatoryModules({}), + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementOnly(), + }), + }), + + Signer: InMemorySigner, + TransactionSender: InMemoryTransactionSender, + QueryTransportModule: StateServiceQueryModule, + NetworkStateTransportModule: BlockStorageNetworkStateModule, + BlockExplorerTransportModule: InMemoryBlockExplorer, + }); + + appchain.configure({ + Runtime: { + Balances: { + totalSupply: UInt64.from(1000), + }, + Withdrawals: {}, + }, + + Sequencer: { + Database: {}, + BlockTrigger: {}, + Mempool: {}, + BatchProducerModule: {}, + LocalTaskWorkerModule: { + ...VanillaTaskWorkerModules.defaultConfig(), + }, + BaseLayer: baseLayerConfig, + SettlementSigner: { + feepayer: sequencerKey, + contractKeys: [settlementKey], + }, + BlockProducerModule: {}, + FeeStrategy: {}, + SettlementModule: {}, + SequencerStartupModule: {}, + + TaskQueue: { + simulatedDuration: 0, + }, + }, + Protocol: { + StateTransitionProver: {}, + BlockHeight: {}, + AccountState: {}, + BlockProver: {}, + LastStateRoot: {}, + SettlementContractModule: { + SettlementContract: {}, + }, + }, + TransactionSender: {}, + QueryTransportModule: {}, + Signer: { + signer: sequencerKey, + }, + NetworkStateTransportModule: {}, + BlockExplorerTransportModule: {}, + }); + + return appchain; + } + + let appChain: ReturnType; + + async function createBatch( + withTransactions: boolean, + customNonce: number = 0, + txs: PendingTransaction[] = [] + ) { + const mempool = appChain.sequencer.resolve("Mempool") as PrivateMempool; + if (withTransactions) { + const key = testAccounts[0]; + const tx = createTransaction({ + runtime: appChain.runtime, + method: ["Balances", "mint"], + privateKey: key, + args: [Field(1), key.toPublicKey(), UInt64.from(1e9 * 100)], + nonce: customNonce, + }); + + await mempool.add(tx); + } + await mapSequential(txs, async (tx) => { + await mempool.add(tx); + }); + + const result = await trigger.produceBlockAndBatch(); + const [block] = result; + + console.log( + `block ${block?.height.toString()} ${block?.fromMessagesHash.toString()} -> ${block?.toMessagesHash.toString()}` + ); + + return result; + } + + beforeAll(async () => { + appChain = setupAppChain(); + + await appChain.start( + settlementType === "proven", + container.createChildContainer() + ); + + settlementModule = appChain.sequencer.resolve( + "SettlementModule" + ) as SettlementModule; + trigger = + appChain.sequencer.dependencyContainer.resolve( + "BlockTrigger" + ); + blockQueue = appChain.sequencer.resolve("BlockQueue") as BlockQueue; + + const networkUtils = + appChain.sequencer.dependencyContainer.resolve( + "NetworkUtils" + ); + const accs = await networkUtils.getFundedAccounts(3); + testAccounts = accs.slice(1); + + await networkUtils.waitForNetwork(); + + console.log( + `Funding ${sequencerKey.toPublicKey().toBase58()} from ${accs[0].toPublicKey().toBase58()}` + ); + + await networkUtils.faucet(sequencerKey.toPublicKey(), 20 * 1e9); + }, timeout * 3); + + afterAll(async () => { + container.resolve(ContractArgsRegistry).resetArgs("SettlementContract"); + + await appChain.close(); + }); + + let nonceCounter = 0; + + it("should throw error", async () => { + await expect(settlementModule.checkDeployment()).rejects.toThrow(); + }); + + it( + "should deploy settlement contracts", + async () => { + // Deploy contract + await settlementModule.deploy( + { + settlementContract: settlementKey.toPublicKey(), + }, + { + nonce: nonceCounter, + } + ); + + nonceCounter += 1; + + console.log("Deployed"); + }, + timeout + ); + + it( + "should settle", + async () => { + try { + const [, batch] = await createBatch(true); + + const input = BlockProverPublicInput.fromFields( + batch!.proof.publicInput.map((x) => Field(x)) + ); + expect(input.stateRoot.toString()).toStrictEqual( + LinkedMerkleTree.EMPTY_ROOT.toString() + ); + + const lastBlock = await blockQueue.getLatestBlockAndResult(); + + await trigger.settle(batch!, {}); + nonceCounter++; + + // TODO Check Smartcontract tx layout (call to dispatch with good preconditions, etc) + + console.log("Block settled"); + + await settlementModule.utils.fetchContractAccounts({ + address: settlementModule.getSettlementContractAddress(), + }); + const settlement = settlementModule.getSettlementContract(); + expectDefined(lastBlock); + expectDefined(lastBlock.result); + expect(settlement.networkStateHash.get().toString()).toStrictEqual( + lastBlock!.result.afterNetworkState.hash().toString() + ); + expect(settlement.stateRoot.get().toString()).toStrictEqual( + lastBlock!.result.stateRoot.toString() + ); + expect(settlement.blockHashRoot.get().toString()).toStrictEqual( + lastBlock!.result.blockHashRoot.toString() + ); + } catch (e) { + console.error(e); + throw e; + } + }, + timeout + ); + + it("should not throw error after settlement", async () => { + expect.assertions(1); + + await expect(settlementModule.checkDeployment()).resolves.toBeUndefined(); + }); +}; diff --git a/packages/sequencer/test/settlement/Settlement.test.ts b/packages/sequencer/test/settlement/Settlement.test.ts index fd7ee60c5..b1aa7044a 100644 --- a/packages/sequencer/test/settlement/Settlement.test.ts +++ b/packages/sequencer/test/settlement/Settlement.test.ts @@ -3,6 +3,7 @@ import { FungibleToken } from "mina-fungible-token"; import { MinaBaseLayerConfig } from "../../src"; import { settlementTestFn } from "./Settlement"; +import { settlementOnlyTestFn } from "./Settlement-only"; describe.each(["mock-proofs", "signed"] as const)( "Settlement contracts: local blockchain - %s", @@ -22,5 +23,9 @@ describe.each(["mock-proofs", "signed"] as const)( tokenOwner: FungibleToken, }); }); + + describe("Settlement only", () => { + settlementOnlyTestFn(type, network); + }); } ); diff --git a/packages/sequencer/test/settlement/Settlement.ts b/packages/sequencer/test/settlement/Settlement.ts index cfc97c045..f0012e9cc 100644 --- a/packages/sequencer/test/settlement/Settlement.ts +++ b/packages/sequencer/test/settlement/Settlement.ts @@ -9,11 +9,12 @@ import { Runtime } from "@proto-kit/module"; import { BlockProverPublicInput, BridgeContract, + ContractArgsRegistry, + DispatchSmartContract, NetworkState, Protocol, ReturnType, SettlementContractModule, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { ClientAppChain, @@ -129,6 +130,7 @@ export const settlementTestFn = ( { BaseLayer: MinaBaseLayer, SettlementModule: SettlementModule, + BridgingModule: BridgingModule, SettlementSigner: InMemoryMinaSigner, }, { @@ -143,7 +145,8 @@ export const settlementTestFn = ( Protocol: Protocol.from({ ...VanillaProtocolModules.mandatoryModules({}), - SettlementContractModule: SettlementContractModule.with({ + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementAndBridging(), FungibleToken: FungibleTokenContractModule, FungibleTokenAdmin: FungibleTokenAdminContractModule, }), @@ -170,7 +173,9 @@ export const settlementTestFn = ( BlockTrigger: {}, Mempool: {}, BatchProducerModule: {}, - LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + LocalTaskWorkerModule: { + ...VanillaTaskWorkerModules.defaultConfig(), + }, BaseLayer: baseLayerConfig, SettlementSigner: { feepayer: sequencerKey, @@ -184,6 +189,7 @@ export const settlementTestFn = ( BlockProducerModule: {}, FeeStrategy: {}, SettlementModule: {}, + BridgingModule: {}, SequencerStartupModule: {}, TaskQueue: { @@ -302,8 +308,7 @@ export const settlementTestFn = ( }, timeout * 3); afterAll(async () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - SettlementSmartContractBase.args = undefined as any; + container.resolve(ContractArgsRegistry).resetArgs("SettlementContract"); await appChain.close(); }); @@ -313,37 +318,55 @@ export const settlementTestFn = ( let acc0L2Nonce = 0; it("should throw error", async () => { - const deploymentPromise = + const additionalAddresses = tokenConfig === undefined - ? settlementModule.checkDeployment() - : settlementModule.checkDeployment([ + ? undefined + : [ { address: tokenBridgeKey.toPublicKey(), tokenId: tokenOwner!.deriveTokenId(), }, - ]); + ]; - await expect(deploymentPromise).rejects.toThrow(); + await expect( + settlementModule.checkDeployment(additionalAddresses) + ).rejects.toThrow(); }); it( - "should deploy", + "should deploy settlement contracts", async () => { // Deploy contract await settlementModule.deploy( - settlementKey.toPublicKey(), - dispatchKey.toPublicKey(), - minaBridgeKey.toPublicKey(), + { + dispatchContract: dispatchKey.toPublicKey(), + settlementContract: settlementKey.toPublicKey(), + }, { nonce: nonceCounter, } ); - nonceCounter += 2; + nonceCounter += 1; console.log("Deployed"); }, - timeout * 2 + timeout + ); + + it( + "should deploy mina bridge", + async () => { + // Deploy contract + await bridgingModule.deployMinaBridge(minaBridgeKey.toPublicKey(), { + nonce: nonceCounter, + }); + + nonceCounter += 1; + + console.log("Deployed mina bridge"); + }, + timeout ); if (tokenConfig !== undefined) { @@ -398,7 +421,8 @@ export const settlementTestFn = ( signingWithSignatureCheck: [ tokenOwnerPubKeys.tokenOwner, tokenOwnerPubKeys.admin, - ...settlementModule.getContractAddresses(), + settlementModule.getSettlementContractAddress(), + bridgingModule.getDispatchContractAddress(), ], }); @@ -457,9 +481,8 @@ export const settlementTestFn = ( it( "should deploy custom token bridge", async () => { - await settlementModule.deployTokenBridge( + await bridgingModule.deployTokenBridge( tokenOwner!, - tokenOwnerPubKeys.tokenOwner, tokenBridgeKey.toPublicKey(), { nonce: nonceCounter++, @@ -500,9 +523,9 @@ export const settlementTestFn = ( console.log("Block settled"); await settlementModule.utils.fetchContractAccounts({ - address: settlementModule.getAddresses().settlement, + address: settlementModule.getSettlementContractAddress(), }); - const { settlement } = settlementModule.getContracts(); + const settlement = settlementModule.getSettlementContract(); expectDefined(lastBlock); expectDefined(lastBlock.result); expect(settlement.networkStateHash.get().toString()).toStrictEqual( @@ -526,7 +549,9 @@ export const settlementTestFn = ( "should include deposit", async () => { try { - const { settlement, dispatch } = settlementModule.getContracts(); + const settlement = settlementModule.getSettlementContract(); + const dispatch = + bridgingModule.getDispatchContract() as DispatchSmartContract; const bridge = new BridgeContract( tokenBridgeKey.toPublicKey(), bridgedTokenId @@ -579,7 +604,7 @@ export const settlementTestFn = ( settlementModule.utils.signTransaction(tx, { signingWithSignatureCheck: [ tokenOwnerPubKeys.tokenOwner, - ...settlementModule.getContractAddresses(), + settlementModule.getSettlementContractAddress(), ], signingPublicKeys: [userPublicKey], preventNoncePreconditionFor: [dispatch.address], @@ -770,7 +795,7 @@ export const settlementTestFn = ( signingWithSignatureCheck: [ tokenBridgeKey.toPublicKey(), tokenOwnerPubKeys.tokenOwner, - ...settlementModule.getContractAddresses(), + settlementModule.getSettlementContractAddress(), ], signingPublicKeys: [userPublicKey], }); @@ -804,16 +829,18 @@ export const settlementTestFn = ( expect.assertions(1); // Obtain promise of deployment check - const deploymentCheckPromise = + const additionalAddresses = tokenConfig === undefined - ? settlementModule.checkDeployment() - : settlementModule.checkDeployment([ + ? undefined + : [ { address: tokenBridgeKey.toPublicKey(), tokenId: tokenOwner!.deriveTokenId(), }, - ]); + ]; - await expect(deploymentCheckPromise).resolves.toBeUndefined(); + await expect( + settlementModule.checkDeployment(additionalAddresses) + ).resolves.toBeUndefined(); }); };