diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index ffa3e5139..76083a87b 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -255,3 +255,24 @@ export function batch( return range(0, numBatches).map((i) => partitioned[i].map((x) => x[0])); } + +export type Reference = { + set value(value: T); + get value(): T; +}; + +class ReferenceObject { + public constructor(private internalValue: T) {} + + get value() { + return this.internalValue; + } + + set value(t: T) { + this.internalValue = t; + } +} + +export function createReference(initial: T): Reference { + return new ReferenceObject(initial); +} diff --git a/packages/library/src/runtime/Balances.ts b/packages/library/src/runtime/Balances.ts index bffeae88b..716c010fd 100644 --- a/packages/library/src/runtime/Balances.ts +++ b/packages/library/src/runtime/Balances.ts @@ -1,11 +1,6 @@ import { EventsRecord, NoConfig } from "@proto-kit/common"; -import { - RuntimeModule, - runtimeMethod, - state, - runtimeModule, -} from "@proto-kit/module"; -import { StateMap, assert } from "@proto-kit/protocol"; +import { RuntimeModule, runtimeMethod, runtimeModule } from "@proto-kit/module"; +import { StateMap, assert, state } from "@proto-kit/protocol"; import { Field, PublicKey, Struct, Provable } from "o1js"; import { UInt64 } from "../math/UInt64"; diff --git a/packages/library/src/runtime/Withdrawals.ts b/packages/library/src/runtime/Withdrawals.ts index 95b88a827..865d7fe50 100644 --- a/packages/library/src/runtime/Withdrawals.ts +++ b/packages/library/src/runtime/Withdrawals.ts @@ -1,10 +1,5 @@ -import { - RuntimeEvents, - runtimeModule, - RuntimeModule, - state, -} from "@proto-kit/module"; -import { StateMap, Withdrawal } from "@proto-kit/protocol"; +import { RuntimeEvents, runtimeModule, RuntimeModule } from "@proto-kit/module"; +import { StateMap, Withdrawal, state } from "@proto-kit/protocol"; import { Field, PublicKey, Struct } from "o1js"; import { inject } from "tsyringe"; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 38213f46e..e2d5c40ba 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -4,7 +4,6 @@ export * from "./runtime/RuntimeModule"; export * from "./runtime/RuntimeEnvironment"; export * from "./runtime/Runtime"; export * from "./state/InMemoryStateService"; -export * from "./state/decorator"; export * from "./method/MethodParameterEncoder"; export * from "./runtime/MethodIdResolver"; export * from "./factories/MethodIdFactory"; diff --git a/packages/module/src/method/runtimeMethod.ts b/packages/module/src/method/runtimeMethod.ts index 399a0f035..010543b80 100644 --- a/packages/module/src/method/runtimeMethod.ts +++ b/packages/module/src/method/runtimeMethod.ts @@ -95,7 +95,7 @@ export function toWrappedMethod( const stateTransitionsHash = toStateTransitionsHash(stateTransitions); const eventsHash = toEventsHash(events); - const { name, runtime } = this; + const { name, parent: runtime } = this; if (name === undefined) { throw errors.runtimeNameNotSet(); @@ -275,10 +275,10 @@ function runtimeMethodInternal(options: { executionContext.beforeMethod(constructorName, methodName, args); if (executionContext.isTopLevel) { - if (!this.runtime) { + if (!this.parent) { throw errors.runtimeNotProvided(constructorName); } - executionContext.setProver(prover.bind(this.runtime.zkProgrammable)); + executionContext.setProver(prover.bind(this.parent.zkProgrammable)); } let result: unknown; diff --git a/packages/module/src/runtime/Runtime.ts b/packages/module/src/runtime/Runtime.ts index 264c57e8f..e38b8f59a 100644 --- a/packages/module/src/runtime/Runtime.ts +++ b/packages/module/src/runtime/Runtime.ts @@ -371,7 +371,7 @@ export class Runtime containedModule: InstanceType]> ) { containedModule.name = moduleName; - containedModule.runtime = this; + containedModule.parent = this; super.decorateModule(moduleName, containedModule); } diff --git a/packages/module/src/runtime/RuntimeModule.ts b/packages/module/src/runtime/RuntimeModule.ts index 9b725dd8d..d6dd4ceb9 100644 --- a/packages/module/src/runtime/RuntimeModule.ts +++ b/packages/module/src/runtime/RuntimeModule.ts @@ -75,7 +75,7 @@ export class RuntimeModule< public name?: string; - public runtime?: RuntimeEnvironment; + public parent?: RuntimeEnvironment; public events?: RuntimeEvents = undefined; diff --git a/packages/module/src/state/decorator.ts b/packages/module/src/state/decorator.ts deleted file mode 100644 index 65c9825f4..000000000 --- a/packages/module/src/state/decorator.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Path, State } from "@proto-kit/protocol"; - -import type { RuntimeModule } from "../runtime/RuntimeModule.js"; - -const errors = { - missingName: (className: string) => - new Error( - `Unable to provide a unique identifier for state, ${className} is missing a name. - Did you forget to extend your runtime module with 'extends RuntimeModule'?` - ), - - missingRuntime: (className: string) => - new Error( - `Unable to provide 'runtime' for state, ${className} is missing a name. - Did you forget to extend your runtime module with 'extends RuntimeModule'?` - ), -}; - -/** - * Decorates a runtime module property as state, passing down some - * underlying values to improve developer experience. - */ -export function state() { - return >( - target: TargetRuntimeModule, - propertyKey: string - ) => { - let value: State | undefined; - - Object.defineProperty(target, propertyKey, { - enumerable: true, - - get: function get() { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const self = this as TargetRuntimeModule; - - if (self.name === undefined) { - throw errors.missingName(self.constructor.name); - } - - if (!self.runtime) { - throw errors.missingRuntime(self.constructor.name); - } - - const path = Path.fromProperty(self.name, propertyKey); - if (value) { - value.path = path; - - // TODO: why is this complaining about `any`? - - value.stateServiceProvider = self.runtime.stateServiceProvider; - } - return value; - }, - - set: (newValue: State) => { - value = newValue; - }, - }); - }; -} diff --git a/packages/protocol/src/hashing/mina-prefixes.ts b/packages/protocol/src/hashing/mina-prefixes.ts new file mode 100644 index 000000000..7fca6703f --- /dev/null +++ b/packages/protocol/src/hashing/mina-prefixes.ts @@ -0,0 +1,10 @@ +export const MINA_PREFIXES = { + event: "MinaZkappEvent******", + events: "MinaZkappEvents*****", + sequenceEvents: "MinaZkappSeqEvents**", +} as const; + +export const MINA_SALTS = { + empty_actions: "MinaZkappActionsEmpty", + empty_events: "MinaZkappEventsEmpty", +}; diff --git a/packages/protocol/src/hashing/protokit-prefixes.ts b/packages/protocol/src/hashing/protokit-prefixes.ts new file mode 100644 index 000000000..7735e2fbf --- /dev/null +++ b/packages/protocol/src/hashing/protokit-prefixes.ts @@ -0,0 +1,23 @@ +import padEnd from "lodash/padEnd"; +import mapValues from "lodash/mapValues"; + +const length = 20; +function padToHashPrefix(s: string): string { + if (s.length > 20) { + throw new Error(`Prefix string ${s} is too long (max ${length})`); + } + return padEnd(s, length, "*"); +} + +function padPrefixRecord>( + record: T +): { + [Key in keyof T]: string; +} { + return mapValues(record, padToHashPrefix); +} + +export const PROTOKIT_PREFIXES = padPrefixRecord({ + STATE_PROTOCOL: "pk-protocol-state", + STATE_RUNTIME: "pk-runtime-state", +}); diff --git a/packages/protocol/src/hooks/AccountStateHook.ts b/packages/protocol/src/hooks/AccountStateHook.ts index b3aa3e9fe..3369e6683 100644 --- a/packages/protocol/src/hooks/AccountStateHook.ts +++ b/packages/protocol/src/hooks/AccountStateHook.ts @@ -3,7 +3,7 @@ import { injectable } from "tsyringe"; import { noop } from "@proto-kit/common"; import { StateMap } from "../state/StateMap"; -import { protocolState } from "../state/protocol/ProtocolState"; +import { state } from "../state/protocol/ProtocolState"; import { ProvableTransactionHook, BeforeTransactionHookArguments, @@ -16,7 +16,7 @@ export class AccountState extends Struct({ @injectable() export class AccountStateHook extends ProvableTransactionHook { - @protocolState() public accountState = StateMap.from( + @state() public accountState = StateMap.from( PublicKey, AccountState ); diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index e4419af3a..d161590a2 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -60,3 +60,5 @@ export * from "./settlement/modules/NetworkStateSettlementModule"; export * from "./settlement/messages/Deposit"; export * from "./settlement/messages/Withdrawal"; export { constants as ProtocolConstants } from "./Constants"; +export * from "./hashing/protokit-prefixes"; +export * from "./hashing/mina-prefixes"; diff --git a/packages/protocol/src/model/Path.ts b/packages/protocol/src/model/Path.ts index 8bee8cdd7..d5970a5b0 100644 --- a/packages/protocol/src/model/Path.ts +++ b/packages/protocol/src/model/Path.ts @@ -1,4 +1,5 @@ import { Field, type FlexibleProvablePure, Poseidon } from "o1js"; +import { hashWithPrefix } from "@proto-kit/common"; import { stringToField } from "../utils/utils"; @@ -21,10 +22,15 @@ export class Path { * * @param className * @param propertyKey + * @param prefix * @returns Field representation of class name + property name */ - public static fromProperty(className: string, propertyKey: string): Field { - return Poseidon.hash([ + public static fromProperty( + className: string, + propertyKey: string, + prefix: string + ): Field { + return hashWithPrefix(prefix, [ Path.toField(className), Path.toField(propertyKey), Field(0), diff --git a/packages/protocol/src/protocol/Protocol.ts b/packages/protocol/src/protocol/Protocol.ts index f9ebb6fdc..a6a62444f 100644 --- a/packages/protocol/src/protocol/Protocol.ts +++ b/packages/protocol/src/protocol/Protocol.ts @@ -93,7 +93,7 @@ export class Protocol< containedModule: InstanceType]> ) { log.debug(`Decorated ${moduleName}`); - containedModule.protocol = this; + containedModule.parent = this; if (containedModule instanceof TransitioningProtocolModule) { containedModule.name = moduleName; diff --git a/packages/protocol/src/protocol/ProtocolModule.ts b/packages/protocol/src/protocol/ProtocolModule.ts index 18c3c4877..fc220f078 100644 --- a/packages/protocol/src/protocol/ProtocolModule.ts +++ b/packages/protocol/src/protocol/ProtocolModule.ts @@ -11,10 +11,10 @@ import { ProtocolEnvironment } from "./ProtocolEnvironment"; export abstract class ProtocolModule< Config = NoConfig, > extends ConfigurableModule { - public protocol?: ProtocolEnvironment; + public parent?: ProtocolEnvironment; public get areProofsEnabled(): AreProofsEnabled | undefined { - return this.protocol?.getAreProofsEnabled(); + return this.parent?.getAreProofsEnabled(); } public create(childContainerProvider: ChildContainerProvider): void { diff --git a/packages/protocol/src/protocol/ProvableBlockHook.ts b/packages/protocol/src/protocol/ProvableBlockHook.ts index c28a46a04..b5381e193 100644 --- a/packages/protocol/src/protocol/ProvableBlockHook.ts +++ b/packages/protocol/src/protocol/ProvableBlockHook.ts @@ -1,4 +1,5 @@ import { Field } from "o1js"; +import { NoConfig } from "@proto-kit/common"; import { NetworkState } from "../model/network/NetworkState"; import { MethodPublicOutput } from "../model/MethodPublicOutput"; @@ -53,7 +54,7 @@ export function toAfterTransactionHookArgument( // Purpose is to build transition from -> to network state export abstract class ProvableBlockHook< - Config, + Config = NoConfig, > extends TransitioningProtocolModule { public abstract beforeBlock( networkState: NetworkState, diff --git a/packages/protocol/src/settlement/contracts/BridgeContract.ts b/packages/protocol/src/settlement/contracts/BridgeContract.ts index 48fbacda7..55048889a 100644 --- a/packages/protocol/src/settlement/contracts/BridgeContract.ts +++ b/packages/protocol/src/settlement/contracts/BridgeContract.ts @@ -23,6 +23,7 @@ import { } from "../messages/OutgoingMessageArgument"; import { Path } from "../../model/Path"; import { Withdrawal } from "../messages/Withdrawal"; +import { PROTOKIT_PREFIXES } from "../../hashing/protokit-prefixes"; import type { SettlementContractType } from "./SettlementSmartContract"; @@ -137,7 +138,11 @@ export abstract class BridgeContractBase extends TokenContractV2 { const [withdrawalModule, withdrawalStateName] = BridgeContractBase.args.withdrawalStatePath; - const mapPath = Path.fromProperty(withdrawalModule, withdrawalStateName); + const mapPath = Path.fromProperty( + withdrawalModule, + withdrawalStateName, + PROTOKIT_PREFIXES.STATE_RUNTIME + ); // Count account creation fee to return later, so that the sender can fund // those accounts with a separate AU diff --git a/packages/protocol/src/state/protocol/ProtocolState.ts b/packages/protocol/src/state/protocol/ProtocolState.ts index ba9b1325b..0e0c24cca 100644 --- a/packages/protocol/src/state/protocol/ProtocolState.ts +++ b/packages/protocol/src/state/protocol/ProtocolState.ts @@ -1,5 +1,9 @@ +import { createReference, Reference } from "@proto-kit/common"; + import { State } from "../State"; import { Path } from "../../model/Path"; +import { StateServiceProvider } from "../StateServiceProvider"; +import { PROTOKIT_PREFIXES } from "../../hashing/protokit-prefixes"; import { TransitioningProtocolModule } from "../../protocol/TransitioningProtocolModule"; const errors = { @@ -9,52 +13,89 @@ const errors = { Did you forget to extend your block module with 'extends ...Hook'?` ), - missingProtocol: (className: string) => + missingParent: (className: string, type: string, moduleType: string) => new Error( - `Unable to provide 'procotol' for state, ${className} is missing a name. - Did you forget to extend your block module with 'extends ...Hook'?` + `Unable to provide parent '${type}' for state, ${className} is missing a name. + Did you forget to extend your module with 'extends ${moduleType}'?` ), }; +export interface StatefulModule { + name?: string; + parent?: { + stateServiceProvider: StateServiceProvider; + }; +} + +export function createStateGetter( + target: TargetModule, + propertyKey: string, + valueReference: Reference | undefined>, + prefix: string, + debugInfo: { parentName: string; baseModuleNames: string } +) { + return () => { + const { value } = valueReference; + // Short-circuit this to return the state in case its already initialized + if (value !== undefined && value.path !== undefined) { + return value; + } + + if (target.name === undefined) { + throw errors.missingName(target.constructor.name); + } + + if (!target.parent) { + throw errors.missingParent( + target.constructor.name, + debugInfo.parentName, + debugInfo.baseModuleNames + ); + } + + const path = Path.fromProperty(target.name, propertyKey, prefix); + if (value) { + value.path = path; + value.stateServiceProvider = target.parent.stateServiceProvider; + } + return value; + }; +} + /** * Decorates a runtime module property as state, passing down some * underlying values to improve developer experience. */ -export function protocolState() { - return < - TargetTransitioningModule extends TransitioningProtocolModule, - >( +export function state() { + return ( target: TargetTransitioningModule, propertyKey: string ) => { - let value: State | undefined; + const stateReference = createReference | undefined>( + undefined + ); + + const isProtocol = target instanceof TransitioningProtocolModule; + const statePrefix = isProtocol + ? PROTOKIT_PREFIXES.STATE_PROTOCOL + : PROTOKIT_PREFIXES.STATE_RUNTIME; + const debugInfo = isProtocol + ? { parentName: "protocol", baseModuleNames: "...Hook" } + : { parentName: "runtime", baseModuleNames: "RuntimeModule" }; Object.defineProperty(target, propertyKey, { enumerable: true, - get: function get() { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const self = this as TargetTransitioningModule; - - if (self.name === undefined) { - throw errors.missingName(self.constructor.name); - } - - if (!self.protocol) { - throw errors.missingProtocol(self.constructor.name); - } - - // TODO Add Prefix? - const path = Path.fromProperty(self.name, propertyKey); - if (value) { - value.path = path; - value.stateServiceProvider = self.protocol.stateServiceProvider; - } - return value; - }, + get: createStateGetter( + target, + propertyKey, + stateReference, + statePrefix, + debugInfo + ), set: (newValue: State) => { - value = newValue; + stateReference.value = newValue; }, }); }; diff --git a/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts b/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts index 6b5c48c01..eaa2ff8f8 100644 --- a/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts +++ b/packages/protocol/src/utils/MinaPrefixedProvableHashList.ts @@ -1,6 +1,8 @@ import { Field, Poseidon, ProvablePure } from "o1js"; import { hashWithPrefix, prefixToField } from "@proto-kit/common"; +import { MINA_PREFIXES, MINA_SALTS } from "../hashing/mina-prefixes"; + import { ProvableHashList } from "./ProvableHashList"; function salt(prefix: string) { @@ -11,18 +13,12 @@ function salt(prefix: string) { ) as [Field, Field, Field]; } -export const MINA_EVENT_PREFIXES = { - event: "MinaZkappEvent******", - events: "MinaZkappEvents*****", - sequenceEvents: "MinaZkappSeqEvents**", -} as const; - export function emptyActions(): Field { - return salt("MinaZkappActionsEmpty")[0]; + return salt(MINA_SALTS.empty_actions)[0]; } export function emptyEvents(): Field { - return salt("MinaZkappEventsEmpty")[0]; + return salt(MINA_SALTS.empty_events)[0]; } export class MinaActions { @@ -30,8 +26,8 @@ export class MinaActions { action: Field[], previousHash: Field = emptyActions() ): Field { - const actionDataHash = hashWithPrefix(MINA_EVENT_PREFIXES.event, action); - return hashWithPrefix(MINA_EVENT_PREFIXES.sequenceEvents, [ + const actionDataHash = hashWithPrefix(MINA_PREFIXES.event, action); + return hashWithPrefix(MINA_PREFIXES.sequenceEvents, [ previousHash, actionDataHash, ]); @@ -40,11 +36,8 @@ export class MinaActions { export class MinaEvents { static eventHash(event: Field[], previousHash: Field = emptyEvents()): Field { - const actionDataHash = hashWithPrefix(MINA_EVENT_PREFIXES.event, event); - return hashWithPrefix(MINA_EVENT_PREFIXES.events, [ - previousHash, - actionDataHash, - ]); + const actionDataHash = hashWithPrefix(MINA_PREFIXES.event, event); + return hashWithPrefix(MINA_PREFIXES.events, [previousHash, actionDataHash]); } } @@ -68,6 +61,6 @@ export class MinaPrefixedProvableHashList< export class MinaActionsHashList extends MinaPrefixedProvableHashList { public constructor(internalCommitment: Field = Field(0)) { - super(Field, MINA_EVENT_PREFIXES.sequenceEvents, internalCommitment); + super(Field, MINA_PREFIXES.sequenceEvents, internalCommitment); } } diff --git a/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts b/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts index d013c9a0c..0e42fdd78 100644 --- a/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts +++ b/packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts @@ -66,13 +66,13 @@ export type BlockTrackers = Pick< function getAreProofsEnabledFromModule( module: RuntimeModule ): AreProofsEnabled { - if (module.runtime === undefined) { + if (module.parent === undefined) { throw new Error("Runtime on RuntimeModule not set"); } - if (module.runtime.areProofsEnabled === undefined) { + if (module.parent.areProofsEnabled === undefined) { throw new Error("AppChain on Runtime not set"); } - const { areProofsEnabled } = module.runtime; + const { areProofsEnabled } = module.parent; return areProofsEnabled; } diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index a1308208f..c8e2de0c5 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -12,6 +12,7 @@ import { Protocol, SettlementContractModule, TokenMapping, + PROTOKIT_PREFIXES, } from "@proto-kit/protocol"; import { AccountUpdate, @@ -362,7 +363,11 @@ export class BridgingModule extends SequencerModule { const [withdrawalModule, withdrawalStateName] = this.getBridgingModuleConfig().withdrawalStatePath.split("."); - const basePath = Path.fromProperty(withdrawalModule, withdrawalStateName); + const basePath = Path.fromProperty( + withdrawalModule, + withdrawalStateName, + PROTOKIT_PREFIXES.STATE_RUNTIME + ); // Create withdrawal batches and send them as L1 transactions for (let i = 0; i < length; i += OUTGOING_MESSAGE_BATCH_SIZE) { diff --git a/packages/stack/src/scripts/graphql/server.ts b/packages/stack/src/scripts/graphql/server.ts index 73614b714..52ebcf675 100644 --- a/packages/stack/src/scripts/graphql/server.ts +++ b/packages/stack/src/scripts/graphql/server.ts @@ -6,13 +6,8 @@ import { StateServiceQueryModule, } from "@proto-kit/sdk"; import { PrivateKey, PublicKey } from "o1js"; -import { - Runtime, - runtimeMethod, - runtimeModule, - state, -} from "@proto-kit/module"; -import { Protocol, State } from "@proto-kit/protocol"; +import { Runtime, runtimeMethod, runtimeModule } from "@proto-kit/module"; +import { Protocol, State, state } from "@proto-kit/protocol"; import { Balance, Balances,