From c6b65cbc39fa36bee18ab2be82b4dba8adfe6612 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Fri, 12 Dec 2025 21:46:28 +0100 Subject: [PATCH 01/12] First draft of reworking the protocol structure --- .../protocol/src/hooks/NoopSettlementHook.ts | 4 +- packages/protocol/src/index.ts | 7 +- .../settlement/SettlementContractModule.ts | 104 ++++---- .../settlement/contracts/BridgeContract.ts | 4 +- ...ts => BridgingSettlementContractModule.ts} | 41 ++- .../contracts/DispatchSmartContract.ts | 8 +- .../SettlementSmartContractModule.ts | 80 ++++++ .../BridgingSettlementContract.ts} | 249 ++++-------------- .../contracts/settlement/SettlementBase.ts | 217 +++++++++++++++ .../settlement/SettlementContract.ts | 55 ++++ .../modularity/ProvableSettlementHook.ts | 4 +- .../src/settlement/modularity/types.ts | 3 +- .../modules/NetworkStateSettlementModule.ts | 4 +- 13 files changed, 493 insertions(+), 287 deletions(-) rename packages/protocol/src/settlement/contracts/{SettlementContractProtocolModule.ts => BridgingSettlementContractModule.ts} (75%) create mode 100644 packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts rename packages/protocol/src/settlement/contracts/{SettlementSmartContract.ts => settlement/BridgingSettlementContract.ts} (52%) create mode 100644 packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts create mode 100644 packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts 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..171157b6c 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"; diff --git a/packages/protocol/src/settlement/SettlementContractModule.ts b/packages/protocol/src/settlement/SettlementContractModule.ts index 631842d22..88cb24327 100644 --- a/packages/protocol/src/settlement/SettlementContractModule.ts +++ b/packages/protocol/src/settlement/SettlementContractModule.ts @@ -16,17 +16,16 @@ 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 } from "./contracts/SettlementSmartContractModule"; export type SettlementModulesRecord = ModulesRecord< TypedClass> @@ -36,6 +35,12 @@ export type MandatorySettlementModulesRecord = { SettlementContract: TypedClass< ContractModule >; +}; + +export type BridgingSettlementModulesRecord = { + SettlementContract: TypedClass< + ContractModule + >; DispatchContract: TypedClass>; BridgeContract: TypedClass< ContractModule @@ -44,8 +49,7 @@ export type MandatorySettlementModulesRecord = { @injectable() export class SettlementContractModule< - SettlementModules extends SettlementModulesRecord & - MandatorySettlementModulesRecord, + SettlementModules extends SettlementModulesRecord, > extends ModuleContainer implements ProtocolModule @@ -54,10 +58,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 +68,18 @@ export class SettlementContractModule< }; } - public static mandatoryModules() { + public static settlementOnly() { return { - SettlementContract: SettlementContractProtocolModule, - DispatchContract: DispatchContractProtocolModule, - BridgeContract: BridgeContractProtocolModule, + SettlementContract: SettlementContractModule, } 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 +108,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..802bdccbf 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -29,7 +29,7 @@ import { Path } from "../../model/Path"; import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor"; import { PROTOKIT_FIELD_PREFIXES } from "../../hashing/protokit-prefixes"; -import type { SettlementContractType } from "./SettlementSmartContract"; +import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; export type BridgeContractType = { stateRoot: State; @@ -67,7 +67,7 @@ export class BridgeContractContext { export abstract class BridgeContractBase extends TokenContract { public static args: { SettlementContract: - | (TypedClass & typeof SmartContract) + | (TypedClass & typeof SmartContract) | undefined; messageProcessors: OutgoingMessageProcessor[]; batchSize?: number; diff --git a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts similarity index 75% rename from packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts rename to packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts index 6cdf51952..3fc1759aa 100644 --- a/packages/protocol/src/settlement/contracts/SettlementContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts @@ -15,24 +15,21 @@ import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; import { DispatchSmartContractBase } from "./DispatchSmartContract"; import { - SettlementContractType, - SettlementSmartContract, - SettlementSmartContractBase, -} from "./SettlementSmartContract"; + BridgingSettlementContractType, + BridgingSettlementContractBase, + BridgingSettlementContract, +} from "./settlement/BridgingSettlementContract"; import { BridgeContractBase } 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"; @injectable() -export class SettlementContractProtocolModule extends ContractModule< - SettlementContractType, +export class BridgingSettlementContractModule extends ContractModule< + BridgingSettlementContractType, SettlementContractConfig > { public constructor( @@ -49,7 +46,7 @@ export class SettlementContractProtocolModule extends ContractModule< super(); } - public contractFactory(): SmartContractClassFromInterface { + public contractFactory(): SmartContractClassFromInterface { const { hooks, config } = this; const dispatchContract = this.dispatchContractModule.contractFactory(); const bridgeContract = this.bridgeContractModule.contractFactory(); @@ -57,8 +54,8 @@ export class SettlementContractProtocolModule extends ContractModule< const escapeHatchSlotsInterval = config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; - const { args } = SettlementSmartContractBase; - SettlementSmartContractBase.args = { + const { args } = BridgingSettlementContractBase; + BridgingSettlementContractBase.args = { ...args, DispatchContract: dispatchContract, hooks, @@ -72,12 +69,12 @@ export class SettlementContractProtocolModule extends ContractModule< // 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; + BridgeContractBase.args.SettlementContract = BridgingSettlementContract; DispatchSmartContractBase.args.settlementContractClass = - SettlementSmartContract; + BridgingSettlementContract; - return SettlementSmartContract; + return BridgingSettlementContract; } public async compile( @@ -91,10 +88,10 @@ export class SettlementContractProtocolModule extends ContractModule< this.contractFactory(); // Init params - SettlementSmartContractBase.args.BridgeContractVerificationKey = + BridgingSettlementContractBase.args.BridgeContractVerificationKey = bridgeArtifact.BridgeContract.verificationKey; - if (SettlementSmartContractBase.args.signedSettlements === undefined) { + if (BridgingSettlementContractBase.args.signedSettlements === undefined) { throw new Error( "Args not fully initialized - make sure to also include the SettlementModule in the sequencer" ); @@ -103,7 +100,7 @@ export class SettlementContractProtocolModule extends ContractModule< log.debug("Compiling Settlement Contract"); const artifact = await registry.forceProverExists( - async (reg) => await registry.compile(SettlementSmartContract) + async (reg) => await registry.compile(BridgingSettlementContract) ); return { diff --git a/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts b/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts index 3e4de1ab1..f79b77d37 100644 --- a/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts +++ b/packages/protocol/src/settlement/contracts/DispatchSmartContract.ts @@ -26,7 +26,7 @@ import { } from "../../utils/MinaPrefixedProvableHashList"; import { Deposit } from "../messages/Deposit"; -import type { SettlementContractType } from "./SettlementSmartContract"; +import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; import { TokenBridgeDeploymentAuth } from "./authorizations/TokenBridgeDeploymentAuth"; import { UpdateMessagesHashAuth } from "./authorizations/UpdateMessagesHashAuth"; import { @@ -40,6 +40,10 @@ import { export const ACTIONS_EMPTY_HASH = Reducer.initialActionState; export interface DispatchContractType { + events: { + "token-bridge-added": typeof TokenBridgeTreeAddition; + }; + updateMessagesHash: ( executedMessagesHash: Field, newPromisedMessagesHash: Field @@ -67,7 +71,7 @@ export abstract class DispatchSmartContractBase extends SmartContract { public static args: { methodIdMappings: RuntimeMethodIdMapping; incomingMessagesPaths: Record; - settlementContractClass?: TypedClass & + settlementContractClass?: TypedClass & typeof SmartContract; }; diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts new file mode 100644 index 000000000..0106ff32f --- /dev/null +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts @@ -0,0 +1,80 @@ +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 { + BridgingSettlementContractType, + BridgingSettlementContract, +} from "./settlement/BridgingSettlementContract"; +import { SettlementBase } from "./settlement/SettlementBase"; + +export type SettlementContractConfig = { + escapeHatchSlotsInterval?: number; +}; + +// 24 hours +export const DEFAULT_ESCAPE_HATCH = (60 / 3) * 24; + +@injectable() +export class SettlementSmartContractModule extends ContractModule< + BridgingSettlementContractType, + SettlementContractConfig +> { + public constructor( + @injectAll("ProvableSettlementHook") + private readonly hooks: ProvableSettlementHook[], + @inject("BlockProver") + private readonly blockProver: BlockProvable, + private readonly childVerificationKeyService: ChildVerificationKeyService + ) { + super(); + } + + public contractFactory(): SmartContractClassFromInterface { + const { hooks, config } = this; + + const escapeHatchSlotsInterval = + config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; + + const { args } = SettlementBase; + SettlementBase.args = { + ...args, + hooks, + escapeHatchSlotsInterval, + signedSettlements: args?.signedSettlements, + ChildVerificationKeyService: this.childVerificationKeyService, + }; + + return BridgingSettlementContract; + } + + 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(BridgingSettlementContract) + ); + + return { + SettlementSmartContract: artifact, + }; + } +} diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts similarity index 52% rename from packages/protocol/src/settlement/contracts/SettlementSmartContract.ts rename to packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts index 06ee0ce68..a823af09d 100644 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContract.ts +++ b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts @@ -1,10 +1,4 @@ -import { - prefixToField, - TypedClass, - mapSequential, - ChildVerificationKeyService, - LinkedMerkleTree, -} from "@proto-kit/common"; +import { TypedClass, ChildVerificationKeyService } from "@proto-kit/common"; import { AccountUpdate, Bool, @@ -17,52 +11,31 @@ import { 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 { NetworkState } from "../../../model/network/NetworkState"; +import { ProvableSettlementHook } from "../../modularity/ProvableSettlementHook"; +import { DispatchContractType } from "../DispatchSmartContract"; +import { BridgeContractType } from "../BridgeContract"; +import { TokenBridgeDeploymentAuth } from "../authorizations/TokenBridgeDeploymentAuth"; +import { UpdateMessagesHashAuth } from "../authorizations/UpdateMessagesHashAuth"; -import { DispatchContractType } from "./DispatchSmartContract"; -import { BridgeContractType } from "./BridgeContract"; -import { TokenBridgeDeploymentAuth } from "./authorizations/TokenBridgeDeploymentAuth"; -import { UpdateMessagesHashAuth } from "./authorizations/UpdateMessagesHashAuth"; +import { DynamicBlockProof, SettlementBase } from "./SettlementBase"; /* 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 { +export interface BridgingSettlementContractType { authorizationField: State; deployAndInitialize: ( @@ -76,7 +49,6 @@ export interface SettlementContractType { settle: ( blockProof: DynamicBlockProof, signature: Signature, - dispatchContractAddress: PublicKey, publicKey: PublicKey, inputNetworkState: NetworkState, outputNetworkState: NetworkState, @@ -89,9 +61,6 @@ export interface SettlementContractType { ) => Promise; } -// Some random prefix for the sequencer signature -export const BATCH_SIGNATURE_PREFIX = prefixToField("pk-batchSignature"); - // @singleton() // export class SettlementSmartContractStaticArgs { // public args?: { @@ -106,7 +75,7 @@ export const BATCH_SIGNATURE_PREFIX = prefixToField("pk-batchSignature"); // }; // } -export abstract class SettlementSmartContractBase extends TokenContract { +export abstract class BridgingSettlementContractBase extends SettlementBase { // 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); @@ -126,12 +95,7 @@ export abstract class SettlementSmartContractBase extends TokenContract { "token-bridge-deployed": TokenMapping, }; - abstract sequencerKey: State; - abstract lastSettlementL1BlockHeight: State; - abstract stateRoot: State; - abstract networkStateHash: State; - abstract blockHashRoot: State; - abstract dispatchContractAddressX: State; + abstract dispatchContractAddress: State; abstract authorizationField: State; @@ -143,10 +107,19 @@ export abstract class SettlementSmartContractBase extends TokenContract { return this.self; } + protected async initializeBaseBridging( + sequencer: PublicKey, + dispatchContract: PublicKey + ) { + await super.initializeBase(sequencer); + + this.dispatchContractAddress.set(dispatchContract); + } + // TODO Like these properties, I am too lazy to properly infer the types here private assertLazyConfigsInitialized() { const uninitializedProperties: string[] = []; - const { args } = SettlementSmartContractBase; + const { args } = BridgingSettlementContractBase; if (args.BridgeContractPermissions === undefined) { uninitializedProperties.push("BridgeContractPermissions"); } @@ -162,17 +135,12 @@ export abstract class SettlementSmartContractBase extends TokenContract { } } - protected async deployTokenBridge( - tokenId: Field, - address: PublicKey, - dispatchContractAddress: PublicKey, - dispatchContractPreconditionEnforced = false - ) { + protected async deployTokenBridge(tokenId: Field, address: PublicKey) { Provable.asProver(() => { this.assertLazyConfigsInitialized(); }); - const { args } = SettlementSmartContractBase; + const { args } = BridgingSettlementContractBase; const BridgeContractClass = args.BridgeContract; const bridgeContract = new BridgeContractClass(address, tokenId); @@ -223,11 +191,8 @@ export abstract class SettlementSmartContractBase extends TokenContract { }) ); - // 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); - } + const dispatchContractAddress = + this.dispatchContractAddress.getAndRequireEquals(); // Set authorization for the auth callback, that we need this.authorizationField.set( @@ -238,61 +203,35 @@ export abstract class SettlementSmartContractBase extends TokenContract { }).hash() ); const dispatchContract = - new SettlementSmartContractBase.args.DispatchContract( + new BridgingSettlementContractBase.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( + protected async settleBaseBridging( 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" + await super.settleBase( + blockProof, + signature, + publicKey, + inputNetworkState, + outputNetworkState, + newPromisedMessagesHash ); - const { DispatchContract, escapeHatchSlotsInterval, hooks } = - SettlementSmartContractBase.args; + const dispatchContractAddress = + this.dispatchContractAddress.getAndRequireEquals(); + + const { DispatchContract } = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + (this.constructor as typeof BridgingSettlementContractBase).args; // Get dispatch contract values // These values are witnesses but will be checked later on the AU @@ -300,90 +239,6 @@ export abstract class SettlementSmartContractBase extends TokenContract { 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, @@ -408,14 +263,12 @@ export abstract class SettlementSmartContractBase extends TokenContract { promisedMessagesHash, newPromisedMessagesHash ); - - this.lastSettlementL1BlockHeight.set(minBlockHeightIncluded); } } -export class SettlementSmartContract - extends SettlementSmartContractBase - implements SettlementContractType +export class BridgingSettlementContract + extends BridgingSettlementContractBase + implements BridgingSettlementContractType { @state(Field) public sequencerKey = State(); @state(UInt32) public lastSettlementL1BlockHeight = State(); @@ -424,7 +277,7 @@ export class SettlementSmartContract @state(Field) public networkStateHash = State(); @state(Field) public blockHashRoot = State(); - @state(Field) public dispatchContractAddressX = State(); + @state(PublicKey) public dispatchContractAddress = State(); @state(Field) public authorizationField = State(); @@ -438,7 +291,7 @@ export class SettlementSmartContract this.self.account.permissions.set(permissions); - await this.initializeBase(sequencer, dispatchContract); + await this.initializeBaseBridging(sequencer, dispatchContract); } @method async approveBase(forest: AccountUpdateForest) { @@ -446,28 +299,22 @@ export class SettlementSmartContract } @method - public async addTokenBridge( - tokenId: Field, - address: PublicKey, - dispatchContract: PublicKey - ) { - await this.deployTokenBridge(tokenId, address, dispatchContract); + public async addTokenBridge(tokenId: Field, address: PublicKey) { + await this.deployTokenBridge(tokenId, address); } @method public async settle( blockProof: DynamicBlockProof, signature: Signature, - dispatchContractAddress: PublicKey, publicKey: PublicKey, inputNetworkState: NetworkState, outputNetworkState: NetworkState, newPromisedMessagesHash: Field ) { - return await this.settleBase( + return await this.settleBaseBridging( blockProof, signature, - dispatchContractAddress, publicKey, inputNetworkState, outputNetworkState, 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..a35425275 --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -0,0 +1,217 @@ +import { + Bool, + DynamicProof, + Field, + PublicKey, + Signature, + State, + TokenContract, + UInt32, +} from "o1js"; +import { + ChildVerificationKeyService, + LinkedMerkleTree, + mapSequential, + prefixToField, +} from "@proto-kit/common"; + +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"; + +/* 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; + + settle: ( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) => Promise; +} + +export abstract class SettlementBase extends TokenContract { + public static args: { + hooks: ProvableSettlementHook[]; + escapeHatchSlotsInterval: number; + signedSettlements: boolean | undefined; + ChildVerificationKeyService: ChildVerificationKeyService; + }; + + 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; + + protected async settleBase( + blockProof: DynamicBlockProof, + signature: Signature, + publicKey: PublicKey, + inputNetworkState: NetworkState, + outputNetworkState: NetworkState, + newPromisedMessagesHash: Field + ) { + // Brought in as a constant + const blockProofVk = + SettlementBase.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 { escapeHatchSlotsInterval, hooks } = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + (this.constructor as typeof SettlementBase).args; + + // 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); + } + + // TODO Move all settlement-only logic here from the old impl +} + +// TODO Connect the above with the Smartcontract API implementing the abstract class + +/* 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..0a0c802f8 --- /dev/null +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts @@ -0,0 +1,55 @@ +import { + State, + UInt32, + AccountUpdateForest, + state, + method, + PublicKey, + Field, + Signature, +} from "o1js"; + +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(); + + @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..03da15930 100644 --- a/packages/protocol/src/settlement/modularity/types.ts +++ b/packages/protocol/src/settlement/modularity/types.ts @@ -5,13 +5,14 @@ import { SmartContractClassFromInterface, } from "../ContractModule"; import type { SettlementModulesRecord } from "../SettlementContractModule"; +import { SmartContract } from "o1js"; export type InferContractType< Module extends TypedClass>, > = 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, From 468a75adc66ea77e01c23a4d06d7ba268a2e0dae Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Wed, 17 Dec 2025 22:05:46 +0100 Subject: [PATCH 02/12] Changed static args injection to global container injection --- packages/common/src/types.ts | 12 +- packages/protocol/src/index.ts | 1 + .../src/settlement/ContractArgsRegistry.ts | 29 +++++ .../settlement/SettlementContractModule.ts | 7 +- .../BridgingSettlementContractModule.ts | 29 +++-- .../SettlementSmartContractModule.ts | 25 ++-- .../settlement/BridgingSettlementContract.ts | 117 ++++++++++-------- .../contracts/settlement/SettlementBase.ts | 70 ++++++++--- .../settlement/SettlementContract.ts | 20 +++ .../src/settlement/modularity/types.ts | 2 +- 10 files changed, 221 insertions(+), 91 deletions(-) create mode 100644 packages/protocol/src/settlement/ContractArgsRegistry.ts 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/index.ts b/packages/protocol/src/index.ts index 171157b6c..080dfc646 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -63,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..d0b733645 --- /dev/null +++ b/packages/protocol/src/settlement/ContractArgsRegistry.ts @@ -0,0 +1,29 @@ +import { injectable, singleton } from "tsyringe"; + +export interface StaticInitializationContract { + getInitializationArgs(): Args; + + // name: string; +} + +@injectable() +@singleton() +export class ContractArgsRegistry { + args: Record = {}; + + public setArgs( + // contract: TypedClass>, + name: string, + args: Type + ) { + this.args[name] = args; + } + + public getArgs( + // contract: TypedClass> + name: string + ): Type | undefined { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return this.args[name] as Type; + } +} diff --git a/packages/protocol/src/settlement/SettlementContractModule.ts b/packages/protocol/src/settlement/SettlementContractModule.ts index 88cb24327..3bfaa778b 100644 --- a/packages/protocol/src/settlement/SettlementContractModule.ts +++ b/packages/protocol/src/settlement/SettlementContractModule.ts @@ -25,7 +25,10 @@ import { import { GetContracts, InferContractType } from "./modularity/types"; import { BridgingSettlementContractType } from "./contracts/settlement/BridgingSettlementContract"; import { SettlementContractType } from "./contracts/settlement/SettlementBase"; -import { SettlementContractConfig } from "./contracts/SettlementSmartContractModule"; +import { + SettlementContractConfig, + SettlementSmartContractModule, +} from "./contracts/SettlementSmartContractModule"; export type SettlementModulesRecord = ModulesRecord< TypedClass> @@ -70,7 +73,7 @@ export class SettlementContractModule< public static settlementOnly() { return { - SettlementContract: SettlementContractModule, + SettlementContract: SettlementSmartContractModule, } as const; } diff --git a/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts index 3fc1759aa..09b1270e9 100644 --- a/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts @@ -12,12 +12,13 @@ import { SmartContractClassFromInterface, } from "../ContractModule"; import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { DispatchSmartContractBase } from "./DispatchSmartContract"; import { BridgingSettlementContractType, - BridgingSettlementContractBase, BridgingSettlementContract, + BridgingSettlementContractArgs, } from "./settlement/BridgingSettlementContract"; import { BridgeContractBase } from "./BridgeContract"; import { DispatchContractProtocolModule } from "./DispatchContractProtocolModule"; @@ -26,6 +27,7 @@ import { DEFAULT_ESCAPE_HATCH, SettlementContractConfig, } from "./SettlementSmartContractModule"; +import { SettlementContract } from "./settlement/SettlementContract"; @injectable() export class BridgingSettlementContractModule extends ContractModule< @@ -41,7 +43,8 @@ export class BridgingSettlementContractModule 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(); } @@ -54,8 +57,11 @@ export class BridgingSettlementContractModule extends ContractModule< const escapeHatchSlotsInterval = config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; - const { args } = BridgingSettlementContractBase; - BridgingSettlementContractBase.args = { + const args = + this.argsRegistry.getArgs( + "SettlementContract" + ); + const newArgs = { ...args, DispatchContract: dispatchContract, hooks, @@ -66,6 +72,7 @@ export class BridgingSettlementContractModule extends ContractModule< signedSettlements: args?.signedSettlements, ChildVerificationKeyService: this.childVerificationKeyService, }; + this.argsRegistry.setArgs("SettlementContract", newArgs); // 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 @@ -88,10 +95,14 @@ export class BridgingSettlementContractModule extends ContractModule< this.contractFactory(); // Init params - BridgingSettlementContractBase.args.BridgeContractVerificationKey = + const args = + this.argsRegistry.getArgs( + "SettlementContract" + )!; + args.BridgeContractVerificationKey = bridgeArtifact.BridgeContract.verificationKey; - if (BridgingSettlementContractBase.args.signedSettlements === undefined) { + if (args.signedSettlements === undefined) { throw new Error( "Args not fully initialized - make sure to also include the SettlementModule in the sequencer" ); @@ -100,7 +111,11 @@ export class BridgingSettlementContractModule extends ContractModule< log.debug("Compiling Settlement Contract"); const artifact = await registry.forceProverExists( - async (reg) => await registry.compile(BridgingSettlementContract) + async (reg) => + await registry.compile( + BridgingSettlementContract, + SettlementContract.name + ) ); return { diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts index 0106ff32f..d1afb3a4b 100644 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts @@ -12,12 +12,14 @@ import { SmartContractClassFromInterface, } from "../ContractModule"; import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; +import { BridgingSettlementContract } from "./settlement/BridgingSettlementContract"; import { - BridgingSettlementContractType, - BridgingSettlementContract, -} from "./settlement/BridgingSettlementContract"; -import { SettlementBase } from "./settlement/SettlementBase"; + SettlementContractArgs, + SettlementContractType, +} from "./settlement/SettlementBase"; +import { SettlementContract } from "./settlement/SettlementContract"; export type SettlementContractConfig = { escapeHatchSlotsInterval?: number; @@ -28,7 +30,7 @@ export const DEFAULT_ESCAPE_HATCH = (60 / 3) * 24; @injectable() export class SettlementSmartContractModule extends ContractModule< - BridgingSettlementContractType, + SettlementContractType, SettlementContractConfig > { public constructor( @@ -36,25 +38,28 @@ export class SettlementSmartContractModule extends ContractModule< private readonly hooks: ProvableSettlementHook[], @inject("BlockProver") private readonly blockProver: BlockProvable, - private readonly childVerificationKeyService: ChildVerificationKeyService + private readonly childVerificationKeyService: ChildVerificationKeyService, + private readonly argsRegistry: ContractArgsRegistry ) { super(); } - public contractFactory(): SmartContractClassFromInterface { + public contractFactory(): SmartContractClassFromInterface { const { hooks, config } = this; const escapeHatchSlotsInterval = config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; - const { args } = SettlementBase; - SettlementBase.args = { + const args = + this.argsRegistry.getArgs("SettlementContract"); + const newArgs = { ...args, hooks, escapeHatchSlotsInterval, signedSettlements: args?.signedSettlements, ChildVerificationKeyService: this.childVerificationKeyService, }; + this.argsRegistry.setArgs("SettlementContract", newArgs); return BridgingSettlementContract; } @@ -70,7 +75,7 @@ export class SettlementSmartContractModule extends ContractModule< log.debug("Compiling Settlement Contract"); const artifact = await registry.forceProverExists( - async (reg) => await registry.compile(BridgingSettlementContract) + async (reg) => await registry.compile(SettlementContract) ); return { diff --git a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts index a823af09d..f5aeee9ff 100644 --- a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts +++ b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts @@ -1,4 +1,4 @@ -import { TypedClass, ChildVerificationKeyService } from "@proto-kit/common"; +import { TypedClass, O1PublicKeyOption } from "@proto-kit/common"; import { AccountUpdate, Bool, @@ -18,15 +18,24 @@ import { TokenId, DeployArgs, } from "o1js"; +import { container } from "tsyringe"; import { NetworkState } from "../../../model/network/NetworkState"; -import { ProvableSettlementHook } from "../../modularity/ProvableSettlementHook"; import { DispatchContractType } from "../DispatchSmartContract"; import { BridgeContractType } from "../BridgeContract"; import { TokenBridgeDeploymentAuth } from "../authorizations/TokenBridgeDeploymentAuth"; import { UpdateMessagesHashAuth } from "../authorizations/UpdateMessagesHashAuth"; +import { + ContractArgsRegistry, + StaticInitializationContract, +} from "../../ContractArgsRegistry"; -import { DynamicBlockProof, SettlementBase } from "./SettlementBase"; +import { + DynamicBlockProof, + SettlementBase, + SettlementContractArgs, + SettlementContractType, +} from "./SettlementBase"; /* eslint-disable @typescript-eslint/lines-between-class-members */ @@ -35,30 +44,11 @@ export class TokenMapping extends Struct({ publicKey: PublicKey, }) {} -export interface BridgingSettlementContractType { +export interface BridgingSettlementContractType extends SettlementContractType { authorizationField: State; - deployAndInitialize: ( - args: DeployArgs | undefined, - permissions: Permissions, - sequencer: PublicKey, - dispatchContract: PublicKey - ) => Promise; - assertStateRoot: (root: Field) => AccountUpdate; - settle: ( - blockProof: DynamicBlockProof, - signature: Signature, - publicKey: PublicKey, - inputNetworkState: NetworkState, - outputNetworkState: NetworkState, - newPromisedMessagesHash: Field - ) => Promise; - addTokenBridge: ( - tokenId: Field, - address: PublicKey, - dispatchContract: PublicKey - ) => Promise; + addTokenBridge: (tokenId: Field, address: PublicKey) => Promise; } // @singleton() @@ -75,21 +65,39 @@ export interface BridgingSettlementContractType { // }; // } -export abstract class BridgingSettlementContractBase extends SettlementBase { +export interface BridgingSettlementContractArgs extends SettlementContractArgs { + DispatchContract: TypedClass; + BridgeContract: TypedClass & typeof SmartContract; + // Lazily initialized + BridgeContractVerificationKey: VerificationKey | undefined; + BridgeContractPermissions: Permissions | undefined; + signedSettlements: boolean | undefined; +} + +export abstract class BridgingSettlementContractBase + extends SettlementBase + implements StaticInitializationContract +{ // 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; - }; + // 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; + // }; + + public getInitializationArgs(): BridgingSettlementContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("SettlementContract")!; + } events = { "token-bridge-deployed": TokenMapping, @@ -119,7 +127,7 @@ export abstract class BridgingSettlementContractBase extends SettlementBase { // TODO Like these properties, I am too lazy to properly infer the types here private assertLazyConfigsInitialized() { const uninitializedProperties: string[] = []; - const { args } = BridgingSettlementContractBase; + const args = this.getInitializationArgs(); if (args.BridgeContractPermissions === undefined) { uninitializedProperties.push("BridgeContractPermissions"); } @@ -135,20 +143,22 @@ export abstract class BridgingSettlementContractBase extends SettlementBase { } } + // 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) { Provable.asProver(() => { this.assertLazyConfigsInitialized(); }); - const { args } = BridgingSettlementContractBase; - const BridgeContractClass = args.BridgeContract; - const bridgeContract = new BridgeContractClass(address, tokenId); - const { BridgeContractVerificationKey, signedSettlements, BridgeContractPermissions, - } = args; + BridgeContract: BridgeContractClass, + DispatchContract, + } = this.getInitializationArgs(); + + const bridgeContract = new BridgeContractClass(address, tokenId); if ( signedSettlements === undefined || @@ -169,9 +179,9 @@ export abstract class BridgingSettlementContractBase extends SettlementBase { // 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!, + BridgeContractVerificationKey, + signedSettlements!, + BridgeContractPermissions!, this.address ); @@ -202,10 +212,7 @@ export abstract class BridgingSettlementContractBase extends SettlementBase { address, }).hash() ); - const dispatchContract = - new BridgingSettlementContractBase.args.DispatchContract( - dispatchContractAddress - ); + const dispatchContract = new DispatchContract(dispatchContractAddress); await dispatchContract.enableTokenDeposits(tokenId, address, this.address); } @@ -229,9 +236,7 @@ export abstract class BridgingSettlementContractBase extends SettlementBase { const dispatchContractAddress = this.dispatchContractAddress.getAndRequireEquals(); - const { DispatchContract } = - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - (this.constructor as typeof BridgingSettlementContractBase).args; + const { DispatchContract } = this.getInitializationArgs(); // Get dispatch contract values // These values are witnesses but will be checked later on the AU @@ -285,13 +290,17 @@ export class BridgingSettlementContract args: DeployArgs | undefined, permissions: Permissions, sequencer: PublicKey, - dispatchContract: 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); + await this.initializeBaseBridging(sequencer, dispatchContract.value); } @method async approveBase(forest: AccountUpdateForest) { diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts index a35425275..0cd5b2beb 100644 --- a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -1,12 +1,15 @@ import { Bool, + DeployArgs, DynamicProof, Field, + Option, PublicKey, Signature, State, TokenContract, UInt32, + Permissions, } from "o1js"; import { ChildVerificationKeyService, @@ -14,6 +17,7 @@ import { mapSequential, prefixToField, } from "@proto-kit/common"; +import { container } from "tsyringe"; import { BlockHashMerkleTree } from "../../../prover/block/accummulators/BlockHashMerkleTree"; import { NetworkState } from "../../../model/network/NetworkState"; @@ -26,6 +30,10 @@ import { BlockProverPublicInput, BlockProverPublicOutput, } from "../../../prover/block/BlockProvable"; +import { + ContractArgsRegistry, + StaticInitializationContract, +} from "../../ContractArgsRegistry"; /* eslint-disable @typescript-eslint/lines-between-class-members */ @@ -50,6 +58,13 @@ export interface SettlementContractType { networkStateHash: State; blockHashRoot: State; + deployAndInitialize: ( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: Option + ) => Promise; + settle: ( blockProof: DynamicBlockProof, signature: Signature, @@ -60,13 +75,29 @@ export interface SettlementContractType { ) => Promise; } -export abstract class SettlementBase extends TokenContract { - public static args: { - hooks: ProvableSettlementHook[]; - escapeHatchSlotsInterval: number; - signedSettlements: boolean | undefined; - ChildVerificationKeyService: ChildVerificationKeyService; - }; +export interface SettlementContractArgs { + hooks: ProvableSettlementHook[]; + escapeHatchSlotsInterval: number; + signedSettlements: boolean | undefined; + ChildVerificationKeyService: ChildVerificationKeyService; +} + +export abstract class SettlementBase + extends TokenContract + implements StaticInitializationContract +{ + getInitializationArgs(): SettlementContractArgs { + return container + .resolve(ContractArgsRegistry) + .getArgs("SettlementContract")!; + } + // + // public static args: { + // hooks: ProvableSettlementHook[]; + // escapeHatchSlotsInterval: number; + // signedSettlements: boolean | undefined; + // ChildVerificationKeyService: ChildVerificationKeyService; + // }; abstract sequencerKey: State; abstract lastSettlementL1BlockHeight: State; @@ -90,6 +121,13 @@ export abstract class SettlementBase extends TokenContract { newPromisedMessagesHash: Field ): Promise; + abstract deployAndInitialize( + args: DeployArgs | undefined, + permissions: Permissions, + sequencer: PublicKey, + dispatchContract: Option + ): Promise; + protected async settleBase( blockProof: DynamicBlockProof, signature: Signature, @@ -98,30 +136,30 @@ export abstract class SettlementBase extends TokenContract { outputNetworkState: NetworkState, newPromisedMessagesHash: Field ) { + const { + escapeHatchSlotsInterval, + hooks, + ChildVerificationKeyService: childVerificationKeyService, + } = this.getInitializationArgs(); + // Brought in as a constant const blockProofVk = - SettlementBase.args.ChildVerificationKeyService.getVerificationKey( - "BlockProver" - ); + childVerificationKeyService.getVerificationKey("BlockProver"); if (!blockProofVk.hash.isConstant()) { throw new Error("Sanity check - vk hash has to be constant"); } - // Verify the blockproof - blockProof.verify(blockProofVk); + 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 { escapeHatchSlotsInterval, hooks } = - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - (this.constructor as typeof SettlementBase).args; - // Get block height and use the lower bound for all ops const minBlockHeightIncluded = this.network.blockchainLength.get(); this.network.blockchainLength.requireBetween( diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts index 0a0c802f8..95321684f 100644 --- a/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementContract.ts @@ -7,7 +7,10 @@ import { PublicKey, Field, Signature, + DeployArgs, + Permissions, } from "o1js"; +import { O1PublicKeyOption } from "@proto-kit/common"; import { NetworkState } from "../../../model/network/NetworkState"; @@ -31,6 +34,23 @@ export class SettlementContract @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); } diff --git a/packages/protocol/src/settlement/modularity/types.ts b/packages/protocol/src/settlement/modularity/types.ts index 03da15930..b05abdf6f 100644 --- a/packages/protocol/src/settlement/modularity/types.ts +++ b/packages/protocol/src/settlement/modularity/types.ts @@ -1,11 +1,11 @@ import { TypedClass } from "@proto-kit/common"; +import { SmartContract } from "o1js"; import { ContractModule, SmartContractClassFromInterface, } from "../ContractModule"; import type { SettlementModulesRecord } from "../SettlementContractModule"; -import { SmartContract } from "o1js"; export type InferContractType< Module extends TypedClass>, From c4b3f52888f01e0361722b22a4a0070350dc8d5c Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Wed, 17 Dec 2025 22:10:40 +0100 Subject: [PATCH 03/12] Restructured Settlement and Bridging to be functional in isolation --- .../common/src/compiling/CompileRegistry.ts | 15 +- .../production/tasks/CircuitCompilerTask.ts | 28 +- .../src/sequencer/SequencerStartupModule.ts | 14 +- .../src/sequencer/SettlementStartupModule.ts | 6 +- .../src/settlement/BridgingModule.ts | 156 ++++++- .../src/settlement/SettlementModule.ts | 382 ++++++------------ .../interactions/AddressRegistry.ts | 19 + .../interactions/DeployInteraction.ts | 11 + .../interactions/SettleInteraction.ts | 11 + .../bridging/BridgingDeployInteraction.ts | 131 ++++++ .../bridging/BridgingSettlementInteraction.ts | 127 ++++++ .../vanilla/VanillaDeployInteraction.ts | 120 ++++++ .../vanilla/VanillaSettlementInteraction.ts | 118 ++++++ .../messages/IncomingMessagesService.ts | 8 +- .../worker/startup/WorkerRegistrationTask.ts | 24 +- .../sequencer/test/integration/Proven.test.ts | 19 +- .../sequencer/test/settlement/Settlement.ts | 90 +++-- 17 files changed, 931 insertions(+), 348 deletions(-) create mode 100644 packages/sequencer/src/settlement/interactions/AddressRegistry.ts create mode 100644 packages/sequencer/src/settlement/interactions/DeployInteraction.ts create mode 100644 packages/sequencer/src/settlement/interactions/SettleInteraction.ts create mode 100644 packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts create mode 100644 packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts create mode 100644 packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts create mode 100644 packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts 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/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts index f3a06bd72..61d2de0c6 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,8 +154,11 @@ export class CircuitCompilerTask extends UnpreparingTask< } if (input.isSignedSettlement !== undefined) { - const contractArgs = SettlementSmartContractBase.args; - SettlementSmartContractBase.args = { + const contractArgs = + this.contractArgsRegistry.getArgs( + "SettlementContract" + ); + const newArgs = { ...contractArgs, signedSettlements: input.isSignedSettlement, // TODO Add distinction between mina and custom tokens @@ -160,6 +167,7 @@ export class CircuitCompilerTask extends UnpreparingTask< : new ProvenSettlementPermissions() ).bridgeContractMina(), }; + this.contractArgsRegistry.setArgs("SettlementContract", newArgs); } // TODO make adaptive diff --git a/packages/sequencer/src/sequencer/SequencerStartupModule.ts b/packages/sequencer/src/sequencer/SequencerStartupModule.ts index 8d2294de0..b3fec37ff 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,12 @@ 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 + const args = + this.contractArgsRegistry.getArgs( + "SettlementContract" + )!; + args.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..be3827bbd 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, @@ -42,9 +43,8 @@ export class SettlementStartupModule { SettlementSmartContract: CompileArtifact; DispatchSmartContract: CompileArtifact; }> { - const settlementVerificationKey = this.compileRegistry.getArtifact( - "SettlementSmartContract" - ); + const settlementVerificationKey = + this.compileRegistry.getArtifact("SettlementContract"); const dispatchVerificationKey = this.compileRegistry.getArtifact( "DispatchSmartContract" ); diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index 90bf94282..0f1d74fe0 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,6 +29,7 @@ import { Mina, Provable, PublicKey, + SmartContract, TokenContract, TokenId, Transaction, @@ -41,12 +46,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 +62,9 @@ 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"; export type SettlementTokenConfig = Record< string, @@ -67,6 +78,12 @@ export type SettlementTokenConfig = Record< } >; +export type BridgingModuleConfig = { + addresses?: { + DispatchContract: PublicKey; + }; +}; + /** * Module that facilitates all transaction creation and monitoring for * bridging related operations. @@ -74,7 +91,7 @@ export type SettlementTokenConfig = Record< * for those as needed */ @injectable() -export class BridgingModule { +export class BridgingModule extends SequencerModule { private seenBridgeDeployments: { latestDeployment: number; // tokenId => Bridge address @@ -86,6 +103,8 @@ export class BridgingModule { private utils: SettlementUtils; + protected dispatchContract?: DispatchContractType & SmartContract; + public constructor( @inject("Protocol") private readonly protocol: Protocol, @@ -99,18 +118,44 @@ 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 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 +172,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 @@ -154,6 +200,71 @@ export class BridgingModule { }; } + 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 { @@ -170,9 +281,9 @@ export class BridgingModule { 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 +422,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 +468,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 +658,29 @@ export class BridgingModule { return txs; } + + public async start(): Promise { + const contractArgs = + this.argsRegistry.getArgs( + "SettlementContract" + ); + + this.argsRegistry.setArgs("SettlementContract", { + ...contractArgs, + // 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 */ } diff --git a/packages/sequencer/src/settlement/SettlementModule.ts b/packages/sequencer/src/settlement/SettlementModule.ts index 597da53b3..a2004d45a 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; - }; + const contracts = settlementContractModule.createContracts({ + SettlementContract: address, + }); + this.contract = contracts.SettlementContract; } - 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,65 @@ 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" - ); - } - - 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], - }); - - await this.transactionSender.proveAndSendTransaction(txSigned, "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 start(): Promise { - const contractArgs = SettlementSmartContractBase.args; + const contractArgs = + this.argsRegistry.getArgs("SettlementContract"); - SettlementSmartContractBase.args = { + this.argsRegistry.setArgs("SettlementContract", { ...contractArgs, signedSettlements: this.baseLayer.isSignedSettlement(), - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (this.baseLayer.isSignedSettlement() - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; + }); + + const settlementContractAddress = this.config.addresses?.SettlementContract; + if (settlementContractAddress !== undefined) { + this.addressRegistry.addContractAddress( + "SettlementContract", + settlementContractAddress + ); + + 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 +265,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..cf88cc90d --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/AddressRegistry.ts @@ -0,0 +1,19 @@ +import { PublicKey } from "o1js"; + +export interface AddressRegistry { + getContractAddress(identifier: string): PublicKey | undefined; + + addContractAddress(identifier: string, address: PublicKey): void; +} + +export class InMemoryAddressRegistry implements AddressRegistry { + addresses: Record = {}; + + addContractAddress(identifier: string, address: PublicKey): void { + this.addresses[identifier] = address; + } + + getContractAddress(identifier: string): PublicKey { + return this.addresses[identifier]; + } +} 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..a75afc5c2 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts @@ -0,0 +1,131 @@ +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; + + // TODO Move this workflow to AddressRegistry + const sm = this.settlementContractModule(); + const { + SettlementContract: settlementContract, + DispatchContract: dispatchContract, + } = sm.createContracts({ + SettlementContract: settlementKey, + DispatchContract: dispatchKey, + }); + + const verificationsKeys = + await this.settlementStartupModule.retrieveVerificationKeys(); + + 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.SettlementSmartContract.verificationKey, + }, + permissions.settlementContract(), + feepayer, + O1PublicKeyOption.from(dispatchKey) + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: [...this.signer.getContractAddresses()], + }); + // this.signer.signTx(tx); + // 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"); + + 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..62b34c106 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts @@ -0,0 +1,127 @@ +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" + ); + } + + // TODO Move this workflow to AddressRegistry + 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..054882db0 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts @@ -0,0 +1,120 @@ +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; + + // TODO Move this workflow to AddressRegistry + 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(); + + 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 settlementContract.deployAndInitialize( + { + verificationKey: + verificationsKeys.SettlementSmartContract.verificationKey, + }, + permissions.settlementContract(), + feepayer, + O1PublicKeyOption.none() + ); + } + ); + + utils.signTransaction(tx, { + signingWithSignatureCheck: [...this.signer.getContractAddresses()], + }); + // this.signer.signTx(tx); + // 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"); + + 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..35f3ebb53 --- /dev/null +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts @@ -0,0 +1,118 @@ +import { inject } 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"; + +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"); + } + + // TODO Move this workflow to AddressRegistry + 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..a7aef8bd7 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 { 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..1ff4c0373 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,13 +88,19 @@ export class WorkerRegistrationTask } if (input.bridgeContractVerificationKey !== undefined) { - SettlementSmartContractBase.args.BridgeContractVerificationKey = - input.bridgeContractVerificationKey; + const args = + this.contractArgsRegistry.getArgs( + "SettlementContract" + )!; + args.BridgeContractVerificationKey = input.bridgeContractVerificationKey; } if (input.isSignedSettlement !== undefined) { - const contractArgs = SettlementSmartContractBase.args; - SettlementSmartContractBase.args = { + const contractArgs = + this.contractArgsRegistry.getArgs( + "SettlementContract" + )!; + this.contractArgsRegistry.setArgs("SettlementContract", { ...contractArgs, signedSettlements: input.isSignedSettlement, // TODO Add distinction between mina and custom tokens @@ -100,7 +108,7 @@ export class WorkerRegistrationTask ? 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..86ad46bd0 100644 --- a/packages/sequencer/test/integration/Proven.test.ts +++ b/packages/sequencer/test/integration/Proven.test.ts @@ -9,11 +9,11 @@ import { import { Runtime } from "@proto-kit/module"; import { BridgeContract, + BridgingSettlementContract, + ContractArgsRegistry, DispatchSmartContract, Protocol, SettlementContractModule, - SettlementSmartContract, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { VanillaProtocolModules } from "@proto-kit/library"; import { container } from "tsyringe"; @@ -66,7 +66,8 @@ describe.skip("Proven", () => { ProtocolStateTestHook, // ProtocolStateTestHook2, }), - SettlementContractModule: SettlementContractModule.with({ + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementAndBridging(), // FungibleToken: FungibleTokenContractModule, // FungibleTokenAdmin: FungibleTokenAdminContractModule, }), @@ -103,10 +104,7 @@ describe.skip("Proven", () => { type: "local", }, }, - SettlementModule: { - // TODO - feepayer: PrivateKey.random(), - }, + SettlementModule: {}, }, Runtime: { Balances: {}, @@ -172,7 +170,8 @@ describe.skip("Proven", () => { }, }); vkService.setCompileRegistry(registry); - SettlementSmartContractBase.args = { + + container.resolve(ContractArgsRegistry).setArgs("SettlementContract", { DispatchContract: DispatchSmartContract, ChildVerificationKeyService: vkService, BridgeContractVerificationKey: MOCK_VERIFICATION_KEY, @@ -182,8 +181,8 @@ describe.skip("Proven", () => { BridgeContractPermissions: new ProvenSettlementPermissions().bridgeContractMina(), escapeHatchSlotsInterval: 1000, - }; - const vk = await SettlementSmartContract.compile(); + }); + const vk = await BridgingSettlementContract.compile(); console.log(vk.verificationKey); } catch (e) { console.error(e); diff --git a/packages/sequencer/test/settlement/Settlement.ts b/packages/sequencer/test/settlement/Settlement.ts index cfc97c045..6cad77ff8 100644 --- a/packages/sequencer/test/settlement/Settlement.ts +++ b/packages/sequencer/test/settlement/Settlement.ts @@ -3,17 +3,19 @@ import { mapSequential, TypedClass, LinkedMerkleTree, + log, } from "@proto-kit/common"; import { VanillaProtocolModules } from "@proto-kit/library"; import { Runtime } from "@proto-kit/module"; import { BlockProverPublicInput, BridgeContract, + ContractArgsRegistry, + DispatchSmartContract, NetworkState, Protocol, ReturnType, SettlementContractModule, - SettlementSmartContractBase, } from "@proto-kit/protocol"; import { ClientAppChain, @@ -129,6 +131,7 @@ export const settlementTestFn = ( { BaseLayer: MinaBaseLayer, SettlementModule: SettlementModule, + BridgingModule: BridgingModule, SettlementSigner: InMemoryMinaSigner, }, { @@ -143,7 +146,8 @@ export const settlementTestFn = ( Protocol: Protocol.from({ ...VanillaProtocolModules.mandatoryModules({}), - SettlementContractModule: SettlementContractModule.with({ + SettlementContractModule: SettlementContractModule.from({ + ...SettlementContractModule.settlementAndBridging(), FungibleToken: FungibleTokenContractModule, FungibleTokenAdmin: FungibleTokenAdminContractModule, }), @@ -170,7 +174,9 @@ export const settlementTestFn = ( BlockTrigger: {}, Mempool: {}, BatchProducerModule: {}, - LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + LocalTaskWorkerModule: { + ...VanillaTaskWorkerModules.defaultConfig(), + }, BaseLayer: baseLayerConfig, SettlementSigner: { feepayer: sequencerKey, @@ -184,6 +190,7 @@ export const settlementTestFn = ( BlockProducerModule: {}, FeeStrategy: {}, SettlementModule: {}, + BridgingModule: {}, SequencerStartupModule: {}, TaskQueue: { @@ -262,6 +269,8 @@ export const settlementTestFn = ( } beforeAll(async () => { + log.setLevel("INFO"); + appChain = setupAppChain(); await appChain.start( @@ -302,8 +311,9 @@ export const settlementTestFn = ( }, timeout * 3); afterAll(async () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - SettlementSmartContractBase.args = undefined as any; + container + .resolve(ContractArgsRegistry) + .setArgs("SettlementContract", undefined); await appChain.close(); }); @@ -313,37 +323,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 +426,8 @@ export const settlementTestFn = ( signingWithSignatureCheck: [ tokenOwnerPubKeys.tokenOwner, tokenOwnerPubKeys.admin, - ...settlementModule.getContractAddresses(), + settlementModule.getSettlementContractAddress(), + bridgingModule.getDispatchContractAddress(), ], }); @@ -457,9 +486,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 +528,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 +554,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 +609,7 @@ export const settlementTestFn = ( settlementModule.utils.signTransaction(tx, { signingWithSignatureCheck: [ tokenOwnerPubKeys.tokenOwner, - ...settlementModule.getContractAddresses(), + settlementModule.getSettlementContractAddress(), ], signingPublicKeys: [userPublicKey], preventNoncePreconditionFor: [dispatch.address], @@ -770,7 +800,7 @@ export const settlementTestFn = ( signingWithSignatureCheck: [ tokenBridgeKey.toPublicKey(), tokenOwnerPubKeys.tokenOwner, - ...settlementModule.getContractAddresses(), + settlementModule.getSettlementContractAddress(), ], signingPublicKeys: [userPublicKey], }); @@ -804,16 +834,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(); }); }; From e4e4ae64cfda21429680db0a94148083b086cd33 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Wed, 17 Dec 2025 22:20:17 +0100 Subject: [PATCH 04/12] Some refactorings --- .../src/settlement/ContractArgsRegistry.ts | 13 ++------- .../settlement/SettlementContractModule.ts | 26 +---------------- .../settlement/BridgingSettlementContract.ts | 29 ------------------- .../contracts/settlement/SettlementBase.ts | 11 ------- .../bridging/BridgingDeployInteraction.ts | 4 --- .../vanilla/VanillaDeployInteraction.ts | 4 --- .../sequencer/test/settlement/Settlement.ts | 3 -- 7 files changed, 3 insertions(+), 87 deletions(-) diff --git a/packages/protocol/src/settlement/ContractArgsRegistry.ts b/packages/protocol/src/settlement/ContractArgsRegistry.ts index d0b733645..d6c8e5e35 100644 --- a/packages/protocol/src/settlement/ContractArgsRegistry.ts +++ b/packages/protocol/src/settlement/ContractArgsRegistry.ts @@ -2,8 +2,6 @@ import { injectable, singleton } from "tsyringe"; export interface StaticInitializationContract { getInitializationArgs(): Args; - - // name: string; } @injectable() @@ -11,18 +9,11 @@ export interface StaticInitializationContract { export class ContractArgsRegistry { args: Record = {}; - public setArgs( - // contract: TypedClass>, - name: string, - args: Type - ) { + public setArgs(name: string, args: Type) { this.args[name] = args; } - public getArgs( - // contract: TypedClass> - name: string - ): Type | undefined { + public getArgs(name: string): Type | undefined { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return this.args[name] as Type; } diff --git a/packages/protocol/src/settlement/SettlementContractModule.ts b/packages/protocol/src/settlement/SettlementContractModule.ts index 3bfaa778b..7cacb16af 100644 --- a/packages/protocol/src/settlement/SettlementContractModule.ts +++ b/packages/protocol/src/settlement/SettlementContractModule.ts @@ -7,7 +7,7 @@ import { noop, StringKeyOf, } from "@proto-kit/common"; -import { Field, PublicKey, SmartContract } from "o1js"; +import { Field, PublicKey } from "o1js"; import { injectable } from "tsyringe"; import { ProtocolEnvironment } from "../protocol/ProtocolEnvironment"; @@ -123,28 +123,4 @@ export class SettlementContractModule< SettlementModules[ContractName] >; } - - 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/settlement/BridgingSettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts index f5aeee9ff..40c2625f7 100644 --- a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts +++ b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts @@ -51,20 +51,6 @@ export interface BridgingSettlementContractType extends SettlementContractType { addTokenBridge: (tokenId: Field, address: PublicKey) => Promise; } -// @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 interface BridgingSettlementContractArgs extends SettlementContractArgs { DispatchContract: TypedClass; BridgeContract: TypedClass & typeof SmartContract; @@ -78,21 +64,6 @@ export abstract class BridgingSettlementContractBase extends SettlementBase implements StaticInitializationContract { - // 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; - // }; - public getInitializationArgs(): BridgingSettlementContractArgs { return container .resolve(ContractArgsRegistry) diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts index 0cd5b2beb..6cc920052 100644 --- a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -91,13 +91,6 @@ export abstract class SettlementBase .resolve(ContractArgsRegistry) .getArgs("SettlementContract")!; } - // - // public static args: { - // hooks: ProvableSettlementHook[]; - // escapeHatchSlotsInterval: number; - // signedSettlements: boolean | undefined; - // ChildVerificationKeyService: ChildVerificationKeyService; - // }; abstract sequencerKey: State; abstract lastSettlementL1BlockHeight: State; @@ -246,10 +239,6 @@ export abstract class SettlementBase this.lastSettlementL1BlockHeight.set(minBlockHeightIncluded); } - - // TODO Move all settlement-only logic here from the old impl } -// TODO Connect the above with the Smartcontract API implementing the abstract class - /* eslint-enable @typescript-eslint/lines-between-class-members */ diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts index a75afc5c2..174bd7453 100644 --- a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts @@ -115,11 +115,7 @@ export class BridgingDeployInteraction implements DeployInteraction { utils.signTransaction(tx, { signingWithSignatureCheck: [...this.signer.getContractAddresses()], }); - // this.signer.signTx(tx); - // 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"); this.addressRegistry.addContractAddress( diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts index 054882db0..382699478 100644 --- a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts @@ -105,11 +105,7 @@ export class VanillaDeployInteraction implements DeployInteraction { utils.signTransaction(tx, { signingWithSignatureCheck: [...this.signer.getContractAddresses()], }); - // this.signer.signTx(tx); - // 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"); this.addressRegistry.addContractAddress( diff --git a/packages/sequencer/test/settlement/Settlement.ts b/packages/sequencer/test/settlement/Settlement.ts index 6cad77ff8..b972f0286 100644 --- a/packages/sequencer/test/settlement/Settlement.ts +++ b/packages/sequencer/test/settlement/Settlement.ts @@ -3,7 +3,6 @@ import { mapSequential, TypedClass, LinkedMerkleTree, - log, } from "@proto-kit/common"; import { VanillaProtocolModules } from "@proto-kit/library"; import { Runtime } from "@proto-kit/module"; @@ -269,8 +268,6 @@ export const settlementTestFn = ( } beforeAll(async () => { - log.setLevel("INFO"); - appChain = setupAppChain(); await appChain.start( From eb1113506a1b73ed5b1fea077362da59ca303ceb Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Wed, 17 Dec 2025 23:15:58 +0100 Subject: [PATCH 05/12] Refactored ArgsRegistry --- .../src/settlement/ContractArgsRegistry.ts | 50 +++++++++++++++++-- .../settlement/SettlementContractModule.ts | 26 +++++++++- .../BridgingSettlementContractModule.ts | 44 ++++++++-------- .../SettlementSmartContractModule.ts | 9 +--- .../settlement/BridgingSettlementContract.ts | 48 +++++------------- .../contracts/settlement/SettlementBase.ts | 13 ++++- .../production/tasks/CircuitCompilerTask.ts | 25 ++++------ .../src/sequencer/SequencerStartupModule.ts | 11 ++-- .../src/settlement/BridgingModule.ts | 23 ++++----- .../src/settlement/SettlementModule.ts | 14 ++---- .../worker/startup/WorkerRegistrationTask.ts | 33 ++++++------ .../sequencer/test/settlement/Settlement.ts | 4 +- 12 files changed, 166 insertions(+), 134 deletions(-) diff --git a/packages/protocol/src/settlement/ContractArgsRegistry.ts b/packages/protocol/src/settlement/ContractArgsRegistry.ts index d6c8e5e35..f9c2f0020 100644 --- a/packages/protocol/src/settlement/ContractArgsRegistry.ts +++ b/packages/protocol/src/settlement/ContractArgsRegistry.ts @@ -1,20 +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 setArgs(name: string, args: Type) { - this.args[name] = args; + 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 getArgs(name: string): Type | undefined { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return this.args[name] as Type; + 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 7cacb16af..3bfaa778b 100644 --- a/packages/protocol/src/settlement/SettlementContractModule.ts +++ b/packages/protocol/src/settlement/SettlementContractModule.ts @@ -7,7 +7,7 @@ import { noop, StringKeyOf, } from "@proto-kit/common"; -import { Field, PublicKey } from "o1js"; +import { Field, PublicKey, SmartContract } from "o1js"; import { injectable } from "tsyringe"; import { ProtocolEnvironment } from "../protocol/ProtocolEnvironment"; @@ -123,4 +123,28 @@ export class SettlementContractModule< SettlementModules[ContractName] >; } + + 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/BridgingSettlementContractModule.ts b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts index 09b1270e9..bdd8a461b 100644 --- a/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts @@ -19,6 +19,7 @@ import { BridgingSettlementContractType, BridgingSettlementContract, BridgingSettlementContractArgs, + BridgingSettlementContractArgsSchema, } from "./settlement/BridgingSettlementContract"; import { BridgeContractBase } from "./BridgeContract"; import { DispatchContractProtocolModule } from "./DispatchContractProtocolModule"; @@ -57,22 +58,16 @@ export class BridgingSettlementContractModule extends ContractModule< const escapeHatchSlotsInterval = config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; - const args = - this.argsRegistry.getArgs( - "SettlementContract" - ); - const newArgs = { - ...args, - DispatchContract: dispatchContract, - hooks, - escapeHatchSlotsInterval, - BridgeContract: bridgeContract, - BridgeContractVerificationKey: args?.BridgeContractVerificationKey, - BridgeContractPermissions: args?.BridgeContractPermissions, - signedSettlements: args?.signedSettlements, - ChildVerificationKeyService: this.childVerificationKeyService, - }; - this.argsRegistry.setArgs("SettlementContract", newArgs); + this.argsRegistry.addArgs( + "SettlementContract", + { + DispatchContract: dispatchContract, + hooks, + escapeHatchSlotsInterval, + BridgeContract: bridgeContract, + 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 @@ -95,13 +90,18 @@ export class BridgingSettlementContractModule extends ContractModule< this.contractFactory(); // Init params - const args = - this.argsRegistry.getArgs( - "SettlementContract" - )!; - args.BridgeContractVerificationKey = - bridgeArtifact.BridgeContract.verificationKey; + this.argsRegistry.addArgs( + "SettlementContract", + { + BridgeContractVerificationKey: + bridgeArtifact.BridgeContract.verificationKey, + } + ); + const args = this.argsRegistry.getArgs( + "SettlementContract", + BridgingSettlementContractArgsSchema + ); if (args.signedSettlements === undefined) { throw new Error( "Args not fully initialized - make sure to also include the SettlementModule in the sequencer" diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts index d1afb3a4b..e93a28eaa 100644 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts @@ -50,16 +50,11 @@ export class SettlementSmartContractModule extends ContractModule< const escapeHatchSlotsInterval = config.escapeHatchSlotsInterval ?? DEFAULT_ESCAPE_HATCH; - const args = - this.argsRegistry.getArgs("SettlementContract"); - const newArgs = { - ...args, + this.argsRegistry.addArgs("SettlementContract", { hooks, escapeHatchSlotsInterval, - signedSettlements: args?.signedSettlements, ChildVerificationKeyService: this.childVerificationKeyService, - }; - this.argsRegistry.setArgs("SettlementContract", newArgs); + }); return BridgingSettlementContract; } diff --git a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts index 40c2625f7..7610f86d5 100644 --- a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts +++ b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts @@ -27,6 +27,7 @@ import { TokenBridgeDeploymentAuth } from "../authorizations/TokenBridgeDeployme import { UpdateMessagesHashAuth } from "../authorizations/UpdateMessagesHashAuth"; import { ContractArgsRegistry, + NaiveObjectSchema, StaticInitializationContract, } from "../../ContractArgsRegistry"; @@ -34,6 +35,7 @@ import { DynamicBlockProof, SettlementBase, SettlementContractArgs, + SettlementContractArgsSchema, SettlementContractType, } from "./SettlementBase"; @@ -56,10 +58,18 @@ export interface BridgingSettlementContractArgs extends SettlementContractArgs { BridgeContract: TypedClass & typeof SmartContract; // Lazily initialized BridgeContractVerificationKey: VerificationKey | undefined; - BridgeContractPermissions: Permissions | undefined; - signedSettlements: boolean | undefined; + BridgeContractPermissions: Permissions; } +export const BridgingSettlementContractArgsSchema: NaiveObjectSchema = + { + ...SettlementContractArgsSchema, + DispatchContract: "Required", + BridgeContract: "Required", + BridgeContractVerificationKey: "Optional", + BridgeContractPermissions: "Required", + }; + export abstract class BridgingSettlementContractBase extends SettlementBase implements StaticInitializationContract @@ -67,7 +77,7 @@ export abstract class BridgingSettlementContractBase public getInitializationArgs(): BridgingSettlementContractArgs { return container .resolve(ContractArgsRegistry) - .getArgs("SettlementContract")!; + .getArgs("SettlementContract", BridgingSettlementContractArgsSchema); } events = { @@ -95,32 +105,9 @@ export abstract class BridgingSettlementContractBase this.dispatchContractAddress.set(dispatchContract); } - // TODO Like these properties, I am too lazy to properly infer the types here - private assertLazyConfigsInitialized() { - const uninitializedProperties: string[] = []; - const args = this.getInitializationArgs(); - 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}` - )}` - ); - } - } - // 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) { - Provable.asProver(() => { - this.assertLazyConfigsInitialized(); - }); - const { BridgeContractVerificationKey, signedSettlements, @@ -131,15 +118,6 @@ export abstract class BridgingSettlementContractBase const bridgeContract = new BridgeContractClass(address, tokenId); - if ( - signedSettlements === undefined || - BridgeContractPermissions === undefined - ) { - throw new Error( - "Static arguments for SettlementSmartContract not initialized" - ); - } - if ( BridgeContractVerificationKey !== undefined && !BridgeContractVerificationKey.hash.isConstant() diff --git a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts index 6cc920052..cc6401c03 100644 --- a/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts +++ b/packages/protocol/src/settlement/contracts/settlement/SettlementBase.ts @@ -32,6 +32,7 @@ import { } from "../../../prover/block/BlockProvable"; import { ContractArgsRegistry, + NaiveObjectSchema, StaticInitializationContract, } from "../../ContractArgsRegistry"; @@ -78,10 +79,18 @@ export interface SettlementContractType { export interface SettlementContractArgs { hooks: ProvableSettlementHook[]; escapeHatchSlotsInterval: number; - signedSettlements: boolean | undefined; + signedSettlements: boolean; ChildVerificationKeyService: ChildVerificationKeyService; } +export const SettlementContractArgsSchema: NaiveObjectSchema = + { + ChildVerificationKeyService: "Required", + hooks: "Required", + escapeHatchSlotsInterval: "Required", + signedSettlements: "Required", + }; + export abstract class SettlementBase extends TokenContract implements StaticInitializationContract @@ -89,7 +98,7 @@ export abstract class SettlementBase getInitializationArgs(): SettlementContractArgs { return container .resolve(ContractArgsRegistry) - .getArgs("SettlementContract")!; + .getArgs("SettlementContract", SettlementContractArgsSchema); } abstract sequencerKey: State; diff --git a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts index 61d2de0c6..2e14928d8 100644 --- a/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts +++ b/packages/sequencer/src/protocol/production/tasks/CircuitCompilerTask.ts @@ -154,20 +154,17 @@ export class CircuitCompilerTask extends UnpreparingTask< } if (input.isSignedSettlement !== undefined) { - const contractArgs = - this.contractArgsRegistry.getArgs( - "SettlementContract" - ); - const newArgs = { - ...contractArgs, - signedSettlements: input.isSignedSettlement, - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (input.isSignedSettlement - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }; - this.contractArgsRegistry.setArgs("SettlementContract", newArgs); + 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 b3fec37ff..603835f7a 100644 --- a/packages/sequencer/src/sequencer/SequencerStartupModule.ts +++ b/packages/sequencer/src/sequencer/SequencerStartupModule.ts @@ -171,11 +171,12 @@ export class SequencerStartupModule const bridgeVk = protocolBridgeArtifacts.BridgeContract; if (bridgeVk !== undefined) { // TODO Inject CompileRegistry directly - const args = - this.contractArgsRegistry.getArgs( - "SettlementContract" - )!; - args.BridgeContractVerificationKey = bridgeVk.verificationKey; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { + BridgeContractVerificationKey: bridgeVk.verificationKey, + } + ); } await this.registrationFlow.start({ diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index 0f1d74fe0..1f5afaeb1 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -660,19 +660,16 @@ export class BridgingModule extends SequencerModule { } public async start(): Promise { - const contractArgs = - this.argsRegistry.getArgs( - "SettlementContract" - ); - - this.argsRegistry.setArgs("SettlementContract", { - ...contractArgs, - // TODO Add distinction between mina and custom tokens - BridgeContractPermissions: (this.baseLayer.isSignedSettlement() - ? new SignedSettlementPermissions() - : new ProvenSettlementPermissions() - ).bridgeContractMina(), - }); + 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) { diff --git a/packages/sequencer/src/settlement/SettlementModule.ts b/packages/sequencer/src/settlement/SettlementModule.ts index a2004d45a..4ab3439d3 100644 --- a/packages/sequencer/src/settlement/SettlementModule.ts +++ b/packages/sequencer/src/settlement/SettlementModule.ts @@ -131,10 +131,10 @@ export class SettlementModule SettlementContractModule >("SettlementContractModule"); - const contracts = settlementContractModule.createContracts({ - SettlementContract: address, - }); - this.contract = contracts.SettlementContract; + this.contract = settlementContractModule.createContract( + "SettlementContract", + address + ); } return this.contract; } @@ -191,11 +191,7 @@ export class SettlementModule } public async start(): Promise { - const contractArgs = - this.argsRegistry.getArgs("SettlementContract"); - - this.argsRegistry.setArgs("SettlementContract", { - ...contractArgs, + this.argsRegistry.addArgs("SettlementContract", { signedSettlements: this.baseLayer.isSignedSettlement(), }); diff --git a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts index 1ff4c0373..30339d0fc 100644 --- a/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts +++ b/packages/sequencer/src/worker/worker/startup/WorkerRegistrationTask.ts @@ -88,27 +88,24 @@ export class WorkerRegistrationTask } if (input.bridgeContractVerificationKey !== undefined) { - const args = - this.contractArgsRegistry.getArgs( - "SettlementContract" - )!; - args.BridgeContractVerificationKey = input.bridgeContractVerificationKey; + this.contractArgsRegistry.addArgs( + "SettlementContract", + { BridgeContractVerificationKey: input.bridgeContractVerificationKey } + ); } if (input.isSignedSettlement !== undefined) { - const contractArgs = - this.contractArgsRegistry.getArgs( - "SettlementContract" - )!; - this.contractArgsRegistry.setArgs("SettlementContract", { - ...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/settlement/Settlement.ts b/packages/sequencer/test/settlement/Settlement.ts index b972f0286..f0012e9cc 100644 --- a/packages/sequencer/test/settlement/Settlement.ts +++ b/packages/sequencer/test/settlement/Settlement.ts @@ -308,9 +308,7 @@ export const settlementTestFn = ( }, timeout * 3); afterAll(async () => { - container - .resolve(ContractArgsRegistry) - .setArgs("SettlementContract", undefined); + container.resolve(ContractArgsRegistry).resetArgs("SettlementContract"); await appChain.close(); }); From d7dc1019da16ee2c8c650a9a044015b6e5239c52 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 18 Dec 2025 12:26:32 +0100 Subject: [PATCH 06/12] Moved bridge addresses to registry --- .../src/settlement/BridgingModule.ts | 32 +++++++++++-------- .../interactions/AddressRegistry.ts | 12 +++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index 1f5afaeb1..25ef3fcbb 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -92,13 +92,11 @@ export type BridgingModuleConfig = { */ @injectable() 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; @@ -184,18 +182,20 @@ export class BridgingModule extends SequencerModule { .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, }; } @@ -268,14 +268,18 @@ export class BridgingModule extends SequencerModule { 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) { diff --git a/packages/sequencer/src/settlement/interactions/AddressRegistry.ts b/packages/sequencer/src/settlement/interactions/AddressRegistry.ts index cf88cc90d..449f345cd 100644 --- a/packages/sequencer/src/settlement/interactions/AddressRegistry.ts +++ b/packages/sequencer/src/settlement/interactions/AddressRegistry.ts @@ -1,14 +1,22 @@ 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; } @@ -16,4 +24,8 @@ export class InMemoryAddressRegistry implements AddressRegistry { getContractAddress(identifier: string): PublicKey { return this.addresses[identifier]; } + + hasContractAddress(identifier: string): boolean { + return this.addresses[identifier] !== undefined; + } } From 33dbd45e455dfc987c48a8f6cb1b971c09a58a70 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 18 Dec 2025 12:28:42 +0100 Subject: [PATCH 07/12] Removed not planned todo --- .../interactions/bridging/BridgingDeployInteraction.ts | 1 - .../interactions/bridging/BridgingSettlementInteraction.ts | 1 - .../settlement/interactions/vanilla/VanillaDeployInteraction.ts | 1 - .../interactions/vanilla/VanillaSettlementInteraction.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts index 174bd7453..c1743ae09 100644 --- a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts @@ -61,7 +61,6 @@ export class BridgingDeployInteraction implements DeployInteraction { const nonce = options?.nonce ?? 0; - // TODO Move this workflow to AddressRegistry const sm = this.settlementContractModule(); const { SettlementContract: settlementContract, diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts index 62b34c106..ee8b74983 100644 --- a/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingSettlementInteraction.ts @@ -62,7 +62,6 @@ export class BridgingSettlementInteraction implements SettleInteraction { ); } - // TODO Move this workflow to AddressRegistry const sm = this.settlementContractModule(); const { SettlementContract: settlementContract, diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts index 382699478..0d5cec0d6 100644 --- a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts @@ -64,7 +64,6 @@ export class VanillaDeployInteraction implements DeployInteraction { const nonce = options?.nonce ?? 0; - // TODO Move this workflow to AddressRegistry const sm = this.settlementContractModule(); const { SettlementContract: settlementContract } = sm.createContracts({ SettlementContract: settlementKey, diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts index 35f3ebb53..6f54961a6 100644 --- a/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts @@ -57,7 +57,6 @@ export class VanillaSettlementInteraction implements SettleInteraction { throw new Error("Settlement addresses haven't been initialized"); } - // TODO Move this workflow to AddressRegistry const sm = this.settlementContractModule(); const { SettlementContract: settlementContract } = sm.createContracts({ SettlementContract: settlementKey, From 43143373d07212b2de3ba902303e80171d84decd Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 18 Dec 2025 13:21:35 +0100 Subject: [PATCH 08/12] Moved other contracts to ContractArgsRegistry architecture --- .../settlement/contracts/BridgeContract.ts | 54 +++++++++++++------ .../contracts/BridgeContractProtocolModule.ts | 10 ++-- .../BridgingSettlementContractModule.ts | 31 +++++------ .../DispatchContractProtocolModule.ts | 23 ++++---- .../contracts/DispatchSmartContract.ts | 52 ++++++++++++------ .../SettlementSmartContractModule.ts | 2 +- .../sequencer/test/integration/Proven.test.ts | 25 +++++---- 7 files changed, 121 insertions(+), 76 deletions(-) diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index 802bdccbf..1771bbfed 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -16,7 +16,7 @@ import { TokenId, VerificationKey, } from "o1js"; -import { noop, range, TypedClass } from "@proto-kit/common"; +import { batch, noop, range, TypedClass } from "@proto-kit/common"; import { container, injectable, singleton } from "tsyringe"; import { @@ -30,6 +30,12 @@ import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor import { PROTOKIT_FIELD_PREFIXES } from "../../hashing/protokit-prefixes"; import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../ContractArgsRegistry"; +import { hash } from "node:crypto"; export type BridgeContractType = { stateRoot: State; @@ -64,19 +70,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 +154,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 +167,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..33582903d 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts @@ -6,9 +6,11 @@ import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor import { BridgeContract, + BridgeContractArgs, BridgeContractBase, BridgeContractType, } from "./BridgeContract"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; export type BridgeContractConfig = { outgoingBatchSize?: number; @@ -21,7 +23,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 +32,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/BridgingSettlementContractModule.ts b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts index bdd8a461b..430fde6af 100644 --- a/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts @@ -14,14 +14,17 @@ import { import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; import { ContractArgsRegistry } from "../ContractArgsRegistry"; -import { DispatchSmartContractBase } from "./DispatchSmartContract"; +import { + DispatchContractArgs, + DispatchSmartContractBase, +} from "./DispatchSmartContract"; import { BridgingSettlementContractType, BridgingSettlementContract, BridgingSettlementContractArgs, BridgingSettlementContractArgsSchema, } from "./settlement/BridgingSettlementContract"; -import { BridgeContractBase } from "./BridgeContract"; +import { BridgeContractArgs, BridgeContractBase } from "./BridgeContract"; import { DispatchContractProtocolModule } from "./DispatchContractProtocolModule"; import { BridgeContractProtocolModule } from "./BridgeContractProtocolModule"; import { @@ -69,12 +72,14 @@ export class BridgingSettlementContractModule extends ContractModule< } ); - // 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 = BridgingSettlementContract; - - DispatchSmartContractBase.args.settlementContractClass = - BridgingSettlementContract; + // 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 BridgingSettlementContract; } @@ -98,16 +103,6 @@ export class BridgingSettlementContractModule extends ContractModule< } ); - const args = this.argsRegistry.getArgs( - "SettlementContract", - BridgingSettlementContractArgsSchema - ); - if (args.signedSettlements === undefined) { - throw new Error( - "Args not fully initialized - make sure to also include the SettlementModule in the sequencer" - ); - } - log.debug("Compiling Settlement Contract"); const artifact = await registry.forceProverExists( diff --git a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts index 5b3663a82..6a27e25de 100644 --- a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts @@ -7,11 +7,13 @@ import { ContractModule, SmartContractClassFromInterface, } from "../ContractModule"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { DispatchSmartContract, DispatchContractType, DispatchSmartContractBase, + DispatchContractArgs, } from "./DispatchSmartContract"; export type DispatchContractConfig = { @@ -23,7 +25,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 +57,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 f79b77d37..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,6 +26,11 @@ import { MinaEvents, } from "../../utils/MinaPrefixedProvableHashList"; import { Deposit } from "../messages/Deposit"; +import { + ContractArgsRegistry, + NaiveObjectSchema, + StaticInitializationContract, +} from "../ContractArgsRegistry"; import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; import { TokenBridgeDeploymentAuth } from "./authorizations/TokenBridgeDeploymentAuth"; @@ -67,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 @@ -93,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 @@ -109,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({ @@ -179,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 @@ -322,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/SettlementSmartContractModule.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts index e93a28eaa..817f03e3d 100644 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts @@ -56,7 +56,7 @@ export class SettlementSmartContractModule extends ContractModule< ChildVerificationKeyService: this.childVerificationKeyService, }); - return BridgingSettlementContract; + return SettlementContract; } public async compile( diff --git a/packages/sequencer/test/integration/Proven.test.ts b/packages/sequencer/test/integration/Proven.test.ts index 86ad46bd0..6413ac2ff 100644 --- a/packages/sequencer/test/integration/Proven.test.ts +++ b/packages/sequencer/test/integration/Proven.test.ts @@ -10,6 +10,7 @@ import { Runtime } from "@proto-kit/module"; import { BridgeContract, BridgingSettlementContract, + BridgingSettlementContractArgs, ContractArgsRegistry, DispatchSmartContract, Protocol, @@ -171,17 +172,19 @@ describe.skip("Proven", () => { }); vkService.setCompileRegistry(registry); - container.resolve(ContractArgsRegistry).setArgs("SettlementContract", { - DispatchContract: DispatchSmartContract, - ChildVerificationKeyService: vkService, - BridgeContractVerificationKey: MOCK_VERIFICATION_KEY, - signedSettlements: false, - BridgeContract: BridgeContract, - hooks: [], - BridgeContractPermissions: - new ProvenSettlementPermissions().bridgeContractMina(), - escapeHatchSlotsInterval: 1000, - }); + 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) { From 12381f17709a12cce1c819a7d2c2d8740aa7b71c Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 18 Dec 2025 13:23:13 +0100 Subject: [PATCH 09/12] Fix linting errors --- .../protocol/src/settlement/contracts/BridgeContract.ts | 7 +++---- .../settlement/contracts/BridgeContractProtocolModule.ts | 3 +-- .../contracts/BridgingSettlementContractModule.ts | 8 ++------ .../contracts/DispatchContractProtocolModule.ts | 1 - .../settlement/contracts/SettlementSmartContractModule.ts | 1 - .../contracts/settlement/BridgingSettlementContract.ts | 1 - 6 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index 1771bbfed..f56f8ec95 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -16,7 +16,7 @@ import { TokenId, VerificationKey, } from "o1js"; -import { batch, noop, range, TypedClass } from "@proto-kit/common"; +import { noop, range, TypedClass } from "@proto-kit/common"; import { container, injectable, singleton } from "tsyringe"; import { @@ -28,14 +28,13 @@ import { import { Path } from "../../model/Path"; import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor"; import { PROTOKIT_FIELD_PREFIXES } from "../../hashing/protokit-prefixes"; - -import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; import { ContractArgsRegistry, NaiveObjectSchema, StaticInitializationContract, } from "../ContractArgsRegistry"; -import { hash } from "node:crypto"; + +import type { BridgingSettlementContractType } from "./settlement/BridgingSettlementContract"; export type BridgeContractType = { stateRoot: State; diff --git a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts index 33582903d..e4537d242 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContractProtocolModule.ts @@ -3,14 +3,13 @@ import { CompileRegistry } from "@proto-kit/common"; import { ContractModule } from "../ContractModule"; import { OutgoingMessageProcessor } from "../modularity/OutgoingMessageProcessor"; +import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { BridgeContract, BridgeContractArgs, - BridgeContractBase, BridgeContractType, } from "./BridgeContract"; -import { ContractArgsRegistry } from "../ContractArgsRegistry"; export type BridgeContractConfig = { outgoingBatchSize?: number; diff --git a/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts index 430fde6af..9d1fef1ed 100644 --- a/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts +++ b/packages/protocol/src/settlement/contracts/BridgingSettlementContractModule.ts @@ -14,17 +14,13 @@ import { import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; import { ContractArgsRegistry } from "../ContractArgsRegistry"; -import { - DispatchContractArgs, - DispatchSmartContractBase, -} from "./DispatchSmartContract"; +import { DispatchContractArgs } from "./DispatchSmartContract"; import { BridgingSettlementContractType, BridgingSettlementContract, BridgingSettlementContractArgs, - BridgingSettlementContractArgsSchema, } from "./settlement/BridgingSettlementContract"; -import { BridgeContractArgs, BridgeContractBase } from "./BridgeContract"; +import { BridgeContractArgs } from "./BridgeContract"; import { DispatchContractProtocolModule } from "./DispatchContractProtocolModule"; import { BridgeContractProtocolModule } from "./BridgeContractProtocolModule"; import { diff --git a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts index 6a27e25de..f421d9f1e 100644 --- a/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts +++ b/packages/protocol/src/settlement/contracts/DispatchContractProtocolModule.ts @@ -12,7 +12,6 @@ import { ContractArgsRegistry } from "../ContractArgsRegistry"; import { DispatchSmartContract, DispatchContractType, - DispatchSmartContractBase, DispatchContractArgs, } from "./DispatchSmartContract"; diff --git a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts index 817f03e3d..c776e5ff5 100644 --- a/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts +++ b/packages/protocol/src/settlement/contracts/SettlementSmartContractModule.ts @@ -14,7 +14,6 @@ import { import { ProvableSettlementHook } from "../modularity/ProvableSettlementHook"; import { ContractArgsRegistry } from "../ContractArgsRegistry"; -import { BridgingSettlementContract } from "./settlement/BridgingSettlementContract"; import { SettlementContractArgs, SettlementContractType, diff --git a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts index 7610f86d5..4865e4ec5 100644 --- a/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts +++ b/packages/protocol/src/settlement/contracts/settlement/BridgingSettlementContract.ts @@ -14,7 +14,6 @@ import { VerificationKey, Permissions, Struct, - Provable, TokenId, DeployArgs, } from "o1js"; From a6a6a2b9f7cd771c223e9125881e9d1780f8b2d2 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 18 Dec 2025 15:04:54 +0100 Subject: [PATCH 10/12] Added settlement-only test --- .../src/protocol/baselayer/MinaBaseLayer.ts | 12 - .../src/sequencer/SettlementStartupModule.ts | 37 ++- .../src/settlement/BridgingModule.ts | 12 + .../bridging/BridgingDeployInteraction.ts | 7 +- .../vanilla/VanillaDeployInteraction.ts | 8 +- .../vanilla/VanillaSettlementInteraction.ts | 3 +- .../messages/IncomingMessagesService.ts | 2 +- .../test/settlement/Settlement-only.ts | 306 ++++++++++++++++++ .../test/settlement/Settlement.test.ts | 5 + 9 files changed, 354 insertions(+), 38 deletions(-) create mode 100644 packages/sequencer/test/settlement/Settlement-only.ts 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/sequencer/SettlementStartupModule.ts b/packages/sequencer/src/sequencer/SettlementStartupModule.ts index be3827bbd..1936fb3c1 100644 --- a/packages/sequencer/src/sequencer/SettlementStartupModule.ts +++ b/packages/sequencer/src/sequencer/SettlementStartupModule.ts @@ -39,39 +39,38 @@ export class SettlementStartupModule { return artifacts; } - private async getArtifacts(retry: boolean): Promise<{ - SettlementSmartContract: CompileArtifact; - DispatchSmartContract: CompileArtifact; - }> { - const settlementVerificationKey = - this.compileRegistry.getArtifact("SettlementContract"); - 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 25ef3fcbb..4c13102bc 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -36,6 +36,7 @@ import { UInt32, } from "o1js"; import { + DependencyRecord, filterNonUndefined, LinkedMerkleTree, log, @@ -65,6 +66,7 @@ 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, @@ -126,6 +128,14 @@ export class BridgingModule extends SequencerModule { this.utils = new SettlementUtils(baseLayer, signer); } + public dependencies() { + return { + IncomingMessagesService: { + useClass: IncomingMessagesService, + }, + } satisfies DependencyRecord; + } + public getDispatchContract() { if (this.dispatchContract === undefined) { const address = this.getDispatchContractAddress(); @@ -685,3 +695,5 @@ export class BridgingModule extends SequencerModule { } /* eslint-enable no-await-in-loop */ } + +// BridgingModule satisfies DependencyFactory; diff --git a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts index c1743ae09..c1c24d4a4 100644 --- a/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/bridging/BridgingDeployInteraction.ts @@ -71,7 +71,10 @@ export class BridgingDeployInteraction implements DeployInteraction { }); const verificationsKeys = - await this.settlementStartupModule.retrieveVerificationKeys(); + await this.settlementStartupModule.retrieveVerificationKeys({ + SettlementContract: true, + DispatchSmartContract: true, + }); const utils = new SettlementUtils(this.baseLayer, this.signer); await utils.fetchContractAccounts(settlementContract, dispatchContract); @@ -102,7 +105,7 @@ export class BridgingDeployInteraction implements DeployInteraction { await settlementContract.deployAndInitialize( { verificationKey: - verificationsKeys.SettlementSmartContract.verificationKey, + verificationsKeys.SettlementContract.verificationKey, }, permissions.settlementContract(), feepayer, diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts index 0d5cec0d6..c5fb126fe 100644 --- a/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaDeployInteraction.ts @@ -73,7 +73,9 @@ export class VanillaDeployInteraction implements DeployInteraction { await utils.fetchContractAccounts(settlementContract); const verificationsKeys = - await this.settlementStartupModule.retrieveVerificationKeys(); + await this.settlementStartupModule.retrieveVerificationKeys({ + SettlementContract: true, + }); const permissions = this.baseLayer.isSignedSettlement() ? new SignedSettlementPermissions() @@ -87,12 +89,12 @@ export class VanillaDeployInteraction implements DeployInteraction { memo: "Protokit settlement deploy", }, async () => { - AccountUpdate.fundNewAccount(feepayer, 2); + AccountUpdate.fundNewAccount(feepayer, 1); await settlementContract.deployAndInitialize( { verificationKey: - verificationsKeys.SettlementSmartContract.verificationKey, + verificationsKeys.SettlementContract.verificationKey, }, permissions.settlementContract(), feepayer, diff --git a/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts index 6f54961a6..096438ab5 100644 --- a/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts +++ b/packages/sequencer/src/settlement/interactions/vanilla/VanillaSettlementInteraction.ts @@ -1,4 +1,4 @@ -import { inject } from "tsyringe"; +import { inject, injectable } from "tsyringe"; import { Field, Mina } from "o1js"; import { BATCH_SIGNATURE_PREFIX, @@ -21,6 +21,7 @@ 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") diff --git a/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts b/packages/sequencer/src/settlement/messages/IncomingMessagesService.ts index a7aef8bd7..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 { BridgingModule } from "../BridgingModule"; +import type { BridgingModule } from "../BridgingModule"; import { IncomingMessageAdapter } from "./IncomingMessageAdapter"; diff --git a/packages/sequencer/test/settlement/Settlement-only.ts b/packages/sequencer/test/settlement/Settlement-only.ts new file mode 100644 index 000000000..87a4e952d --- /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, batch] = 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); + }); } ); From 4fd61f27476044c8633878f9c71fe2edf3ecf1b6 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 18 Dec 2025 15:35:40 +0100 Subject: [PATCH 11/12] Fixed dependency issue --- packages/sequencer/src/settlement/BridgingModule.ts | 2 +- packages/sequencer/test/settlement/Settlement-only.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index 4c13102bc..a575c5e3a 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -128,7 +128,7 @@ export class BridgingModule extends SequencerModule { this.utils = new SettlementUtils(baseLayer, signer); } - public dependencies() { + public static dependencies() { return { IncomingMessagesService: { useClass: IncomingMessagesService, diff --git a/packages/sequencer/test/settlement/Settlement-only.ts b/packages/sequencer/test/settlement/Settlement-only.ts index 87a4e952d..6fe5d8dad 100644 --- a/packages/sequencer/test/settlement/Settlement-only.ts +++ b/packages/sequencer/test/settlement/Settlement-only.ts @@ -179,7 +179,7 @@ export const settlementOnlyTestFn = ( }); const result = await trigger.produceBlockAndBatch(); - const [block, batch] = result; + const [block] = result; console.log( `block ${block?.height.toString()} ${block?.fromMessagesHash.toString()} -> ${block?.toMessagesHash.toString()}` From 6e792274963454ba23d9149166a487e098b7ccc4 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 18 Dec 2025 15:48:59 +0100 Subject: [PATCH 12/12] Fixed Proven test again --- packages/sequencer/test/integration/Proven.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sequencer/test/integration/Proven.test.ts b/packages/sequencer/test/integration/Proven.test.ts index 6413ac2ff..0526d93d2 100644 --- a/packages/sequencer/test/integration/Proven.test.ts +++ b/packages/sequencer/test/integration/Proven.test.ts @@ -151,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 () => {