diff --git a/bin/test-runner/w3f/codec/work-package.ts b/bin/test-runner/w3f/codec/work-package.ts index 3bc91d571..9875b5666 100644 --- a/bin/test-runner/w3f/codec/work-package.ts +++ b/bin/test-runner/w3f/codec/work-package.ts @@ -21,10 +21,10 @@ export const workPackageFromJson = json.object( }, ({ authorization, auth_code_host, auth_code_hash, authorizer_config, context, items }) => WorkPackage.create({ - authorization, + authToken: authorization, authCodeHost: auth_code_host, authCodeHash: auth_code_hash, - parametrization: authorizer_config, + authConfiguration: authorizer_config, context, items: FixedSizeArray.new(items, tryAsWorkItemsCount(items.length)), }), diff --git a/packages/jam/block/work-package.ts b/packages/jam/block/work-package.ts index 1423e5946..2a3519d5c 100644 --- a/packages/jam/block/work-package.ts +++ b/packages/jam/block/work-package.ts @@ -31,7 +31,7 @@ export const MAX_NUMBER_OF_WORK_ITEMS = 16; /** * A piece of work done within a core. * - * `P = (j ∈ Y, h ∈ NS, u ∈ H, p ∈ Y, x ∈ X, w ∈ ⟦I⟧1∶I) + * `P = (j ∈ Y, h ∈ NS, u ∈ H, f ∈ Y, x ∈ X, w ∈ ⟦I⟧1∶I) * * https://graypaper.fluffylabs.dev/#/579bd12/197000197200 */ @@ -40,8 +40,8 @@ export class WorkPackage extends WithDebug { authCodeHost: codec.u32.asOpaque(), authCodeHash: codec.bytes(HASH_SIZE).asOpaque(), context: RefineContext.Codec, - authorization: codec.blob, - parametrization: codec.blob, + authToken: codec.blob, + authConfiguration: codec.blob, items: codec.sequenceVarLen(WorkItem.Codec).convert( (x) => x, (items) => FixedSizeArray.new(items, tryAsWorkItemsCount(items.length)), @@ -49,25 +49,25 @@ export class WorkPackage extends WithDebug { }); static create({ - authorization, + authToken, authCodeHost, authCodeHash, - parametrization, + authConfiguration, context, items, }: CodecRecord) { - return new WorkPackage(authorization, authCodeHost, authCodeHash, parametrization, context, items); + return new WorkPackage(authToken, authCodeHost, authCodeHash, authConfiguration, context, items); } private constructor( /** `j`: simple blob acting as an authorization token */ - public readonly authorization: BytesBlob, + public readonly authToken: BytesBlob, /** `h`: index of the service that hosts the authorization code */ public readonly authCodeHost: ServiceId, /** `u`: authorization code hash */ public readonly authCodeHash: CodeHash, - /** `p`: authorization parametrization blob */ - public readonly parametrization: BytesBlob, + /** `f`: authorization configuration blob */ + public readonly authConfiguration: BytesBlob, /** `x`: context in which the refine function should run */ public readonly context: RefineContext, /** diff --git a/packages/jam/executor/pvm-executor.ts b/packages/jam/executor/pvm-executor.ts index 2cac1314a..45c0db4f4 100644 --- a/packages/jam/executor/pvm-executor.ts +++ b/packages/jam/executor/pvm-executor.ts @@ -57,6 +57,10 @@ export type AccumulateHostCallExternalities = { serviceExternalities: general.AccountsInfo & general.AccountsLookup & general.AccountsWrite & general.AccountsRead; }; +export type IsAuthorizedHostCallExternalities = { + fetchExternalities: general.IIsAuthorizedFetch; +}; + type OnTransferHostCallExternalities = { partialState: general.AccountsInfo & general.AccountsLookup & general.AccountsWrite & general.AccountsRead; fetchExternalities: general.IFetchExternalities; @@ -134,6 +138,17 @@ export class PvmExecutor { return accumulateHandlers.concat(generalHandlers); } + /** Prepare is-authorized host call handlers */ + private static prepareIsAuthorizedHostCalls(serviceId: ServiceId, externalities: IsAuthorizedHostCallExternalities) { + const generalHandlers: HostCallHandler[] = [ + new general.LogHostCall(serviceId), + new general.GasHostCall(serviceId), + new general.Fetch(serviceId, externalities.fetchExternalities), + ]; + + return generalHandlers; + } + /** Prepare on transfer host call handlers */ private static prepareOnTransferHostCalls(serviceId: ServiceId, externalities: OnTransferHostCallExternalities) { const generalHandlers: HostCallHandler[] = [ @@ -175,6 +190,18 @@ export class PvmExecutor { return new PvmExecutor(serviceCode, hostCallHandlers, entrypoint.REFINE, instances); } + /** A utility function that can be used to prepare is-authorized executor */ + static async createIsAuthorizedExecutor( + serviceId: ServiceId, + serviceCode: BytesBlob, + externalities: IsAuthorizedHostCallExternalities, + pvm: PvmBackend, + ) { + const hostCallHandlers = PvmExecutor.prepareIsAuthorizedHostCalls(serviceId, externalities); + const instances = await PvmExecutor.prepareBackend(pvm); + return new PvmExecutor(serviceCode, hostCallHandlers, entrypoint.IS_AUTHORIZED, instances); + } + /** A utility function that can be used to prepare accumulate executor */ static async createAccumulateExecutor( serviceId: ServiceId, diff --git a/packages/jam/in-core/fixtures/authorizer.pvm b/packages/jam/in-core/fixtures/authorizer.pvm new file mode 100644 index 000000000..6642a755d Binary files /dev/null and b/packages/jam/in-core/fixtures/authorizer.pvm differ diff --git a/packages/jam/in-core/in-core.test.ts b/packages/jam/in-core/in-core.test.ts index 397e23c97..ba4bccbe1 100644 --- a/packages/jam/in-core/in-core.test.ts +++ b/packages/jam/in-core/in-core.test.ts @@ -1,6 +1,8 @@ import assert from "node:assert"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { before, describe, it } from "node:test"; -import type { HeaderHash, StateRootHash } from "@typeberry/block"; +import type { CodeHash, HeaderHash, StateRootHash } from "@typeberry/block"; import { tryAsCoreIndex, tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/block"; import type { WorkPackageHash } from "@typeberry/block/refine-context.js"; import { RefineContext } from "@typeberry/block/refine-context.js"; @@ -8,24 +10,54 @@ import { WorkItem } from "@typeberry/block/work-item.js"; import { tryAsWorkItemsCount, WorkPackage } from "@typeberry/block/work-package.js"; import { Bytes, BytesBlob } from "@typeberry/bytes"; import { Encoder } from "@typeberry/codec"; -import { asKnownSize, FixedSizeArray } from "@typeberry/collections"; +import { asKnownSize, FixedSizeArray, HashDictionary } from "@typeberry/collections"; import { type ChainSpec, PvmBackend, tinyChainSpec } from "@typeberry/config"; import { InMemoryStates } from "@typeberry/database"; -import { Blake2b, HASH_SIZE, WithHash } from "@typeberry/hash"; -import { tryAsU16 } from "@typeberry/numbers"; -import { testState } from "@typeberry/state/test.utils.js"; +import { Blake2b, HASH_SIZE, type OpaqueHash, WithHash } from "@typeberry/hash"; +import { tryAsU16, tryAsU32, tryAsU64 } from "@typeberry/numbers"; +import { InMemoryService, InMemoryState, PreimageItem, ServiceAccountInfo } from "@typeberry/state"; import { InCore, RefineError } from "./in-core.js"; +// Load the authorizer PVM fixture (checks authToken === authConfiguration). +const AUTHORIZER_PVM = BytesBlob.blobFrom(readFileSync(resolve(import.meta.dirname, "fixtures/authorizer.pvm"))); +const AUTH_SERVICE_ID = tryAsServiceId(1); + let blake2b: Blake2b; before(async () => { blake2b = await Blake2b.createHasher(); }); -function createWorkItem(serviceId = 1) { +function getAuthCodeHash() { + return blake2b.hashBytes(AUTHORIZER_PVM).asOpaque(); +} + +function createService(serviceId: typeof AUTH_SERVICE_ID, codeHash: OpaqueHash, code: BytesBlob): InMemoryService { + return new InMemoryService(serviceId, { + info: ServiceAccountInfo.create({ + codeHash: codeHash.asOpaque(), + balance: tryAsU64(10_000_000_000), + accumulateMinGas: tryAsServiceGas(0n), + onTransferMinGas: tryAsServiceGas(0n), + storageUtilisationBytes: tryAsU64(0), + storageUtilisationCount: tryAsU32(0), + gratisStorage: tryAsU64(0), + created: tryAsTimeSlot(0), + lastAccumulation: tryAsTimeSlot(0), + parentService: tryAsServiceId(0), + }), + preimages: HashDictionary.fromEntries( + [PreimageItem.create({ hash: codeHash.asOpaque(), blob: code })].map((x) => [x.hash, x]), + ), + lookupHistory: HashDictionary.fromEntries([]), + storage: new Map(), + }); +} + +function createWorkItem(codeHash: CodeHash, serviceId = 1) { return WorkItem.create({ service: tryAsServiceId(serviceId), - codeHash: Bytes.zero(HASH_SIZE).asOpaque(), + codeHash, payload: BytesBlob.empty(), refineGasLimit: tryAsServiceGas(1_000_000), accumulateGasLimit: tryAsServiceGas(1_000_000), @@ -35,12 +67,17 @@ function createWorkItem(serviceId = 1) { }); } -function createWorkPackage(anchorHash: HeaderHash, stateRoot: StateRootHash, lookupAnchorSlot = 0) { +function createWorkPackage( + anchorHash: HeaderHash, + stateRoot: StateRootHash, + authCodeHash: CodeHash, + lookupAnchorSlot = 0, +) { return WorkPackage.create({ - authorization: BytesBlob.empty(), - authCodeHost: tryAsServiceId(1), - authCodeHash: Bytes.zero(HASH_SIZE).asOpaque(), - parametrization: BytesBlob.empty(), + authToken: BytesBlob.empty(), + authCodeHost: AUTH_SERVICE_ID, + authCodeHash, + authConfiguration: BytesBlob.empty(), context: RefineContext.create({ anchor: anchorHash, stateRoot, @@ -49,7 +86,7 @@ function createWorkPackage(anchorHash: HeaderHash, stateRoot: StateRootHash, loo lookupAnchorSlot: tryAsTimeSlot(lookupAnchorSlot), prerequisites: [], }), - items: FixedSizeArray.new([createWorkItem()], tryAsWorkItemsCount(1)), + items: FixedSizeArray.new([createWorkItem(authCodeHash)], tryAsWorkItemsCount(1)), }); } @@ -68,7 +105,8 @@ describe("InCore", () => { const anchorHash = Bytes.fill(HASH_SIZE, 1).asOpaque(); const stateRoot = Bytes.zero(HASH_SIZE).asOpaque(); - const workPackage = createWorkPackage(anchorHash, stateRoot); + const authCodeHash = getAuthCodeHash(); + const workPackage = createWorkPackage(anchorHash, stateRoot, authCodeHash); const result = await inCore.refine( hashWorkPackage(spec, workPackage), @@ -86,12 +124,16 @@ describe("InCore", () => { const states = new InMemoryStates(spec); const inCore = new InCore(spec, states, PvmBackend.BuiltIn, blake2b); + const authCodeHash = getAuthCodeHash(); const anchorHash = Bytes.fill(HASH_SIZE, 1).asOpaque(); - const state = testState(); + const state = InMemoryState.partial(spec, { + timeslot: tryAsTimeSlot(16), + services: new Map([[AUTH_SERVICE_ID, createService(AUTH_SERVICE_ID, authCodeHash, AUTHORIZER_PVM)]]), + }); await states.insertInitialState(anchorHash, state); const correctStateRoot = await states.getStateRoot(state); - const workPackage = createWorkPackage(anchorHash, correctStateRoot, state.timeslot); + const workPackage = createWorkPackage(anchorHash, correctStateRoot, authCodeHash, state.timeslot); const result = await inCore.refine( hashWorkPackage(spec, workPackage), @@ -100,7 +142,7 @@ describe("InCore", () => { asKnownSize([[]]), ); - assert.strictEqual(result.isOk, true); + assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); assert.strictEqual(result.ok.report.coreIndex, 0); assert.strictEqual(result.ok.report.results.length, 1); }); diff --git a/packages/jam/in-core/in-core.ts b/packages/jam/in-core/in-core.ts index c53157807..e4bf46286 100644 --- a/packages/jam/in-core/in-core.ts +++ b/packages/jam/in-core/in-core.ts @@ -1,48 +1,27 @@ -import { - type CodeHash, - type CoreIndex, - type Segment, - type SegmentIndex, - type ServiceGas, - type ServiceId, - tryAsCoreIndex, - tryAsServiceGas, -} from "@typeberry/block"; -import { W_C } from "@typeberry/block/gp-constants.js"; -import { - type AuthorizerHash, - type RefineContext, - type WorkPackageHash, - WorkPackageInfo, -} from "@typeberry/block/refine-context.js"; -import type { WorkItem, WorkItemExtrinsic } from "@typeberry/block/work-item.js"; +import type { CoreIndex, Segment } from "@typeberry/block"; +import { type RefineContext, type WorkPackageHash, WorkPackageInfo } from "@typeberry/block/refine-context.js"; +import type { WorkItemExtrinsic } from "@typeberry/block/work-item.js"; import type { WorkPackage } from "@typeberry/block/work-package.js"; import { WorkPackageSpec, WorkReport } from "@typeberry/block/work-report.js"; -import { WorkExecResult, WorkExecResultKind, WorkRefineLoad, WorkResult } from "@typeberry/block/work-result.js"; -import { Bytes, BytesBlob } from "@typeberry/bytes"; -import { codec, Encoder } from "@typeberry/codec"; -import { asKnownSize, FixedSizeArray, type KnownSizeArray } from "@typeberry/collections"; +import { Bytes } from "@typeberry/bytes"; +import { asKnownSize, FixedSizeArray } from "@typeberry/collections"; import type { ChainSpec, PvmBackend } from "@typeberry/config"; import type { StatesDb } from "@typeberry/database"; -import { PvmExecutor, type RefineHostCallExternalities, ReturnStatus, type ReturnValue } from "@typeberry/executor"; -import { type Blake2b, HASH_SIZE, type WithHash } from "@typeberry/hash"; +import type { Blake2b, WithHash } from "@typeberry/hash"; +import { HASH_SIZE } from "@typeberry/hash"; import { Logger } from "@typeberry/logger"; import { tryAsU8, tryAsU16, tryAsU32 } from "@typeberry/numbers"; -import type { State } from "@typeberry/state"; -import { RefineFetchExternalities } from "@typeberry/transition/externalities/refine-fetch-externalities.js"; -import { assertEmpty, assertNever, Result } from "@typeberry/utils"; -import { RefineExternalitiesImpl } from "./externalities/refine.js"; +import { assertEmpty, Result } from "@typeberry/utils"; +import { AuthorizationError, type AuthorizationOk, IsAuthorized } from "./is-authorized.js"; +import { type ImportedSegment, type PerWorkItem, Refine, type RefineItemResult } from "./refine.js"; + +export type { ImportedSegment, PerWorkItem, RefineItemResult } from "./refine.js"; export type RefineResult = { report: WorkReport; exports: PerWorkItem; }; -export type RefineItemResult = { - result: WorkResult; - exports: readonly Segment[]; -}; - export enum RefineError { /** State for context anchor block or lookup anchor is not found in the DB. */ StateMissing = 0, @@ -54,53 +33,21 @@ export enum RefineError { AuthorizationError = 3, } -enum ServiceCodeError { - /** Service id is not found in the state. */ - ServiceNotFound = 0, - /** Expected service code does not match the state one. */ - ServiceCodeMismatch = 1, - /** Code preimage missing. */ - ServiceCodeMissing = 2, - /** Code blob is too big. */ - ServiceCodeTooBig = 3, -} - -enum AuthorizationError {} - -type AuthorizationOk = { - authorizerHash: AuthorizerHash; - authorizationGasUsed: ServiceGas; - authorizationOutput: BytesBlob; -}; - -export type PerWorkItem = KnownSizeArray; - -export type ImportedSegment = { - index: SegmentIndex; - data: Segment; -}; - const logger = Logger.new(import.meta.filename, "refine"); -/** https://graypaper.fluffylabs.dev/#/ab2cdbd/2ffe002ffe00?v=0.7.2 */ -const ARGS_CODEC = codec.object({ - core: codec.varU32.convert( - (x) => tryAsU32(x), - (x) => tryAsCoreIndex(x), - ), - workItemIndex: codec.varU32, - serviceId: codec.varU32.asOpaque(), - payloadLength: codec.varU32, - packageHash: codec.bytes(HASH_SIZE).asOpaque(), -}); - export class InCore { + private readonly isAuthorized: IsAuthorized; + private readonly refineItem: Refine; + constructor( public readonly chainSpec: ChainSpec, private readonly states: StatesDb, - private readonly pvmBackend: PvmBackend, - private readonly blake2b: Blake2b, - ) {} + pvmBackend: PvmBackend, + blake2b: Blake2b, + ) { + this.isAuthorized = new IsAuthorized(chainSpec, pvmBackend, blake2b); + this.refineItem = new Refine(chainSpec, pvmBackend, blake2b); + } /** * Work-report computation function. @@ -119,7 +66,7 @@ export class InCore { extrinsics: PerWorkItem, ): Promise> { const workPackageHash = workPackageAndHash.hash; - const { context, authorization, authCodeHash, authCodeHost, parametrization, items, ...rest } = + const { context, authToken, authCodeHash, authCodeHost, authConfiguration, items, ...rest } = workPackageAndHash.data; assertEmpty(rest); @@ -127,8 +74,9 @@ export class InCore { // TODO [ToDr] Verify prerequisites logger.log`[core:${core}] Attempting to refine work package with ${items.length} items.`; - // TODO [ToDr] GP link // Verify anchor block + // https://graypaper.fluffylabs.dev/#/ab2cdbd/15cd0215cd02?v=0.7.2 + // TODO [ToDr] Validation const state = this.states.getState(context.anchor); if (state === null) { return Result.error(RefineError.StateMissing, () => `State at anchor block ${context.anchor} is missing.`); @@ -160,7 +108,14 @@ export class InCore { } // Check authorization - const authResult = await this.authorizePackage(authorization, authCodeHost, authCodeHash, parametrization); + const authResult = await this.isAuthorized.invoke( + state, + core, + authToken, + authCodeHost, + authCodeHash, + authConfiguration, + ); if (authResult.isError) { return Result.error( RefineError.AuthorizationError, @@ -172,11 +127,11 @@ export class InCore { // Verify the work items let exportOffset = 0; - const refineResults: Awaited>[] = []; + const refineResults: RefineItemResult[] = []; for (const [idx, item] of items.entries()) { logger.info`[core:${core}][i:${idx}] Refining item for service ${item.service}.`; - const result = await this.refineItem( + const result = await this.refineItem.invoke( state, lookupState, idx, @@ -193,11 +148,11 @@ export class InCore { // amalgamate the work report now return Result.ok( - this.amalgamateWorkReport(asKnownSize(refineResults), authResult.ok, workPackageHash, context, core), + InCore.amalgamateWorkReport(asKnownSize(refineResults), authResult.ok, workPackageHash, context, core), ); } - private amalgamateWorkReport( + private static amalgamateWorkReport( refineResults: PerWorkItem, authResult: AuthorizationOk, workPackageHash: WorkPackageHash, @@ -251,203 +206,4 @@ export class InCore { exports: asKnownSize(exports), }; } - - private async authorizePackage( - _authorization: BytesBlob, - _authCodeHost: ServiceId, - _authCodeHash: CodeHash, - _parametrization: BytesBlob, - ): Promise> { - // TODO [ToDr] Check authorization? - const authorizerHash = Bytes.zero(HASH_SIZE).asOpaque(); - const authorizationGasUsed = tryAsServiceGas(0); - const authorizationOutput = BytesBlob.empty(); - - return Result.ok({ - authorizerHash, - authorizationGasUsed, - authorizationOutput, - }); - } - - private async refineItem( - state: State, - lookupState: State, - idx: number, - item: WorkItem, - allImports: PerWorkItem, - allExtrinsics: PerWorkItem, - coreIndex: CoreIndex, - workPackageHash: WorkPackageHash, - exportOffset: number, - ): Promise { - const payloadHash = this.blake2b.hashBytes(item.payload); - const baseResult = { - serviceId: item.service, - codeHash: item.codeHash, - payloadHash, - gas: item.refineGasLimit, - }; - const imports = allImports[idx]; - const extrinsics = allExtrinsics[idx]; - const baseLoad = { - importedSegments: tryAsU32(imports.length), - extrinsicCount: tryAsU32(extrinsics.length), - extrinsicSize: tryAsU32(extrinsics.reduce((acc, x) => acc + x.length, 0)), - }; - const maybeCode = this.getServiceCode(state, idx, item); - - if (maybeCode.isError) { - const error = - maybeCode.error === ServiceCodeError.ServiceCodeTooBig - ? WorkExecResultKind.codeOversize - : WorkExecResultKind.badCode; - return { - exports: [], - result: WorkResult.create({ - ...baseResult, - result: WorkExecResult.error(error), - load: WorkRefineLoad.create({ - ...baseLoad, - gasUsed: tryAsServiceGas(item.refineGasLimit), - exportedSegments: tryAsU32(0), - }), - }), - }; - } - - const code = maybeCode.ok; - const externalities = this.createRefineExternalities({ - payload: item.payload, - imports: allImports, - extrinsics: allExtrinsics, - currentServiceId: item.service, - lookupState, - exportOffset, - }); - - const executor = await PvmExecutor.createRefineExecutor(item.service, code, externalities, this.pvmBackend); - - const args = Encoder.encodeObject(ARGS_CODEC, { - serviceId: item.service, - core: coreIndex, - workItemIndex: tryAsU32(idx), - payloadLength: tryAsU32(item.payload.length), - packageHash: workPackageHash, - }); - - const execResult = await executor.run(args, item.refineGasLimit); - - const exports = externalities.refine.getExportedSegments(); - if (exports.length !== item.exportCount) { - return { - exports, - result: WorkResult.create({ - ...baseResult, - result: WorkExecResult.error(WorkExecResultKind.incorrectNumberOfExports), - load: WorkRefineLoad.create({ - ...baseLoad, - gasUsed: tryAsServiceGas(item.refineGasLimit), - exportedSegments: tryAsU32(0), - }), - }), - }; - } - - const result = this.extractWorkResult(execResult); - - return { - exports, - result: WorkResult.create({ - ...baseResult, - result, - load: WorkRefineLoad.create({ - ...baseLoad, - gasUsed: tryAsServiceGas(execResult.consumedGas), - exportedSegments: tryAsU32(exports.length), - }), - }), - }; - } - - extractWorkResult(execResult: ReturnValue) { - if (execResult.status === ReturnStatus.OK) { - const slice = execResult.memorySlice; - // TODO [ToDr] Verify the output size and change digestTooBig? - return WorkExecResult.ok(BytesBlob.blobFrom(slice)); - } - - switch (execResult.status) { - case ReturnStatus.OOG: - return WorkExecResult.error(WorkExecResultKind.outOfGas); - case ReturnStatus.PANIC: - return WorkExecResult.error(WorkExecResultKind.panic); - default: - assertNever(execResult); - } - } - - private getServiceCode(state: State, idx: number, item: WorkItem) { - const serviceId = item.service; - const service = state.getService(serviceId); - // TODO [ToDr] GP link - // missing service - if (service === null) { - return Result.error( - ServiceCodeError.ServiceNotFound, - () => `[i:${idx}] Service ${serviceId} is missing in state.`, - ); - } - - // TODO [ToDr] GP link - // TODO [ToDr] shall we rather use the old codehash instead - if (!service.getInfo().codeHash.isEqualTo(item.codeHash)) { - return Result.error( - ServiceCodeError.ServiceCodeMismatch, - () => - `[i:${idx}] Service ${serviceId} has invalid code hash. Ours: ${service.getInfo().codeHash}, expected: ${item.codeHash}`, - ); - } - - const code = service.getPreimage(item.codeHash.asOpaque()); - if (code === null) { - return Result.error( - ServiceCodeError.ServiceCodeMissing, - () => `[i:${idx}] Code ${item.codeHash} for service ${serviceId} was not found.`, - ); - } - - if (code.length > W_C) { - return Result.error( - ServiceCodeError.ServiceCodeTooBig, - () => - `[i:${idx}] Code ${item.codeHash} for service ${serviceId} is too big! ${code.length} bytes vs ${W_C} bytes max.`, - ); - } - - return Result.ok(code); - } - - private createRefineExternalities(args: { - payload: BytesBlob; - imports: PerWorkItem; - extrinsics: PerWorkItem; - currentServiceId: ServiceId; - lookupState: State; - exportOffset: number; - }): RefineHostCallExternalities { - // TODO [ToDr] Pass all required fetch data - const fetchExternalities = new RefineFetchExternalities(this.chainSpec); - const refine = RefineExternalitiesImpl.create({ - currentServiceId: args.currentServiceId, - lookupState: args.lookupState, - exportOffset: args.exportOffset, - pvmBackend: this.pvmBackend, - }); - - return { - fetchExternalities, - refine, - }; - } } diff --git a/packages/jam/in-core/is-authorized.test.ts b/packages/jam/in-core/is-authorized.test.ts new file mode 100644 index 000000000..866e6eba8 --- /dev/null +++ b/packages/jam/in-core/is-authorized.test.ts @@ -0,0 +1,185 @@ +import assert from "node:assert"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { before, describe, it } from "node:test"; +import type { CodeHash } from "@typeberry/block"; +import { tryAsCoreIndex, tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/block"; +import { Bytes, BytesBlob } from "@typeberry/bytes"; +import { HashDictionary } from "@typeberry/collections"; +import { PvmBackend, tinyChainSpec } from "@typeberry/config"; +import { Blake2b, HASH_SIZE, type OpaqueHash } from "@typeberry/hash"; +import { tryAsU32, tryAsU64 } from "@typeberry/numbers"; +import { InMemoryService, InMemoryState, PreimageItem, ServiceAccountInfo } from "@typeberry/state"; +import { AuthorizationError, IsAuthorized } from "./is-authorized.js"; + +let blake2b: Blake2b; + +before(async () => { + blake2b = await Blake2b.createHasher(); +}); + +// Load the authorizer PVM fixture. +// This authorizer checks that authToken === authConfiguration and returns "Auth=". +// https://github.com/tomusdrw/as-lan/blob/main/examples/authorizer/assembly/authorize.ts +const AUTHORIZER_PVM = BytesBlob.blobFrom(readFileSync(resolve(import.meta.dirname, "fixtures/authorizer.pvm"))); + +const AUTH_SERVICE_ID = tryAsServiceId(42); + +function createService(serviceId: typeof AUTH_SERVICE_ID, codeHash: OpaqueHash, code: BytesBlob): InMemoryService { + return new InMemoryService(serviceId, { + info: ServiceAccountInfo.create({ + codeHash: codeHash.asOpaque(), + balance: tryAsU64(10_000_000_000), + accumulateMinGas: tryAsServiceGas(0n), + onTransferMinGas: tryAsServiceGas(0n), + storageUtilisationBytes: tryAsU64(0), + storageUtilisationCount: tryAsU32(0), + gratisStorage: tryAsU64(0), + created: tryAsTimeSlot(0), + lastAccumulation: tryAsTimeSlot(0), + parentService: tryAsServiceId(0), + }), + preimages: HashDictionary.fromEntries( + [PreimageItem.create({ hash: codeHash.asOpaque(), blob: code })].map((x) => [x.hash, x]), + ), + lookupHistory: HashDictionary.fromEntries([]), + storage: new Map(), + }); +} + +describe("IsAuthorized", () => { + const spec = tinyChainSpec; + + function getAuthCodeHash() { + return blake2b.hashBytes(AUTHORIZER_PVM).asOpaque(); + } + + function createStateWithService(codeHash: OpaqueHash, code: BytesBlob) { + return InMemoryState.partial(spec, { + timeslot: tryAsTimeSlot(16), + services: new Map([[AUTH_SERVICE_ID, createService(AUTH_SERVICE_ID, codeHash, code)]]), + }); + } + + it("should authorize when token matches configuration", async () => { + const authCodeHash = getAuthCodeHash(); + const state = createStateWithService(authCodeHash, AUTHORIZER_PVM); + const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); + const token = BytesBlob.blobFromString("hello"); + + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), token, AUTH_SERVICE_ID, authCodeHash, token); + + assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); + + // Verify the authorization output starts with "Auth=" + const outputStr = Buffer.from(result.ok.authorizationOutput.raw).toString("utf8"); + assert.ok( + outputStr.startsWith("Auth="), + `Expected "Auth=" prefix but got "${outputStr.slice(0, 30)}"`, + ); + + // Verify the authorizer hash is H(code_hash ++ configuration) + const expectedHash = blake2b.hashBlobs([authCodeHash, token]); + assert.ok(result.ok.authorizerHash.isEqualTo(expectedHash), "authorizerHash should be H(code_hash || config)"); + + // Verify gas was consumed + assert.ok(Number(result.ok.authorizationGasUsed) > 0, "should have consumed some gas"); + }); + + it("should authorize with empty token and configuration", async () => { + const authCodeHash = getAuthCodeHash(); + const state = createStateWithService(authCodeHash, AUTHORIZER_PVM); + const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); + + const result = await isAuthorized.invoke( + state, + tryAsCoreIndex(0), + BytesBlob.empty(), + AUTH_SERVICE_ID, + authCodeHash, + BytesBlob.empty(), + ); + + assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); + const outputStr = Buffer.from(result.ok.authorizationOutput.raw).toString("utf8"); + assert.ok(outputStr.startsWith("Auth=<>"), `Expected "Auth=<>" prefix but got "${outputStr.slice(0, 30)}"`); + }); + + it("should fail when token does not match configuration", async () => { + const authCodeHash = getAuthCodeHash(); + const state = createStateWithService(authCodeHash, AUTHORIZER_PVM); + const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); + + const result = await isAuthorized.invoke( + state, + tryAsCoreIndex(0), + BytesBlob.blobFromString("wrong"), + AUTH_SERVICE_ID, + authCodeHash, + BytesBlob.blobFromString("right"), + ); + + assert.strictEqual(result.isError, true); + assert.strictEqual(result.error, AuthorizationError.PvmFailed); + }); + + it("should fail when auth code host service is missing", async () => { + const authCodeHash = getAuthCodeHash(); + const state = InMemoryState.partial(spec, { + timeslot: tryAsTimeSlot(16), + services: new Map(), + }); + const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); + + const result = await isAuthorized.invoke( + state, + tryAsCoreIndex(0), + BytesBlob.empty(), + AUTH_SERVICE_ID, + authCodeHash, + BytesBlob.empty(), + ); + + assert.strictEqual(result.isError, true); + assert.strictEqual(result.error, AuthorizationError.CodeNotFound); + }); + + it("should fail when auth code preimage is missing", async () => { + const authCodeHash = getAuthCodeHash(); + // Service exists but with no preimages + const emptyService = new InMemoryService(AUTH_SERVICE_ID, { + info: ServiceAccountInfo.create({ + codeHash: Bytes.zero(HASH_SIZE).asOpaque(), + balance: tryAsU64(0), + accumulateMinGas: tryAsServiceGas(0n), + onTransferMinGas: tryAsServiceGas(0n), + storageUtilisationBytes: tryAsU64(0), + storageUtilisationCount: tryAsU32(0), + gratisStorage: tryAsU64(0), + created: tryAsTimeSlot(0), + lastAccumulation: tryAsTimeSlot(0), + parentService: tryAsServiceId(0), + }), + preimages: HashDictionary.fromEntries([]), + lookupHistory: HashDictionary.fromEntries([]), + storage: new Map(), + }); + const state = InMemoryState.partial(spec, { + timeslot: tryAsTimeSlot(16), + services: new Map([[AUTH_SERVICE_ID, emptyService]]), + }); + const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); + + const result = await isAuthorized.invoke( + state, + tryAsCoreIndex(0), + BytesBlob.empty(), + AUTH_SERVICE_ID, + authCodeHash, + BytesBlob.empty(), + ); + + assert.strictEqual(result.isError, true); + assert.strictEqual(result.error, AuthorizationError.CodeNotFound); + }); +}); diff --git a/packages/jam/in-core/is-authorized.ts b/packages/jam/in-core/is-authorized.ts new file mode 100644 index 000000000..1469c09dc --- /dev/null +++ b/packages/jam/in-core/is-authorized.ts @@ -0,0 +1,114 @@ +import { type CodeHash, type CoreIndex, type ServiceGas, type ServiceId, tryAsServiceGas } from "@typeberry/block"; +import { G_I, W_A } from "@typeberry/block/gp-constants.js"; +import type { AuthorizerHash } from "@typeberry/block/refine-context.js"; +import { BytesBlob } from "@typeberry/bytes"; +import { codec, Encoder } from "@typeberry/codec"; +import type { ChainSpec, PvmBackend } from "@typeberry/config"; +import { PvmExecutor, ReturnStatus } from "@typeberry/executor"; +import type { Blake2b } from "@typeberry/hash"; +import type { State } from "@typeberry/state"; +import { IsAuthorizedFetchExternalities } from "@typeberry/transition/externalities/is-authorized-fetch-externalities.js"; +import { Result } from "@typeberry/utils"; + +export enum AuthorizationError { + /** BAD: authorizer code not found (service or preimage missing). */ + CodeNotFound = 0, + /** BIG: authorizer code exceeds W_A limit. */ + CodeTooBig = 1, + /** PANIC/OOG: PVM execution failed. */ + PvmFailed = 2, +} + +export type AuthorizationOk = { + authorizerHash: AuthorizerHash; + authorizationGasUsed: ServiceGas; + authorizationOutput: BytesBlob; +}; + +const AUTH_ARGS_CODEC = codec.object({ + coreIndex: codec.u16, +}); + +/** + * IsAuthorized PVM invocation (Psi_I). + * + * https://graypaper.fluffylabs.dev/#/ab2cdbd/2e64002e6400?v=0.7.2 + */ +export class IsAuthorized { + constructor( + private readonly chainSpec: ChainSpec, + private readonly pvmBackend: PvmBackend, + private readonly blake2b: Blake2b, + ) {} + + async invoke( + state: State, + coreIndex: CoreIndex, + authToken: BytesBlob, + authCodeHost: ServiceId, + authCodeHash: CodeHash, + authConfiguration: BytesBlob, + ): Promise> { + // Look up the authorizer code from the auth code host service + const service = state.getService(authCodeHost); + // https://graypaper.fluffylabs.dev/#/ab2cdbd/2eca002eca00?v=0.7.2 + if (service === null) { + return Result.error( + AuthorizationError.CodeNotFound, + () => `Auth code host service ${authCodeHost} not found in state.`, + ); + } + + const code = service.getPreimage(authCodeHash.asOpaque()); + if (code === null) { + return Result.error( + AuthorizationError.CodeNotFound, + () => `Auth code preimage ${authCodeHash} not found in service ${authCodeHost}.`, + ); + } + + // BIG: code exceeds W_A + // https://graypaper.fluffylabs.dev/#/ab2cdbd/2ed6002ed600?v=0.7.2 + if (code.length > W_A) { + return Result.error( + AuthorizationError.CodeTooBig, + () => `Auth code is too big: ${code.length} bytes vs ${W_A} max.`, + ); + } + + // Prepare fetch externalities and executor + const fetchExternalities = new IsAuthorizedFetchExternalities(this.chainSpec, { + authToken, + authConfiguration, + }); + const executor = await PvmExecutor.createIsAuthorizedExecutor( + authCodeHost, + code, + { fetchExternalities }, + this.pvmBackend, + ); + + const args = Encoder.encodeObject(AUTH_ARGS_CODEC, { + coreIndex, + }); + + // Run PVM with gas budget G_I + const gasLimit = tryAsServiceGas(G_I); + const execResult = await executor.run(args, gasLimit); + + if (execResult.status !== ReturnStatus.OK) { + return Result.error( + AuthorizationError.PvmFailed, + () => `IsAuthorized PVM ${ReturnStatus[execResult.status]} (gas used: ${execResult.consumedGas}).`, + ); + } + + // Compute authorizer hash: H(code_hash ++ configuration) + // https://graypaper.fluffylabs.dev/#/ab2cdbd/1b81011b8401?v=0.7.2 + const authorizerHash = this.blake2b.hashBlobs([authCodeHash, authConfiguration]); + const authorizationOutput = BytesBlob.blobFrom(execResult.memorySlice); + const authorizationGasUsed = tryAsServiceGas(execResult.consumedGas); + + return Result.ok({ authorizerHash, authorizationGasUsed, authorizationOutput }); + } +} diff --git a/packages/jam/in-core/refine.test.ts b/packages/jam/in-core/refine.test.ts new file mode 100644 index 000000000..7d294828e --- /dev/null +++ b/packages/jam/in-core/refine.test.ts @@ -0,0 +1,7 @@ +import { describe } from "node:test"; + +describe("Refine", () => { + // TODO [ToDr] Add refine-specific PVM invocation tests. + // These should test Refine.invoke() directly, similar to + // how is-authorized.test.ts tests IsAuthorized.invoke(). +}); diff --git a/packages/jam/in-core/refine.ts b/packages/jam/in-core/refine.ts new file mode 100644 index 000000000..0c76c925d --- /dev/null +++ b/packages/jam/in-core/refine.ts @@ -0,0 +1,253 @@ +import { + type CoreIndex, + type Segment, + type SegmentIndex, + type ServiceGas, + type ServiceId, + tryAsCoreIndex, + tryAsServiceGas, +} from "@typeberry/block"; +import { W_C } from "@typeberry/block/gp-constants.js"; +import type { WorkPackageHash } from "@typeberry/block/refine-context.js"; +import type { WorkItem, WorkItemExtrinsic } from "@typeberry/block/work-item.js"; +import { WorkExecResult, WorkExecResultKind, WorkRefineLoad, WorkResult } from "@typeberry/block/work-result.js"; +import { BytesBlob } from "@typeberry/bytes"; +import { codec, Encoder } from "@typeberry/codec"; +import type { KnownSizeArray } from "@typeberry/collections"; +import type { ChainSpec, PvmBackend } from "@typeberry/config"; +import { PvmExecutor, type RefineHostCallExternalities, ReturnStatus, type ReturnValue } from "@typeberry/executor"; +import { type Blake2b, HASH_SIZE } from "@typeberry/hash"; +import { tryAsU32 } from "@typeberry/numbers"; +import type { State } from "@typeberry/state"; +import { RefineFetchExternalities } from "@typeberry/transition/externalities/refine-fetch-externalities.js"; +import { assertNever, Result } from "@typeberry/utils"; +import { RefineExternalitiesImpl } from "./externalities/refine.js"; + +export type RefineItemResult = { + result: WorkResult; + exports: readonly Segment[]; +}; + +export type PerWorkItem = KnownSizeArray; + +export type ImportedSegment = { + index: SegmentIndex; + data: Segment; +}; + +enum ServiceCodeError { + /** Service id is not found in the state. */ + ServiceNotFound = 0, + /** Expected service code does not match the state one. */ + ServiceCodeMismatch = 1, + /** Code preimage missing. */ + ServiceCodeMissing = 2, + /** Code blob is too big. */ + ServiceCodeTooBig = 3, +} + +/** https://graypaper.fluffylabs.dev/#/ab2cdbd/2ffe002ffe00?v=0.7.2 */ +const REFINE_ARGS_CODEC = codec.object({ + core: codec.varU32.convert( + (x) => tryAsU32(x), + (x) => tryAsCoreIndex(x), + ), + workItemIndex: codec.varU32, + serviceId: codec.varU32.asOpaque(), + payloadLength: codec.varU32, + packageHash: codec.bytes(HASH_SIZE).asOpaque(), +}); + +/** + * Refine PVM invocation (Psi_R). + * + * Executes a single work item's refinement logic. + */ +export class Refine { + constructor( + private readonly chainSpec: ChainSpec, + private readonly pvmBackend: PvmBackend, + private readonly blake2b: Blake2b, + ) {} + + async invoke( + state: State, + lookupState: State, + idx: number, + item: WorkItem, + allImports: PerWorkItem, + allExtrinsics: PerWorkItem, + coreIndex: CoreIndex, + workPackageHash: WorkPackageHash, + exportOffset: number, + ): Promise { + const payloadHash = this.blake2b.hashBytes(item.payload); + const baseResult = { + serviceId: item.service, + codeHash: item.codeHash, + payloadHash, + gas: item.refineGasLimit, + }; + const imports = allImports[idx]; + const extrinsics = allExtrinsics[idx]; + const baseLoad = { + importedSegments: tryAsU32(imports.length), + extrinsicCount: tryAsU32(extrinsics.length), + extrinsicSize: tryAsU32(extrinsics.reduce((acc, x) => acc + x.length, 0)), + }; + const maybeCode = this.getServiceCode(state, idx, item); + + if (maybeCode.isError) { + const error = + maybeCode.error === ServiceCodeError.ServiceCodeTooBig + ? WorkExecResultKind.codeOversize + : WorkExecResultKind.badCode; + return { + exports: [], + result: WorkResult.create({ + ...baseResult, + result: WorkExecResult.error(error), + load: WorkRefineLoad.create({ + ...baseLoad, + gasUsed: tryAsServiceGas(item.refineGasLimit), + exportedSegments: tryAsU32(0), + }), + }), + }; + } + + const code = maybeCode.ok; + const externalities = this.createRefineExternalities({ + payload: item.payload, + imports: allImports, + extrinsics: allExtrinsics, + currentServiceId: item.service, + lookupState, + exportOffset, + }); + + const executor = await PvmExecutor.createRefineExecutor(item.service, code, externalities, this.pvmBackend); + + const args = Encoder.encodeObject(REFINE_ARGS_CODEC, { + serviceId: item.service, + core: coreIndex, + workItemIndex: tryAsU32(idx), + payloadLength: tryAsU32(item.payload.length), + packageHash: workPackageHash, + }); + + const execResult = await executor.run(args, item.refineGasLimit); + + const exports = externalities.refine.getExportedSegments(); + if (exports.length !== item.exportCount) { + return { + exports: [], + result: WorkResult.create({ + ...baseResult, + result: WorkExecResult.error(WorkExecResultKind.incorrectNumberOfExports), + load: WorkRefineLoad.create({ + ...baseLoad, + gasUsed: tryAsServiceGas(item.refineGasLimit), + exportedSegments: tryAsU32(0), + }), + }), + }; + } + + const result = Refine.extractWorkResult(execResult); + + return { + exports, + result: WorkResult.create({ + ...baseResult, + result, + load: WorkRefineLoad.create({ + ...baseLoad, + gasUsed: tryAsServiceGas(execResult.consumedGas), + exportedSegments: tryAsU32(exports.length), + }), + }), + }; + } + + static extractWorkResult(execResult: ReturnValue) { + if (execResult.status === ReturnStatus.OK) { + const slice = execResult.memorySlice; + // TODO [ToDr] Verify the output size and change digestTooBig? + return WorkExecResult.ok(BytesBlob.blobFrom(slice)); + } + + switch (execResult.status) { + case ReturnStatus.OOG: + return WorkExecResult.error(WorkExecResultKind.outOfGas); + case ReturnStatus.PANIC: + return WorkExecResult.error(WorkExecResultKind.panic); + default: + assertNever(execResult); + } + } + + private getServiceCode(state: State, idx: number, item: WorkItem) { + const serviceId = item.service; + const service = state.getService(serviceId); + // TODO [ToDr] GP link + // missing service + if (service === null) { + return Result.error( + ServiceCodeError.ServiceNotFound, + () => `[i:${idx}] Service ${serviceId} is missing in state.`, + ); + } + + // TODO [ToDr] GP link + // TODO [ToDr] shall we rather use the old codehash instead + if (!service.getInfo().codeHash.isEqualTo(item.codeHash)) { + return Result.error( + ServiceCodeError.ServiceCodeMismatch, + () => + `[i:${idx}] Service ${serviceId} has invalid code hash. Ours: ${service.getInfo().codeHash}, expected: ${item.codeHash}`, + ); + } + + const code = service.getPreimage(item.codeHash.asOpaque()); + if (code === null) { + return Result.error( + ServiceCodeError.ServiceCodeMissing, + () => `[i:${idx}] Code ${item.codeHash} for service ${serviceId} was not found.`, + ); + } + + if (code.length > W_C) { + return Result.error( + ServiceCodeError.ServiceCodeTooBig, + () => + `[i:${idx}] Code ${item.codeHash} for service ${serviceId} is too big! ${code.length} bytes vs ${W_C} bytes max.`, + ); + } + + return Result.ok(code); + } + + private createRefineExternalities(args: { + payload: BytesBlob; + imports: PerWorkItem; + extrinsics: PerWorkItem; + currentServiceId: ServiceId; + lookupState: State; + exportOffset: number; + }): RefineHostCallExternalities { + // TODO [ToDr] Pass all required fetch data + const fetchExternalities = new RefineFetchExternalities(this.chainSpec); + const refine = RefineExternalitiesImpl.create({ + currentServiceId: args.currentServiceId, + lookupState: args.lookupState, + exportOffset: args.exportOffset, + pvmBackend: this.pvmBackend, + }); + + return { + fetchExternalities, + refine, + }; + } +} diff --git a/packages/jam/jam-host-calls/general/fetch.test.ts b/packages/jam/jam-host-calls/general/fetch.test.ts index 1c84497c9..1f5f7eea3 100644 --- a/packages/jam/jam-host-calls/general/fetch.test.ts +++ b/packages/jam/jam-host-calls/general/fetch.test.ts @@ -44,10 +44,13 @@ describe("Fetch", () => { it("should write empty result and set IN_OUT_REG to NONE if fetch returns null", async () => { const currentServiceId = tryAsServiceId(10_000); const fetchMock = new RefineFetchMock(); - // authorizerTraceResponse is null by default — Kind 2 legitimately returns null + // oneWorkItem returns null when the work item index has no mock response registered const blob = BytesBlob.blobFromNumbers([]); - const { registers, memory, readBack } = prepareRegsAndMemory(blob, FetchKind.AuthorizerTrace); + const { registers, memory, readBack } = prepareRegsAndMemory(blob, FetchKind.OneWorkItem); + // set work item index to one that has no response → oneWorkItem returns null + registers.set(11, tryAsU64(999)); + fetchMock.oneWorkItemResponses.set("999", null); const fetch = new Fetch(currentServiceId, fetchMock); const result = await fetch.execute(gas, registers, memory); @@ -269,7 +272,7 @@ describe("Fetch", () => { const fetchMock = new RefineFetchMock(); fetchMock.authorizerResponse = blob; - const { registers, memory, readBack, expectedLength } = prepareRegsAndMemory(blob, FetchKind.Authorizer); + const { registers, memory, readBack, expectedLength } = prepareRegsAndMemory(blob, FetchKind.AuthConfiguration); const fetch = new Fetch(currentServiceId, fetchMock); const result = await fetch.execute(gas, registers, memory); @@ -285,7 +288,7 @@ describe("Fetch", () => { const fetchMock = new RefineFetchMock(); fetchMock.authorizationTokenResponse = blob; - const { registers, memory, readBack, expectedLength } = prepareRegsAndMemory(blob, FetchKind.AuthorizationToken); + const { registers, memory, readBack, expectedLength } = prepareRegsAndMemory(blob, FetchKind.AuthToken); const fetch = new Fetch(currentServiceId, fetchMock); const result = await fetch.execute(gas, registers, memory); @@ -418,8 +421,8 @@ describe("Fetch", () => { FetchKind.OtherWorkItemImports, FetchKind.MyImports, FetchKind.WorkPackage, - FetchKind.Authorizer, - FetchKind.AuthorizationToken, + FetchKind.AuthConfiguration, + FetchKind.AuthToken, FetchKind.RefineContext, FetchKind.AllWorkItems, FetchKind.OneWorkItem, @@ -491,14 +494,14 @@ class RefineFetchMock implements IRefineFetch { public constantsResponse: BytesBlob | null = null; public entropyResponse: EntropyHash | null = null; - public authorizerTraceResponse: BytesBlob | null = null; + public authorizerTraceResponse: BytesBlob = BytesBlob.empty(); public workItemExtrinsicResponses: Map = new Map(); public workItemImportResponses: Map = new Map(); - public workPackageResponse: BytesBlob | null = null; - public authorizerResponse: BytesBlob | null = null; - public authorizationTokenResponse: BytesBlob | null = null; - public refineContextResponse: BytesBlob | null = null; - public allWorkItemsResponse: BytesBlob | null = null; + public workPackageResponse: BytesBlob = BytesBlob.empty(); + public authorizerResponse: BytesBlob = BytesBlob.empty(); + public authorizationTokenResponse: BytesBlob = BytesBlob.empty(); + public refineContextResponse: BytesBlob = BytesBlob.empty(); + public allWorkItemsResponse: BytesBlob = BytesBlob.empty(); public oneWorkItemResponses: Map = new Map(); public workItemPayloadResponses: Map = new Map(); @@ -516,7 +519,7 @@ class RefineFetchMock implements IRefineFetch { return this.entropyResponse; } - authorizerTrace(): BytesBlob | null { + authorizerTrace(): BytesBlob { return this.authorizerTraceResponse; } @@ -538,23 +541,23 @@ class RefineFetchMock implements IRefineFetch { return this.workItemImportResponses.get(key) ?? null; } - workPackage(): BytesBlob | null { + workPackage(): BytesBlob { return this.workPackageResponse; } - authorizer(): BytesBlob | null { + authConfiguration(): BytesBlob { return this.authorizerResponse; } - authorizationToken(): BytesBlob | null { + authToken(): BytesBlob { return this.authorizationTokenResponse; } - refineContext(): BytesBlob | null { + refineContext(): BytesBlob { return this.refineContextResponse; } - allWorkItems(): BytesBlob | null { + allWorkItems(): BytesBlob { return this.allWorkItemsResponse; } diff --git a/packages/jam/jam-host-calls/general/fetch.ts b/packages/jam/jam-host-calls/general/fetch.ts index 07153717f..dccc36d11 100644 --- a/packages/jam/jam-host-calls/general/fetch.ts +++ b/packages/jam/jam-host-calls/general/fetch.ts @@ -18,7 +18,7 @@ import { HostCallResult } from "./results.js"; * Ω_Y signature: Ω_Y(ρ, φ, μ, p, n, r, i, ī, x̄, 𝐢, ...) * * Context parameter mapping - * Is-Authorized: Ω_Y(ρ, φ, μ, 𝐩, ∅, ∅, ∅, ∅, ∅, ∅, ∅) + * IsAuthorized: Ω_Y(ρ, φ, μ, 𝐩, ∅, ∅, ∅, ∅, ∅, ∅, ∅) * https://graypaper.fluffylabs.dev/#/ab2cdbd/2e43012e4301?v=0.7.2 * Refine: Ω_Y(ρ, φ, μ, p, H₀, r, i, ī, x̄, ∅, (m,e)) * https://graypaper.fluffylabs.dev/#/ab2cdbd/2fe0012fe001?v=0.7.2 @@ -26,13 +26,13 @@ import { HostCallResult } from "./results.js"; * https://graypaper.fluffylabs.dev/#/ab2cdbd/30c00030c000?v=0.7.2 * * Kind availability per context: - * Kind 0 (constants) — all contexts - * Kind 1 (n) — Refine (H₀), Accumulate (η'₀) - * Kind 2 (r) — Refine only - * Kind 3-4 (x̄ extrinsics) — Refine only - * Kind 5-6 (ī imports) — Refine only - * Kind 7-13 (p work pkg) — Is-Authorized, Refine - * Kind 14-15 (𝐢 acc items) — Accumulate only + * Kind 0 (constants) - all contexts + * Kind 1 (n) - Refine (H₀), Accumulate (η'₀) + * Kind 2 (r) - Refine only + * Kind 3-4 (x̄ extrinsics) - Refine only + * Kind 5-6 (ī imports) - Refine only + * Kind 7-13 (p work pkg) - IsAuthorized, Refine + * Kind 14-15 (𝐢 acc items) - Accumulate only */ export enum FetchContext { IsAuthorized = "isAuthorized", @@ -41,7 +41,7 @@ export enum FetchContext { } /** - * Fetch externalities for the Is-Authorized context. + * Fetch externalities for the IsAuthorized context. * * Ω_Y(ρ, φ, μ, 𝐩, ∅, ∅, ∅, ∅, ∅, ∅, ∅) * https://graypaper.fluffylabs.dev/#/ab2cdbd/2e43012e4301?v=0.7.2 @@ -59,49 +59,49 @@ export interface IIsAuthorizedFetch { constants(): BytesBlob; /** - * Kind 7: Encoded work package — E(𝐩). + * Kind 7: Encoded work package - E(𝐩). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31c10231c102?v=0.7.2 */ - workPackage(): BytesBlob | null; + workPackage(): BytesBlob; /** - * Kind 8: Authorizer code hash and config — p_f. + * Kind 8: Authorizer configuration - p_f. * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31c80231c802?v=0.7.2 */ - authorizer(): BytesBlob | null; + authConfiguration(): BytesBlob; /** - * Kind 9: Authorization token — p_j. + * Kind 9: Authorization token - p_j. * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31cf0231cf02?v=0.7.2 */ - authorizationToken(): BytesBlob | null; + authToken(): BytesBlob; /** - * Kind 10: Refinement context — E(p_x). + * Kind 10: Refinement context - E(p_x). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31da0231da02?v=0.7.2 */ - refineContext(): BytesBlob | null; + refineContext(): BytesBlob; /** - * Kind 11: All work-item summaries — E(↕[S(w) | w ← p_w]). + * Kind 11: All work-item summaries - E(↕[S(w) | w ← p_w]). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31f40231f402?v=0.7.2 */ - allWorkItems(): BytesBlob | null; + allWorkItems(): BytesBlob; /** - * Kind 12: Single work-item summary — S(p_w[φ₁₁]). + * Kind 12: Single work-item summary - S(p_w[φ₁₁]). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31fc0231fc02?v=0.7.2 */ oneWorkItem(workItem: U64): BytesBlob | null; /** - * Kind 13: Work-item payload — p_w[φ₁₁]_y. + * Kind 13: Work-item payload - p_w[φ₁₁]_y. * * https://graypaper.fluffylabs.dev/#/ab2cdbd/313b03313b03?v=0.7.2 */ @@ -127,7 +127,7 @@ export interface IRefineFetch { constants(): BytesBlob; /** - * Kind 1: Entropy pool — H₀ (zero hash). + * Kind 1: Entropy pool - H₀ (zero hash). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/2fe0012fe201?v=0.7.2 */ @@ -138,7 +138,7 @@ export interface IRefineFetch { * * https://graypaper.fluffylabs.dev/#/ab2cdbd/314902314902?v=0.7.2 */ - authorizerTrace(): BytesBlob | null; + authorizerTrace(): BytesBlob; /** * Kind 3 (other) / Kind 4 (my): Work-item extrinsics (x̄). @@ -163,49 +163,49 @@ export interface IRefineFetch { workItemImport(workItem: U64 | null, index: U64): BytesBlob | null; /** - * Kind 7: Encoded work package — E(p). + * Kind 7: Encoded work package - E(p). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31c10231c102?v=0.7.2 */ - workPackage(): BytesBlob | null; + workPackage(): BytesBlob; /** - * Kind 8: Authorizer code hash and config — p_f. + * Kind 8: Authorizer configuration - p_f. * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31c80231c802?v=0.7.2 */ - authorizer(): BytesBlob | null; + authConfiguration(): BytesBlob; /** - * Kind 9: Authorization token — p_j. + * Kind 9: Authorization token - p_j. * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31cf0231cf02?v=0.7.2 */ - authorizationToken(): BytesBlob | null; + authToken(): BytesBlob; /** - * Kind 10: Refinement context — E(p_x). + * Kind 10: Refinement context - E(p_x). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31da0231da02?v=0.7.2 */ - refineContext(): BytesBlob | null; + refineContext(): BytesBlob; /** - * Kind 11: All work-item summaries — E(↕[S(w) | w ← p_w]). + * Kind 11: All work-item summaries - E(↕[S(w) | w ← p_w]). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31f40231f402?v=0.7.2 */ - allWorkItems(): BytesBlob | null; + allWorkItems(): BytesBlob; /** - * Kind 12: Single work-item summary — S(p_w[φ₁₁]). + * Kind 12: Single work-item summary - S(p_w[φ₁₁]). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31fc0231fc02?v=0.7.2 */ oneWorkItem(workItem: U64): BytesBlob | null; /** - * Kind 13: Work-item payload — p_w[φ₁₁]_y. + * Kind 13: Work-item payload - p_w[φ₁₁]_y. * * https://graypaper.fluffylabs.dev/#/ab2cdbd/313b03313b03?v=0.7.2 */ @@ -231,21 +231,21 @@ export interface IAccumulateFetch { constants(): BytesBlob; /** - * Kind 1: Entropy pool — η'₀ (posterior entropy). + * Kind 1: Entropy pool - η'₀ (posterior entropy). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/314302314602?v=0.7.2 */ entropy(): EntropyHash; /** - * Kind 14: All accumulation operands and transfers — E(↕𝐢). + * Kind 14: All accumulation operands and transfers - E(↕𝐢). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/314e03314e03?v=0.7.2 */ allTransfersAndOperands(): BytesBlob | null; /** - * Kind 15: Single accumulation operand or transfer — E(𝐢[φ₁₁]). + * Kind 15: Single accumulation operand or transfer - E(𝐢[φ₁₁]). * * https://graypaper.fluffylabs.dev/#/ab2cdbd/315903315903?v=0.7.2 */ @@ -304,12 +304,12 @@ export class Fetch implements HostCallHandler { private getValue(kind: U32, regs: HostCallRegisters): BytesBlob | null { const ext = this.fetch; - // Kind 0: constants — all contexts + // Kind 0: constants - all contexts if (kind === FetchKind.Constants) { return ext.constants(); } - // Kind 1: entropy — Refine, Accumulate + // Kind 1: entropy - Refine, Accumulate if (kind === FetchKind.Entropy) { if (ext.context === FetchContext.IsAuthorized) { return null; @@ -317,7 +317,7 @@ export class Fetch implements HostCallHandler { return ext.entropy(); } - // Kind 2: authorizer trace — Refine only + // Kind 2: authorizer trace - Refine only if (kind === FetchKind.AuthorizerTrace) { if (ext.context !== FetchContext.Refine) { return null; @@ -325,7 +325,7 @@ export class Fetch implements HostCallHandler { return ext.authorizerTrace(); } - // Kind 3: other work item extrinsics — Refine only + // Kind 3: other work item extrinsics - Refine only if (kind === FetchKind.OtherWorkItemExtrinsics) { if (ext.context !== FetchContext.Refine) { return null; @@ -335,7 +335,7 @@ export class Fetch implements HostCallHandler { return ext.workItemExtrinsic(workItem, index); } - // Kind 4: my extrinsics — Refine only + // Kind 4: my extrinsics - Refine only if (kind === FetchKind.MyExtrinsics) { if (ext.context !== FetchContext.Refine) { return null; @@ -344,7 +344,7 @@ export class Fetch implements HostCallHandler { return ext.workItemExtrinsic(null, index); } - // Kind 5: other work item imports — Refine only + // Kind 5: other work item imports - Refine only if (kind === FetchKind.OtherWorkItemImports) { if (ext.context !== FetchContext.Refine) { return null; @@ -354,7 +354,7 @@ export class Fetch implements HostCallHandler { return ext.workItemImport(workItem, index); } - // Kind 6: my imports — Refine only + // Kind 6: my imports - Refine only if (kind === FetchKind.MyImports) { if (ext.context !== FetchContext.Refine) { return null; @@ -363,7 +363,7 @@ export class Fetch implements HostCallHandler { return ext.workItemImport(null, index); } - // Kind 7: work package — Is-Authorized, Refine + // Kind 7: work package - IsAuthorized, Refine if (kind === FetchKind.WorkPackage) { if (ext.context === FetchContext.Accumulate) { return null; @@ -371,23 +371,23 @@ export class Fetch implements HostCallHandler { return ext.workPackage(); } - // Kind 8: authorizer — Is-Authorized, Refine - if (kind === FetchKind.Authorizer) { + // Kind 8: auth configuration - IsAuthorized, Refine + if (kind === FetchKind.AuthConfiguration) { if (ext.context === FetchContext.Accumulate) { return null; } - return ext.authorizer(); + return ext.authConfiguration(); } - // Kind 9: authorization token — Is-Authorized, Refine - if (kind === FetchKind.AuthorizationToken) { + // Kind 9: authorization token - IsAuthorized, Refine + if (kind === FetchKind.AuthToken) { if (ext.context === FetchContext.Accumulate) { return null; } - return ext.authorizationToken(); + return ext.authToken(); } - // Kind 10: refine context — Is-Authorized, Refine + // Kind 10: refine context - IsAuthorized, Refine if (kind === FetchKind.RefineContext) { if (ext.context === FetchContext.Accumulate) { return null; @@ -395,7 +395,7 @@ export class Fetch implements HostCallHandler { return ext.refineContext(); } - // Kind 11: all work items — Is-Authorized, Refine + // Kind 11: all work items - IsAuthorized, Refine if (kind === FetchKind.AllWorkItems) { if (ext.context === FetchContext.Accumulate) { return null; @@ -403,7 +403,7 @@ export class Fetch implements HostCallHandler { return ext.allWorkItems(); } - // Kind 12: one work item — Is-Authorized, Refine + // Kind 12: one work item - IsAuthorized, Refine if (kind === FetchKind.OneWorkItem) { if (ext.context === FetchContext.Accumulate) { return null; @@ -412,7 +412,7 @@ export class Fetch implements HostCallHandler { return ext.oneWorkItem(workItem); } - // Kind 13: work item payload — Is-Authorized, Refine + // Kind 13: work item payload - IsAuthorized, Refine if (kind === FetchKind.WorkItemPayload) { if (ext.context === FetchContext.Accumulate) { return null; @@ -421,7 +421,7 @@ export class Fetch implements HostCallHandler { return ext.workItemPayload(workItem); } - // Kind 14: all transfers and operands — Accumulate only + // Kind 14: all transfers and operands - Accumulate only if (kind === FetchKind.AllTransfersAndOperands) { if (ext.context !== FetchContext.Accumulate) { return null; @@ -429,7 +429,7 @@ export class Fetch implements HostCallHandler { return ext.allTransfersAndOperands(); } - // Kind 15: one transfer or operand — Accumulate only + // Kind 15: one transfer or operand - Accumulate only if (kind === FetchKind.OneTransferOrOperand) { if (ext.context !== FetchContext.Accumulate) { return null; @@ -451,8 +451,8 @@ export enum FetchKind { OtherWorkItemImports = 5, MyImports = 6, WorkPackage = 7, - Authorizer = 8, - AuthorizationToken = 9, + AuthConfiguration = 8, + AuthToken = 9, RefineContext = 10, AllWorkItems = 11, OneWorkItem = 12, diff --git a/packages/jam/transition/externalities/index.ts b/packages/jam/transition/externalities/index.ts index 189228ba8..c2b85dc83 100644 --- a/packages/jam/transition/externalities/index.ts +++ b/packages/jam/transition/externalities/index.ts @@ -1,4 +1,5 @@ export * from "./accumulate-externalities.js"; export * from "./accumulate-fetch-externalities.js"; export * from "./fetch-externalities.js"; +export * from "./is-authorized-fetch-externalities.js"; export * from "./refine-fetch-externalities.js"; diff --git a/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts b/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts new file mode 100644 index 000000000..effefbfc9 --- /dev/null +++ b/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts @@ -0,0 +1,54 @@ +import { BytesBlob } from "@typeberry/bytes"; +import type { ChainSpec } from "@typeberry/config"; +import { general } from "@typeberry/jam-host-calls"; +import type { U64 } from "@typeberry/numbers"; +import { getEncodedConstants } from "./fetch-externalities.js"; + +export class IsAuthorizedFetchExternalities implements general.IIsAuthorizedFetch { + readonly context = general.FetchContext.IsAuthorized; + + constructor( + private readonly chainSpec: ChainSpec, + private readonly params: { + authToken: BytesBlob; + authConfiguration: BytesBlob; + }, + ) {} + + constants(): BytesBlob { + return getEncodedConstants(this.chainSpec); + } + + // TODO [ToDr] Return encoded work package E(p) + workPackage(): BytesBlob { + return BytesBlob.empty(); + } + + authConfiguration(): BytesBlob { + return this.params.authConfiguration; + } + + authToken(): BytesBlob { + return this.params.authToken; + } + + // TODO [ToDr] Return encoded refinement context + refineContext(): BytesBlob { + return BytesBlob.empty(); + } + + // TODO [ToDr] Return encoded work items + allWorkItems(): BytesBlob { + return BytesBlob.empty(); + } + + // TODO [ToDr] Return single work item summary + oneWorkItem(_workItem: U64): BytesBlob | null { + return null; + } + + // TODO [ToDr] Return work item payload + workItemPayload(_workItem: U64): BytesBlob | null { + return null; + } +} diff --git a/packages/jam/transition/externalities/refine-fetch-externalities.ts b/packages/jam/transition/externalities/refine-fetch-externalities.ts index 2d5d50adc..ea5a05627 100644 --- a/packages/jam/transition/externalities/refine-fetch-externalities.ts +++ b/packages/jam/transition/externalities/refine-fetch-externalities.ts @@ -1,5 +1,5 @@ import type { EntropyHash } from "@typeberry/block"; -import { Bytes, type BytesBlob } from "@typeberry/bytes"; +import { Bytes, BytesBlob } from "@typeberry/bytes"; import type { ChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; import { general } from "@typeberry/jam-host-calls"; @@ -20,36 +20,44 @@ export class RefineFetchExternalities implements general.IRefineFetch { return Bytes.zero(HASH_SIZE).asOpaque(); } - authorizerTrace(): BytesBlob | null { - return null; + // TODO [ToDr] implement + authorizerTrace(): BytesBlob { + return BytesBlob.empty(); } + // TODO [ToDr] implement workItemExtrinsic(_workItem: U64 | null, _index: U64): BytesBlob | null { return null; } + // TODO [ToDr] implement workItemImport(_workItem: U64 | null, _index: U64): BytesBlob | null { return null; } - workPackage(): BytesBlob | null { - return null; + // TODO [ToDr] implement + workPackage(): BytesBlob { + return BytesBlob.empty(); } - authorizer(): BytesBlob | null { - return null; + // TODO [ToDr] implement + authConfiguration(): BytesBlob { + return BytesBlob.empty(); } - authorizationToken(): BytesBlob | null { - return null; + // TODO [ToDr] implement + authToken(): BytesBlob { + return BytesBlob.empty(); } - refineContext(): BytesBlob | null { - return null; + // TODO [ToDr] implement + refineContext(): BytesBlob { + return BytesBlob.empty(); } - allWorkItems(): BytesBlob | null { - return null; + // TODO [ToDr] implement + allWorkItems(): BytesBlob { + return BytesBlob.empty(); } oneWorkItem(_workItem: U64): BytesBlob | null {