From 62b03c26c2d805987b8229cae156985e05d47adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 20 Apr 2026 13:40:40 +0200 Subject: [PATCH 1/6] Refine fetch externalities. --- packages/jam/in-core/in-core.ts | 15 +- packages/jam/in-core/is-authorized.test.ts | 59 ++-- packages/jam/in-core/is-authorized.ts | 15 +- packages/jam/in-core/refine.ts | 20 +- .../externalities/fetch-externalities.ts | 50 +++- .../is-authorized-fetch-externalities.test.ts | 110 +++++++ .../is-authorized-fetch-externalities.ts | 50 ++-- .../refine-fetch-externalities.test.ts | 281 ++++++++++++++++-- .../refine-fetch-externalities.ts | 109 +++++-- 9 files changed, 589 insertions(+), 120 deletions(-) create mode 100644 packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts diff --git a/packages/jam/in-core/in-core.ts b/packages/jam/in-core/in-core.ts index e4bf46286..58ca7fbb7 100644 --- a/packages/jam/in-core/in-core.ts +++ b/packages/jam/in-core/in-core.ts @@ -66,9 +66,7 @@ export class InCore { extrinsics: PerWorkItem, ): Promise> { const workPackageHash = workPackageAndHash.hash; - const { context, authToken, authCodeHash, authCodeHost, authConfiguration, items, ...rest } = - workPackageAndHash.data; - assertEmpty(rest); + const { context, items } = workPackageAndHash.data; // TODO [ToDr] Verify BEEFY root // TODO [ToDr] Verify prerequisites @@ -108,14 +106,7 @@ export class InCore { } // Check authorization - const authResult = await this.isAuthorized.invoke( - state, - core, - authToken, - authCodeHost, - authCodeHash, - authConfiguration, - ); + const authResult = await this.isAuthorized.invoke(state, core, workPackageAndHash.data); if (authResult.isError) { return Result.error( RefineError.AuthorizationError, @@ -134,6 +125,7 @@ export class InCore { const result = await this.refineItem.invoke( state, lookupState, + workPackageAndHash.data, idx, item, imports, @@ -141,6 +133,7 @@ export class InCore { core, workPackageHash, exportOffset, + authResult.ok.authorizationOutput, ); refineResults.push(result); exportOffset += result.exports.length; diff --git a/packages/jam/in-core/is-authorized.test.ts b/packages/jam/in-core/is-authorized.test.ts index 866e6eba8..ac3f09d14 100644 --- a/packages/jam/in-core/is-authorized.test.ts +++ b/packages/jam/in-core/is-authorized.test.ts @@ -4,14 +4,47 @@ 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 { RefineContext } from "@typeberry/block/refine-context.js"; +import { WorkItem } from "@typeberry/block/work-item.js"; +import { tryAsWorkItemsCount, WorkPackage } from "@typeberry/block/work-package.js"; import { Bytes, BytesBlob } from "@typeberry/bytes"; -import { HashDictionary } from "@typeberry/collections"; +import { asKnownSize, FixedSizeArray, 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 { tryAsU16, tryAsU32, tryAsU64 } from "@typeberry/numbers"; import { InMemoryService, InMemoryState, PreimageItem, ServiceAccountInfo } from "@typeberry/state"; import { AuthorizationError, IsAuthorized } from "./is-authorized.js"; +function buildPackage(authCodeHash: CodeHash, authToken: BytesBlob, authConfiguration: BytesBlob): WorkPackage { + const items = [ + WorkItem.create({ + service: tryAsServiceId(1), + codeHash: Bytes.zero(HASH_SIZE).asOpaque(), + refineGasLimit: tryAsServiceGas(1_000_000), + accumulateGasLimit: tryAsServiceGas(1_000_000), + exportCount: tryAsU16(0), + payload: BytesBlob.empty(), + importSegments: asKnownSize([]), + extrinsic: [], + }), + ]; + return WorkPackage.create({ + authToken, + authCodeHost: AUTH_SERVICE_ID, + authCodeHash, + authConfiguration, + context: RefineContext.create({ + anchor: Bytes.zero(HASH_SIZE).asOpaque(), + stateRoot: Bytes.zero(HASH_SIZE).asOpaque(), + beefyRoot: Bytes.zero(HASH_SIZE).asOpaque(), + lookupAnchor: Bytes.zero(HASH_SIZE).asOpaque(), + lookupAnchorSlot: tryAsTimeSlot(16), + prerequisites: [], + }), + items: FixedSizeArray.new(items, tryAsWorkItemsCount(1)), + }); +} + let blake2b: Blake2b; before(async () => { @@ -67,7 +100,7 @@ describe("IsAuthorized", () => { 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); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), buildPackage(authCodeHash, token, token)); assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); @@ -94,10 +127,7 @@ describe("IsAuthorized", () => { const result = await isAuthorized.invoke( state, tryAsCoreIndex(0), - BytesBlob.empty(), - AUTH_SERVICE_ID, - authCodeHash, - BytesBlob.empty(), + buildPackage(authCodeHash, BytesBlob.empty(), BytesBlob.empty()), ); assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); @@ -113,10 +143,7 @@ describe("IsAuthorized", () => { const result = await isAuthorized.invoke( state, tryAsCoreIndex(0), - BytesBlob.blobFromString("wrong"), - AUTH_SERVICE_ID, - authCodeHash, - BytesBlob.blobFromString("right"), + buildPackage(authCodeHash, BytesBlob.blobFromString("wrong"), BytesBlob.blobFromString("right")), ); assert.strictEqual(result.isError, true); @@ -134,10 +161,7 @@ describe("IsAuthorized", () => { const result = await isAuthorized.invoke( state, tryAsCoreIndex(0), - BytesBlob.empty(), - AUTH_SERVICE_ID, - authCodeHash, - BytesBlob.empty(), + buildPackage(authCodeHash, BytesBlob.empty(), BytesBlob.empty()), ); assert.strictEqual(result.isError, true); @@ -173,10 +197,7 @@ describe("IsAuthorized", () => { const result = await isAuthorized.invoke( state, tryAsCoreIndex(0), - BytesBlob.empty(), - AUTH_SERVICE_ID, - authCodeHash, - BytesBlob.empty(), + buildPackage(authCodeHash, BytesBlob.empty(), BytesBlob.empty()), ); assert.strictEqual(result.isError, true); diff --git a/packages/jam/in-core/is-authorized.ts b/packages/jam/in-core/is-authorized.ts index 1469c09dc..4624e7adb 100644 --- a/packages/jam/in-core/is-authorized.ts +++ b/packages/jam/in-core/is-authorized.ts @@ -1,6 +1,7 @@ -import { type CodeHash, type CoreIndex, type ServiceGas, type ServiceId, tryAsServiceGas } from "@typeberry/block"; +import { type CoreIndex, type ServiceGas, 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 type { WorkPackage } from "@typeberry/block/work-package.js"; import { BytesBlob } from "@typeberry/bytes"; import { codec, Encoder } from "@typeberry/codec"; import type { ChainSpec, PvmBackend } from "@typeberry/config"; @@ -44,11 +45,10 @@ export class IsAuthorized { async invoke( state: State, coreIndex: CoreIndex, - authToken: BytesBlob, - authCodeHost: ServiceId, - authCodeHash: CodeHash, - authConfiguration: BytesBlob, + workPackage: WorkPackage, ): Promise> { + const { authCodeHost, authCodeHash, authConfiguration } = workPackage; + // 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 @@ -77,10 +77,7 @@ export class IsAuthorized { } // Prepare fetch externalities and executor - const fetchExternalities = new IsAuthorizedFetchExternalities(this.chainSpec, { - authToken, - authConfiguration, - }); + const fetchExternalities = new IsAuthorizedFetchExternalities(this.chainSpec, workPackage); const executor = await PvmExecutor.createIsAuthorizedExecutor( authCodeHost, code, diff --git a/packages/jam/in-core/refine.ts b/packages/jam/in-core/refine.ts index 0c76c925d..0fb6d96ed 100644 --- a/packages/jam/in-core/refine.ts +++ b/packages/jam/in-core/refine.ts @@ -10,6 +10,7 @@ import { 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 type { WorkPackage } from "@typeberry/block/work-package.js"; import { WorkExecResult, WorkExecResultKind, WorkRefineLoad, WorkResult } from "@typeberry/block/work-result.js"; import { BytesBlob } from "@typeberry/bytes"; import { codec, Encoder } from "@typeberry/codec"; @@ -73,6 +74,7 @@ export class Refine { async invoke( state: State, lookupState: State, + workPackage: WorkPackage, idx: number, item: WorkItem, allImports: PerWorkItem, @@ -80,6 +82,7 @@ export class Refine { coreIndex: CoreIndex, workPackageHash: WorkPackageHash, exportOffset: number, + authorizerTrace: BytesBlob, ): Promise { const payloadHash = this.blake2b.hashBytes(item.payload); const baseResult = { @@ -118,12 +121,14 @@ export class Refine { const code = maybeCode.ok; const externalities = this.createRefineExternalities({ - payload: item.payload, + workPackage, + currentWorkItemIndex: idx, imports: allImports, extrinsics: allExtrinsics, currentServiceId: item.service, lookupState, exportOffset, + authorizerTrace, }); const executor = await PvmExecutor.createRefineExecutor(item.service, code, externalities, this.pvmBackend); @@ -229,15 +234,22 @@ export class Refine { } private createRefineExternalities(args: { - payload: BytesBlob; + workPackage: WorkPackage; + currentWorkItemIndex: number; imports: PerWorkItem; extrinsics: PerWorkItem; currentServiceId: ServiceId; lookupState: State; exportOffset: number; + authorizerTrace: BytesBlob; }): RefineHostCallExternalities { - // TODO [ToDr] Pass all required fetch data - const fetchExternalities = new RefineFetchExternalities(this.chainSpec); + const fetchExternalities = new RefineFetchExternalities(this.chainSpec, { + workPackage: args.workPackage, + currentWorkItemIndex: args.currentWorkItemIndex, + imports: args.imports, + extrinsics: args.extrinsics, + authorizerTrace: args.authorizerTrace, + }); const refine = RefineExternalitiesImpl.create({ currentServiceId: args.currentServiceId, lookupState: args.lookupState, diff --git a/packages/jam/transition/externalities/fetch-externalities.ts b/packages/jam/transition/externalities/fetch-externalities.ts index 3ceff7acf..e406b7445 100644 --- a/packages/jam/transition/externalities/fetch-externalities.ts +++ b/packages/jam/transition/externalities/fetch-externalities.ts @@ -1,10 +1,13 @@ +import type { CodeHash, ServiceGas, ServiceId } from "@typeberry/block"; import { G_I, MAX_REPORT_DEPENDENCIES, O, Q, T, W_A, W_B, W_C, W_M, W_T, W_X } from "@typeberry/block/gp-constants.js"; +import type { WorkItem } from "@typeberry/block/work-item.js"; import { MAX_NUMBER_OF_WORK_ITEMS } from "@typeberry/block/work-package.js"; -import type { BytesBlob } from "@typeberry/bytes"; +import { BytesBlob } from "@typeberry/bytes"; import { codec, Encoder } from "@typeberry/codec"; import type { ChainSpec } from "@typeberry/config"; +import { HASH_SIZE } from "@typeberry/hash"; import { PendingTransfer } from "@typeberry/jam-host-calls"; -import { tryAsU16, tryAsU32, tryAsU64 } from "@typeberry/numbers"; +import { tryAsU16, tryAsU32, tryAsU64, type U64 } from "@typeberry/numbers"; import { BASE_SERVICE_BALANCE, ELECTIVE_BYTE_BALANCE, @@ -124,3 +127,46 @@ export function getEncodedConstants(chainSpec: ChainSpec) { return encodedConsts; } + +/** + * `S(w)` — work-item summary used by fetch kinds 11/12 in both the IsAuthorized + * and Refine contexts. Fixed length of 62 bytes per item. + * + * https://graypaper.fluffylabs.dev/#/ab2cdbd/31fc0231fc02?v=0.7.2 + */ +const WORK_ITEM_SUMMARY_CODEC = codec.object({ + service: codec.u32.asOpaque(), + codeHash: codec.bytes(HASH_SIZE).asOpaque(), + refineGasLimit: codec.u64.asOpaque(), + accumulateGasLimit: codec.u64.asOpaque(), + exportCount: codec.u16, + importSegmentsCount: codec.u16, + extrinsicCount: codec.u16, + payloadLength: codec.u32, +}); + +export function encodeWorkItemSummary(item: WorkItem): BytesBlob { + return Encoder.encodeObject(WORK_ITEM_SUMMARY_CODEC, { + service: item.service, + codeHash: item.codeHash, + refineGasLimit: item.refineGasLimit, + accumulateGasLimit: item.accumulateGasLimit, + exportCount: item.exportCount, + importSegmentsCount: tryAsU16(item.importSegments.length), + extrinsicCount: tryAsU16(item.extrinsic.length), + payloadLength: tryAsU32(item.payload.length), + }); +} + +/** Kind 11 — concatenation of `S(w)` for every item in the package. */ +export function encodeAllWorkItemSummaries(items: readonly WorkItem[]): BytesBlob { + return BytesBlob.blobFromParts(items.map((i) => encodeWorkItemSummary(i).raw)); +} + +/** Converts u64 value taken from a register into valid index of array of given `length`. */ +export function u64ToArrayIndex(v: U64, len: number): number | null { + if (v >= BigInt(len)) { + return null; + } + return Number(v); +} diff --git a/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts b/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts new file mode 100644 index 000000000..5cb999bcd --- /dev/null +++ b/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts @@ -0,0 +1,110 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; + +import type { CodeHash } from "@typeberry/block"; +import { tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/block"; +import { RefineContext } from "@typeberry/block/refine-context.js"; +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 { fullChainSpec, tinyChainSpec } from "@typeberry/config"; +import { HASH_SIZE } from "@typeberry/hash"; +import { tryAsU16, tryAsU64 } from "@typeberry/numbers"; +import { IsAuthorizedFetchExternalities } from "./is-authorized-fetch-externalities.js"; + +function buildWorkItem(overrides: { service?: number; payloadLen?: number } = {}) { + return WorkItem.create({ + service: tryAsServiceId(overrides.service ?? 1), + codeHash: Bytes.fill(HASH_SIZE, 7).asOpaque(), + payload: BytesBlob.blobFrom(new Uint8Array(overrides.payloadLen ?? 3).fill(0xab)), + refineGasLimit: tryAsServiceGas(1_000_000), + accumulateGasLimit: tryAsServiceGas(2_000_000), + importSegments: asKnownSize([]), + extrinsic: [], + exportCount: tryAsU16(0), + }); +} + +function buildPackage(items: WorkItem[] = [buildWorkItem({})]) { + return WorkPackage.create({ + authToken: BytesBlob.blobFrom(new Uint8Array([1, 2, 3])), + authCodeHost: tryAsServiceId(42), + authCodeHash: Bytes.fill(HASH_SIZE, 9).asOpaque(), + authConfiguration: BytesBlob.blobFrom(new Uint8Array([4, 5, 6, 7])), + context: RefineContext.create({ + anchor: Bytes.fill(HASH_SIZE, 1).asOpaque(), + stateRoot: Bytes.fill(HASH_SIZE, 2).asOpaque(), + beefyRoot: Bytes.fill(HASH_SIZE, 3).asOpaque(), + lookupAnchor: Bytes.fill(HASH_SIZE, 4).asOpaque(), + lookupAnchorSlot: tryAsTimeSlot(16), + prerequisites: [], + }), + items: FixedSizeArray.new(items, tryAsWorkItemsCount(items.length)), + }); +} + +describe("IsAuthorizedFetchExternalities", () => { + it("returns different constants for different chain specs", () => { + const tinyExt = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); + const fullExt = new IsAuthorizedFetchExternalities(fullChainSpec, buildPackage()); + assert.notStrictEqual(tinyExt.constants().length, 0); + assert.notDeepStrictEqual(tinyExt.constants(), fullExt.constants()); + }); + + it("returns encoded work package", () => { + const pkg = buildPackage(); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, pkg); + const expected = Encoder.encodeObject(WorkPackage.Codec, pkg, tinyChainSpec); + assert.deepStrictEqual(ext.workPackage().raw, expected.raw); + }); + + it("returns auth configuration and auth token from the package", () => { + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); + assert.deepStrictEqual(ext.authConfiguration().raw, new Uint8Array([4, 5, 6, 7])); + assert.deepStrictEqual(ext.authToken().raw, new Uint8Array([1, 2, 3])); + }); + + it("returns encoded refine context", () => { + const pkg = buildPackage(); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, pkg); + const expected = Encoder.encodeObject(RefineContext.Codec, pkg.context); + assert.deepStrictEqual(ext.refineContext().raw, expected.raw); + }); + + it("returns concatenated work item summaries with 62 bytes per item", () => { + const items = [buildWorkItem({ service: 1 }), buildWorkItem({ service: 2, payloadLen: 5 })]; + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage(items)); + assert.strictEqual(ext.allWorkItems().length, 62 * items.length); + }); + + it("returns a single work item summary (kind 12)", () => { + const items = [buildWorkItem({ service: 1 }), buildWorkItem({ service: 2, payloadLen: 10 })]; + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage(items)); + const one = ext.oneWorkItem(tryAsU64(1)); + assert.ok(one !== null); + assert.strictEqual(one.length, 62); + const serviceId = new DataView(one.raw.buffer, one.raw.byteOffset, 4).getUint32(0, true); + assert.strictEqual(serviceId, 2); + }); + + it("returns null for one work item when index is out of range", () => { + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); + assert.strictEqual(ext.oneWorkItem(tryAsU64(99)), null); + }); + + it("returns the raw payload of a work item (kind 13)", () => { + const items = [buildWorkItem({ service: 1, payloadLen: 2 }), buildWorkItem({ service: 2, payloadLen: 5 })]; + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage(items)); + const payload = ext.workItemPayload(tryAsU64(1)); + assert.ok(payload !== null); + assert.strictEqual(payload.length, 5); + assert.ok(payload.raw.every((x) => x === 0xab)); + }); + + it("returns null for payload when index is out of range", () => { + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); + assert.strictEqual(ext.workItemPayload(tryAsU64(99)), null); + }); +}); diff --git a/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts b/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts index effefbfc9..b0ee6edde 100644 --- a/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts +++ b/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts @@ -1,54 +1,64 @@ -import { BytesBlob } from "@typeberry/bytes"; +import { RefineContext } from "@typeberry/block/refine-context.js"; +import { WorkPackage } from "@typeberry/block/work-package.js"; +import type { BytesBlob } from "@typeberry/bytes"; +import { Encoder } from "@typeberry/codec"; 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"; +import { + encodeAllWorkItemSummaries, + encodeWorkItemSummary, + getEncodedConstants, + u64ToArrayIndex, +} 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; - }, + private readonly pkg: WorkPackage, ) {} constants(): BytesBlob { return getEncodedConstants(this.chainSpec); } - // TODO [ToDr] Return encoded work package E(p) workPackage(): BytesBlob { - return BytesBlob.empty(); + return Encoder.encodeObject(WorkPackage.Codec, this.pkg, this.chainSpec); } authConfiguration(): BytesBlob { - return this.params.authConfiguration; + return this.pkg.authConfiguration; } authToken(): BytesBlob { - return this.params.authToken; + return this.pkg.authToken; } - // TODO [ToDr] Return encoded refinement context refineContext(): BytesBlob { - return BytesBlob.empty(); + return Encoder.encodeObject(RefineContext.Codec, this.pkg.context); } - // TODO [ToDr] Return encoded work items allWorkItems(): BytesBlob { - return BytesBlob.empty(); + return encodeAllWorkItemSummaries(this.pkg.items); } - // TODO [ToDr] Return single work item summary - oneWorkItem(_workItem: U64): BytesBlob | null { - return null; + oneWorkItem(workItem: U64): BytesBlob | null { + const items = this.pkg.items; + const idx = u64ToArrayIndex(workItem, items.length); + if (idx === null) { + return null; + } + return encodeWorkItemSummary(items[idx]); } - // TODO [ToDr] Return work item payload - workItemPayload(_workItem: U64): BytesBlob | null { - return null; + workItemPayload(workItem: U64): BytesBlob | null { + const items = this.pkg.items; + const idx = u64ToArrayIndex(workItem, items.length); + if (idx === null) { + return null; + } + return items[idx].payload; } } diff --git a/packages/jam/transition/externalities/refine-fetch-externalities.test.ts b/packages/jam/transition/externalities/refine-fetch-externalities.test.ts index f14bb3e3c..f60bdbf8d 100644 --- a/packages/jam/transition/externalities/refine-fetch-externalities.test.ts +++ b/packages/jam/transition/externalities/refine-fetch-externalities.test.ts @@ -1,38 +1,261 @@ import assert from "node:assert"; import { describe, it } from "node:test"; +import type { CodeHash } from "@typeberry/block"; +import { tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/block"; +import { RefineContext } from "@typeberry/block/refine-context.js"; +import type { WorkItemExtrinsic } from "@typeberry/block/work-item.js"; +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 { type ChainSpec, fullChainSpec, tinyChainSpec } from "@typeberry/config"; +import { HASH_SIZE } from "@typeberry/hash"; +import { tryAsU16, tryAsU32, tryAsU64 } from "@typeberry/numbers"; +import { asOpaqueType } from "@typeberry/utils"; +import type { ImportedSegment, PerWorkItem } from "../../in-core/refine.js"; import { RefineFetchExternalities } from "./refine-fetch-externalities.js"; -describe("RefineFetchExternalities", () => { - const prepareRefineData = ({ chainSpec }: { chainSpec?: ChainSpec } = {}) => { - const defaultChainSpec = tinyChainSpec; - return new RefineFetchExternalities(chainSpec ?? defaultChainSpec); - }; +const asExtrinsic = (bytes: BytesBlob): WorkItemExtrinsic => asOpaqueType(bytes); + +function buildWorkItem(overrides: { + service?: number; + payloadLen?: number; + exportCount?: number; + importCount?: number; + extrinsicCount?: number; +}) { + const codeHash = Bytes.fill(HASH_SIZE, 7).asOpaque(); + return WorkItem.create({ + service: tryAsServiceId(overrides.service ?? 1), + codeHash, + payload: BytesBlob.blobFrom(new Uint8Array(overrides.payloadLen ?? 3).fill(0xab)), + refineGasLimit: tryAsServiceGas(1_000_000), + accumulateGasLimit: tryAsServiceGas(2_000_000), + importSegments: asKnownSize(new Array(overrides.importCount ?? 0)), + extrinsic: new Array(overrides.extrinsicCount ?? 0), + exportCount: tryAsU16(overrides.exportCount ?? 0), + }); +} + +function buildWorkPackage(items: WorkItem[]) { + return WorkPackage.create({ + authToken: BytesBlob.blobFrom(new Uint8Array([1, 2, 3])), + authCodeHost: tryAsServiceId(42), + authCodeHash: Bytes.fill(HASH_SIZE, 9).asOpaque(), + authConfiguration: BytesBlob.blobFrom(new Uint8Array([4, 5, 6, 7])), + context: RefineContext.create({ + anchor: Bytes.fill(HASH_SIZE, 1).asOpaque(), + stateRoot: Bytes.fill(HASH_SIZE, 2).asOpaque(), + beefyRoot: Bytes.fill(HASH_SIZE, 3).asOpaque(), + lookupAnchor: Bytes.fill(HASH_SIZE, 4).asOpaque(), + lookupAnchorSlot: tryAsTimeSlot(16), + prerequisites: [], + }), + items: FixedSizeArray.new(items, tryAsWorkItemsCount(items.length)), + }); +} + +function prepareRefineData( + opts: { + chainSpec?: ChainSpec; + items?: WorkItem[]; + currentWorkItemIndex?: number; + extrinsics?: PerWorkItem; + imports?: PerWorkItem; + authorizerTrace?: BytesBlob; + } = {}, +) { + const chainSpec = opts.chainSpec ?? tinyChainSpec; + const items = opts.items ?? [buildWorkItem({})]; + const workPackage = buildWorkPackage(items); + return new RefineFetchExternalities(chainSpec, { + workPackage, + currentWorkItemIndex: opts.currentWorkItemIndex ?? 0, + imports: opts.imports ?? asKnownSize(items.map(() => [])), + extrinsics: opts.extrinsics ?? asKnownSize(items.map(() => [])), + authorizerTrace: opts.authorizerTrace ?? BytesBlob.empty(), + }); +} +describe("RefineFetchExternalities", () => { it("should return different constants for different chain specs", () => { - const tinyFetchExternalities = prepareRefineData({ chainSpec: tinyChainSpec }); - const fullFetchExternalities = prepareRefineData({ chainSpec: fullChainSpec }); - - const tinyConstants = tinyFetchExternalities.constants(); - const fullConstants = fullFetchExternalities.constants(); - - assert.notStrictEqual(tinyConstants.length, 0); - assert.notStrictEqual(fullConstants.length, 0); - assert.notDeepStrictEqual(tinyConstants, fullConstants); - }); - - // Pending implementation — these should assert against real fixture values once - // RefineFetchExternalities accepts and exposes the required refine inputs. - it.todo("should return entropy (H₀ header hash of anchor block)"); - it.todo("should return authorizer trace"); - it.todo("should return work item extrinsic"); - it.todo("should return work item import"); - it.todo("should return work package"); - it.todo("should return authorizer"); - it.todo("should return authorization token"); - it.todo("should return refine context"); - it.todo("should return all work items"); - it.todo("should return one work item"); - it.todo("should return work item payload"); + const tinyExt = prepareRefineData({ chainSpec: tinyChainSpec }); + const fullExt = prepareRefineData({ chainSpec: fullChainSpec }); + + assert.notStrictEqual(tinyExt.constants().length, 0); + assert.notStrictEqual(fullExt.constants().length, 0); + assert.notDeepStrictEqual(tinyExt.constants(), fullExt.constants()); + }); + + it("should return entropy H_0 (zero hash) per GP §B.3", () => { + const ext = prepareRefineData(); + const entropy = ext.entropy(); + assert.strictEqual(entropy.length, HASH_SIZE); + assert.ok(entropy.isEqualTo(Bytes.zero(HASH_SIZE).asOpaque())); + }); + + it("should return the supplied authorizer trace", () => { + const trace = BytesBlob.blobFrom(new Uint8Array([0xaa, 0xbb, 0xcc])); + const ext = prepareRefineData({ authorizerTrace: trace }); + assert.deepStrictEqual(ext.authorizerTrace().raw, trace.raw); + }); + + it("should return an extrinsic by work item index and extrinsic index", () => { + const items = [buildWorkItem({}), buildWorkItem({ service: 2 })]; + const extrinsics: PerWorkItem = asKnownSize([ + [asExtrinsic(BytesBlob.blobFrom(new Uint8Array([1])))], + [ + asExtrinsic(BytesBlob.blobFrom(new Uint8Array([2, 2]))), + asExtrinsic(BytesBlob.blobFrom(new Uint8Array([3, 3, 3]))), + ], + ]); + const ext = prepareRefineData({ items, extrinsics }); + + const other = ext.workItemExtrinsic(tryAsU64(1), tryAsU64(1)); + assert.ok(other !== null); + assert.deepStrictEqual(other.raw, new Uint8Array([3, 3, 3])); + }); + + it("should return current item's extrinsic when workItem is null", () => { + const items = [buildWorkItem({}), buildWorkItem({ service: 2 })]; + const extrinsics: PerWorkItem = asKnownSize([ + [asExtrinsic(BytesBlob.blobFrom(new Uint8Array([9])))], + [asExtrinsic(BytesBlob.blobFrom(new Uint8Array([8])))], + ]); + const ext = prepareRefineData({ items, extrinsics, currentWorkItemIndex: 1 }); + + const mine = ext.workItemExtrinsic(null, tryAsU64(0)); + assert.ok(mine !== null); + assert.deepStrictEqual(mine.raw, new Uint8Array([8])); + }); + + it("should return null for out-of-range extrinsic indices", () => { + const items = [buildWorkItem({})]; + const extrinsics: PerWorkItem = asKnownSize([ + [asExtrinsic(BytesBlob.blobFrom(new Uint8Array([1])))], + ]); + const ext = prepareRefineData({ items, extrinsics }); + + assert.strictEqual(ext.workItemExtrinsic(tryAsU64(5), tryAsU64(0)), null); + assert.strictEqual(ext.workItemExtrinsic(tryAsU64(0), tryAsU64(5)), null); + assert.strictEqual(ext.workItemExtrinsic(null, tryAsU64(5)), null); + }); + + it("should treat U64 indices above the safe-integer range as out of range", () => { + const items = [buildWorkItem({})]; + const ext = prepareRefineData({ items }); + const huge = tryAsU64(2n ** 53n); // first value > Number.MAX_SAFE_INTEGER + assert.strictEqual(ext.workItemExtrinsic(huge, tryAsU64(0)), null); + assert.strictEqual(ext.workItemExtrinsic(null, huge), null); + assert.strictEqual(ext.workItemImport(huge, tryAsU64(0)), null); + assert.strictEqual(ext.oneWorkItem(huge), null); + assert.strictEqual(ext.workItemPayload(huge), null); + }); + + it("should return an import segment by work item index and segment index", () => { + const items = [buildWorkItem({}), buildWorkItem({ service: 2 })]; + const segBytes = new Uint8Array(16).fill(0x55); + const imports: PerWorkItem = asKnownSize([ + [], + [{ index: 0 as never, data: Bytes.fromBlob(segBytes, 16) as never }], + ]); + const ext = prepareRefineData({ items, imports }); + + const imp = ext.workItemImport(tryAsU64(1), tryAsU64(0)); + assert.ok(imp !== null); + assert.deepStrictEqual(imp.raw, segBytes); + }); + + it("should return null for out-of-range import indices", () => { + const ext = prepareRefineData(); + assert.strictEqual(ext.workItemImport(tryAsU64(10), tryAsU64(0)), null); + assert.strictEqual(ext.workItemImport(null, tryAsU64(10)), null); + }); + + it("should return encoded work package", () => { + const items = [buildWorkItem({})]; + const ext = prepareRefineData({ items }); + const expected = Encoder.encodeObject(WorkPackage.Codec, buildWorkPackage(items), tinyChainSpec); + assert.deepStrictEqual(ext.workPackage().raw, expected.raw); + }); + + it("should return auth configuration and auth token from the package", () => { + const ext = prepareRefineData(); + assert.deepStrictEqual(ext.authConfiguration().raw, new Uint8Array([4, 5, 6, 7])); + assert.deepStrictEqual(ext.authToken().raw, new Uint8Array([1, 2, 3])); + }); + + it("should return encoded refine context", () => { + const ext = prepareRefineData(); + const context = RefineContext.create({ + anchor: Bytes.fill(HASH_SIZE, 1).asOpaque(), + stateRoot: Bytes.fill(HASH_SIZE, 2).asOpaque(), + beefyRoot: Bytes.fill(HASH_SIZE, 3).asOpaque(), + lookupAnchor: Bytes.fill(HASH_SIZE, 4).asOpaque(), + lookupAnchorSlot: tryAsTimeSlot(16), + prerequisites: [], + }); + const expected = Encoder.encodeObject(RefineContext.Codec, context); + assert.deepStrictEqual(ext.refineContext().raw, expected.raw); + }); + + it("should return concatenated work item summaries (kind 11) with 62 bytes per item", () => { + const items = [ + buildWorkItem({ service: 1, payloadLen: 7, exportCount: 2, importCount: 1, extrinsicCount: 0 }), + buildWorkItem({ service: 2, payloadLen: 4, exportCount: 0, importCount: 0, extrinsicCount: 3 }), + ]; + const ext = prepareRefineData({ items }); + const all = ext.allWorkItems(); + assert.strictEqual(all.length, 62 * items.length); + }); + + it("should return a single work item summary (kind 12)", () => { + const items = [buildWorkItem({ service: 1 }), buildWorkItem({ service: 2, payloadLen: 10 })]; + const ext = prepareRefineData({ items }); + + const one = ext.oneWorkItem(tryAsU64(1)); + assert.ok(one !== null); + assert.strictEqual(one.length, 62); + + // first 4 bytes are the service id (u32 LE). + const serviceId = new DataView(one.raw.buffer, one.raw.byteOffset, 4).getUint32(0, true); + assert.strictEqual(serviceId, 2); + // payload length is the last 4 bytes (u32 LE). + const payloadLen = new DataView(one.raw.buffer, one.raw.byteOffset + 58, 4).getUint32(0, true); + assert.strictEqual(payloadLen, 10); + }); + + it("should return null for one work item when index is out of range", () => { + const ext = prepareRefineData(); + assert.strictEqual(ext.oneWorkItem(tryAsU64(99)), null); + }); + + it("should return the raw payload of a work item (kind 13)", () => { + const items = [buildWorkItem({ service: 1, payloadLen: 2 }), buildWorkItem({ service: 2, payloadLen: 5 })]; + const ext = prepareRefineData({ items }); + const payload = ext.workItemPayload(tryAsU64(1)); + assert.ok(payload !== null); + assert.strictEqual(payload.length, 5); + assert.ok(payload.raw.every((x) => x === 0xab)); + }); + + it("should return null for payload when index is out of range", () => { + const ext = prepareRefineData(); + assert.strictEqual(ext.workItemPayload(tryAsU64(99)), null); + }); + + // guard against silent accidental changes to the helpers — tryAsU32 ensures + // encoded lengths match GP's S(w) spec. + it("uses unsigned little-endian u32 for payload length regardless of platform", () => { + const items = [buildWorkItem({ service: 1, payloadLen: 0x1234 })]; + const ext = prepareRefineData({ items }); + const one = ext.oneWorkItem(tryAsU64(0)); + assert.ok(one !== null); + const payloadLen = new DataView(one.raw.buffer, one.raw.byteOffset + 58, 4).getUint32(0, true); + assert.strictEqual(payloadLen, 0x1234); + // tryAsU32 would throw on negative values + assert.doesNotThrow(() => tryAsU32(0x1234)); + }); }); diff --git a/packages/jam/transition/externalities/refine-fetch-externalities.ts b/packages/jam/transition/externalities/refine-fetch-externalities.ts index ea5a05627..ecb7e24fc 100644 --- a/packages/jam/transition/externalities/refine-fetch-externalities.ts +++ b/packages/jam/transition/externalities/refine-fetch-externalities.ts @@ -1,70 +1,127 @@ import type { EntropyHash } from "@typeberry/block"; -import { Bytes, BytesBlob } from "@typeberry/bytes"; +import { RefineContext } from "@typeberry/block/refine-context.js"; +import type { WorkItemExtrinsic } from "@typeberry/block/work-item.js"; +import { WorkPackage } from "@typeberry/block/work-package.js"; +import { Bytes, type BytesBlob } from "@typeberry/bytes"; +import { Encoder } from "@typeberry/codec"; import type { ChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; import { general } from "@typeberry/jam-host-calls"; import type { U64 } from "@typeberry/numbers"; -import { getEncodedConstants } from "./fetch-externalities.js"; +import type { ImportedSegment, PerWorkItem } from "../../in-core/refine.js"; +import { + encodeAllWorkItemSummaries, + encodeWorkItemSummary, + getEncodedConstants, + u64ToArrayIndex, +} from "./fetch-externalities.js"; + +export type RefineFetchData = { + workPackage: WorkPackage; + /** Index of the work item currently being refined (`i` in GP). */ + currentWorkItemIndex: number; + /** Imports per work item (`ī`). */ + imports: PerWorkItem; + /** Extrinsics per work item (`x̄`). */ + extrinsics: PerWorkItem; + /** Authorizer trace produced by Is-Authorized (`r`). */ + authorizerTrace: BytesBlob; +}; export class RefineFetchExternalities implements general.IRefineFetch { readonly context = general.FetchContext.Refine; - constructor(private readonly chainSpec: ChainSpec) {} + constructor( + private readonly chainSpec: ChainSpec, + private readonly data: RefineFetchData, + ) {} constants(): BytesBlob { return getEncodedConstants(this.chainSpec); } - // Returns H₀ (zero hash) + /** + * Refine entropy is `H₀` (zero hash) per GP §B.3. + * + * https://graypaper.fluffylabs.dev/#/ab2cdbd/2fe0012fe201?v=0.7.2 + */ entropy(): EntropyHash { return Bytes.zero(HASH_SIZE).asOpaque(); } - // TODO [ToDr] implement authorizerTrace(): BytesBlob { - return BytesBlob.empty(); + return this.data.authorizerTrace; } - // TODO [ToDr] implement - workItemExtrinsic(_workItem: U64 | null, _index: U64): BytesBlob | null { - return null; + workItemExtrinsic(workItem: U64 | null, index: U64): BytesBlob | null { + const itemIdx = + workItem === null ? this.data.currentWorkItemIndex : u64ToArrayIndex(workItem, this.data.extrinsics.length); + if (itemIdx === null) { + return null; + } + const perItem = this.data.extrinsics[itemIdx]; + const xIdx = u64ToArrayIndex(index, perItem.length); + if (xIdx === null) { + return null; + } + return perItem[xIdx]; } - // TODO [ToDr] implement - workItemImport(_workItem: U64 | null, _index: U64): BytesBlob | null { - return null; + workItemImport(workItem: U64 | null, index: U64): BytesBlob | null { + const itemIdx = + workItem === null ? this.data.currentWorkItemIndex : u64ToArrayIndex(workItem, this.data.imports.length); + if (itemIdx === null) { + return null; + } + const perItem = this.data.imports[itemIdx]; + const segIdx = u64ToArrayIndex(index, perItem.length); + if (segIdx === null) { + return null; + } + // `Segment` extends `BytesBlob`, so it can be returned directly. + return perItem[segIdx].data; } - // TODO [ToDr] implement workPackage(): BytesBlob { - return BytesBlob.empty(); + return Encoder.encodeObject(WorkPackage.Codec, this.data.workPackage, this.chainSpec); } - // TODO [ToDr] implement authConfiguration(): BytesBlob { - return BytesBlob.empty(); + return this.data.workPackage.authConfiguration; } - // TODO [ToDr] implement authToken(): BytesBlob { - return BytesBlob.empty(); + return this.data.workPackage.authToken; } - // TODO [ToDr] implement refineContext(): BytesBlob { - return BytesBlob.empty(); + return Encoder.encodeObject(RefineContext.Codec, this.data.workPackage.context); } - // TODO [ToDr] implement + /** + * Kind 11: concatenation of `S(w)` for every work item in the package. + * + * https://graypaper.fluffylabs.dev/#/ab2cdbd/31f40231f402?v=0.7.2 + */ allWorkItems(): BytesBlob { - return BytesBlob.empty(); + return encodeAllWorkItemSummaries(this.data.workPackage.items); } - oneWorkItem(_workItem: U64): BytesBlob | null { - return null; + oneWorkItem(workItem: U64): BytesBlob | null { + const items = this.data.workPackage.items; + const idx = u64ToArrayIndex(workItem, items.length); + if (idx === null) { + return null; + } + return encodeWorkItemSummary(items[idx]); } - workItemPayload(_workItem: U64): BytesBlob | null { - return null; + workItemPayload(workItem: U64): BytesBlob | null { + const items = this.data.workPackage.items; + const idx = u64ToArrayIndex(workItem, items.length); + if (idx === null) { + return null; + } + return items[idx].payload; } } From e0bed15af0a19d8f4cf1f5f6ac6c49a6a820b169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 20 Apr 2026 14:42:23 +0200 Subject: [PATCH 2/6] Fix audit --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d50c01279..d6c7fd84b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7271,9 +7271,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { From 1e9aa96138330acffff656f3214e6272c1233941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Mon, 20 Apr 2026 18:42:36 +0200 Subject: [PATCH 3/6] Pre-encode data for fetch --- packages/jam/in-core/in-core.ts | 8 +++- packages/jam/in-core/is-authorized.test.ts | 39 +++++++++--------- packages/jam/in-core/is-authorized.ts | 4 +- packages/jam/in-core/refine.ts | 10 ++--- .../externalities/fetch-externalities.ts | 41 +++++++++++++------ .../is-authorized-fetch-externalities.test.ts | 25 ++++++----- .../is-authorized-fetch-externalities.ts | 34 +++++---------- .../refine-fetch-externalities.test.ts | 18 +++++--- .../refine-fetch-externalities.ts | 41 ++++++------------- 9 files changed, 114 insertions(+), 106 deletions(-) diff --git a/packages/jam/in-core/in-core.ts b/packages/jam/in-core/in-core.ts index 58ca7fbb7..e965d844b 100644 --- a/packages/jam/in-core/in-core.ts +++ b/packages/jam/in-core/in-core.ts @@ -11,6 +11,7 @@ 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 { buildWorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.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"; @@ -105,8 +106,11 @@ export class InCore { ); } + // Eagerly build the per-package fetch data so we pay the encoding cost + const packageFetchData = buildWorkPackageFetchData(this.chainSpec, workPackageAndHash.data); + // Check authorization - const authResult = await this.isAuthorized.invoke(state, core, workPackageAndHash.data); + const authResult = await this.isAuthorized.invoke(state, core, workPackageAndHash.data, packageFetchData); if (authResult.isError) { return Result.error( RefineError.AuthorizationError, @@ -125,7 +129,7 @@ export class InCore { const result = await this.refineItem.invoke( state, lookupState, - workPackageAndHash.data, + packageFetchData, idx, item, imports, diff --git a/packages/jam/in-core/is-authorized.test.ts b/packages/jam/in-core/is-authorized.test.ts index ac3f09d14..99b8a7af3 100644 --- a/packages/jam/in-core/is-authorized.test.ts +++ b/packages/jam/in-core/is-authorized.test.ts @@ -13,8 +13,14 @@ import { PvmBackend, tinyChainSpec } from "@typeberry/config"; import { Blake2b, HASH_SIZE, type OpaqueHash } from "@typeberry/hash"; import { tryAsU16, tryAsU32, tryAsU64 } from "@typeberry/numbers"; import { InMemoryService, InMemoryState, PreimageItem, ServiceAccountInfo } from "@typeberry/state"; +import { buildWorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; import { AuthorizationError, IsAuthorized } from "./is-authorized.js"; +function buildPackageAndFetchData(authCodeHash: CodeHash, authToken: BytesBlob, authConfiguration: BytesBlob) { + const pkg = buildPackage(authCodeHash, authToken, authConfiguration); + return { pkg, fetchData: buildWorkPackageFetchData(tinyChainSpec, pkg) }; +} + function buildPackage(authCodeHash: CodeHash, authToken: BytesBlob, authConfiguration: BytesBlob): WorkPackage { const items = [ WorkItem.create({ @@ -100,7 +106,8 @@ describe("IsAuthorized", () => { const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); const token = BytesBlob.blobFromString("hello"); - const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), buildPackage(authCodeHash, token, token)); + const { pkg, fetchData } = buildPackageAndFetchData(authCodeHash, token, token); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), pkg, fetchData); assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); @@ -124,11 +131,8 @@ describe("IsAuthorized", () => { const state = createStateWithService(authCodeHash, AUTHORIZER_PVM); const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); - const result = await isAuthorized.invoke( - state, - tryAsCoreIndex(0), - buildPackage(authCodeHash, BytesBlob.empty(), BytesBlob.empty()), - ); + const empty = buildPackageAndFetchData(authCodeHash, BytesBlob.empty(), BytesBlob.empty()); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), empty.pkg, empty.fetchData); assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); const outputStr = Buffer.from(result.ok.authorizationOutput.raw).toString("utf8"); @@ -140,11 +144,12 @@ describe("IsAuthorized", () => { const state = createStateWithService(authCodeHash, AUTHORIZER_PVM); const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); - const result = await isAuthorized.invoke( - state, - tryAsCoreIndex(0), - buildPackage(authCodeHash, BytesBlob.blobFromString("wrong"), BytesBlob.blobFromString("right")), + const mismatch = buildPackageAndFetchData( + authCodeHash, + BytesBlob.blobFromString("wrong"), + BytesBlob.blobFromString("right"), ); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), mismatch.pkg, mismatch.fetchData); assert.strictEqual(result.isError, true); assert.strictEqual(result.error, AuthorizationError.PvmFailed); @@ -158,11 +163,8 @@ describe("IsAuthorized", () => { }); const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); - const result = await isAuthorized.invoke( - state, - tryAsCoreIndex(0), - buildPackage(authCodeHash, BytesBlob.empty(), BytesBlob.empty()), - ); + const missing = buildPackageAndFetchData(authCodeHash, BytesBlob.empty(), BytesBlob.empty()); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), missing.pkg, missing.fetchData); assert.strictEqual(result.isError, true); assert.strictEqual(result.error, AuthorizationError.CodeNotFound); @@ -194,11 +196,8 @@ describe("IsAuthorized", () => { }); const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); - const result = await isAuthorized.invoke( - state, - tryAsCoreIndex(0), - buildPackage(authCodeHash, BytesBlob.empty(), BytesBlob.empty()), - ); + const emptyPreimage = buildPackageAndFetchData(authCodeHash, BytesBlob.empty(), BytesBlob.empty()); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), emptyPreimage.pkg, emptyPreimage.fetchData); 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 index 4624e7adb..016de140a 100644 --- a/packages/jam/in-core/is-authorized.ts +++ b/packages/jam/in-core/is-authorized.ts @@ -8,6 +8,7 @@ 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 type { WorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; import { IsAuthorizedFetchExternalities } from "@typeberry/transition/externalities/is-authorized-fetch-externalities.js"; import { Result } from "@typeberry/utils"; @@ -46,6 +47,7 @@ export class IsAuthorized { state: State, coreIndex: CoreIndex, workPackage: WorkPackage, + packageFetchData: WorkPackageFetchData, ): Promise> { const { authCodeHost, authCodeHash, authConfiguration } = workPackage; @@ -77,7 +79,7 @@ export class IsAuthorized { } // Prepare fetch externalities and executor - const fetchExternalities = new IsAuthorizedFetchExternalities(this.chainSpec, workPackage); + const fetchExternalities = new IsAuthorizedFetchExternalities(this.chainSpec, packageFetchData); const executor = await PvmExecutor.createIsAuthorizedExecutor( authCodeHost, code, diff --git a/packages/jam/in-core/refine.ts b/packages/jam/in-core/refine.ts index 0fb6d96ed..fb5910c09 100644 --- a/packages/jam/in-core/refine.ts +++ b/packages/jam/in-core/refine.ts @@ -10,7 +10,6 @@ import { 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 type { WorkPackage } from "@typeberry/block/work-package.js"; import { WorkExecResult, WorkExecResultKind, WorkRefineLoad, WorkResult } from "@typeberry/block/work-result.js"; import { BytesBlob } from "@typeberry/bytes"; import { codec, Encoder } from "@typeberry/codec"; @@ -20,6 +19,7 @@ import { PvmExecutor, type RefineHostCallExternalities, ReturnStatus, type Retur import { type Blake2b, HASH_SIZE } from "@typeberry/hash"; import { tryAsU32 } from "@typeberry/numbers"; import type { State } from "@typeberry/state"; +import type { WorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; import { RefineFetchExternalities } from "@typeberry/transition/externalities/refine-fetch-externalities.js"; import { assertNever, Result } from "@typeberry/utils"; import { RefineExternalitiesImpl } from "./externalities/refine.js"; @@ -74,7 +74,7 @@ export class Refine { async invoke( state: State, lookupState: State, - workPackage: WorkPackage, + packageFetchData: WorkPackageFetchData, idx: number, item: WorkItem, allImports: PerWorkItem, @@ -121,7 +121,7 @@ export class Refine { const code = maybeCode.ok; const externalities = this.createRefineExternalities({ - workPackage, + packageFetchData, currentWorkItemIndex: idx, imports: allImports, extrinsics: allExtrinsics, @@ -234,7 +234,7 @@ export class Refine { } private createRefineExternalities(args: { - workPackage: WorkPackage; + packageFetchData: WorkPackageFetchData; currentWorkItemIndex: number; imports: PerWorkItem; extrinsics: PerWorkItem; @@ -244,7 +244,7 @@ export class Refine { authorizerTrace: BytesBlob; }): RefineHostCallExternalities { const fetchExternalities = new RefineFetchExternalities(this.chainSpec, { - workPackage: args.workPackage, + packageData: args.packageFetchData, currentWorkItemIndex: args.currentWorkItemIndex, imports: args.imports, extrinsics: args.extrinsics, diff --git a/packages/jam/transition/externalities/fetch-externalities.ts b/packages/jam/transition/externalities/fetch-externalities.ts index e406b7445..0daac9b02 100644 --- a/packages/jam/transition/externalities/fetch-externalities.ts +++ b/packages/jam/transition/externalities/fetch-externalities.ts @@ -1,9 +1,9 @@ -import type { CodeHash, ServiceGas, ServiceId } from "@typeberry/block"; +import { type CodeHash, reencodeAsView, type ServiceGas, type ServiceId } from "@typeberry/block"; import { G_I, MAX_REPORT_DEPENDENCIES, O, Q, T, W_A, W_B, W_C, W_M, W_T, W_X } from "@typeberry/block/gp-constants.js"; import type { WorkItem } from "@typeberry/block/work-item.js"; -import { MAX_NUMBER_OF_WORK_ITEMS } from "@typeberry/block/work-package.js"; +import { MAX_NUMBER_OF_WORK_ITEMS, WorkPackage, type WorkPackageView } from "@typeberry/block/work-package.js"; import { BytesBlob } from "@typeberry/bytes"; -import { codec, Encoder } from "@typeberry/codec"; +import { codec, Decoder, type DescribedBy, Encoder, SequenceView } from "@typeberry/codec"; import type { ChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; import { PendingTransfer } from "@typeberry/jam-host-calls"; @@ -129,8 +129,8 @@ export function getEncodedConstants(chainSpec: ChainSpec) { } /** - * `S(w)` — work-item summary used by fetch kinds 11/12 in both the IsAuthorized - * and Refine contexts. Fixed length of 62 bytes per item. + * `S(w)` — work-item summary used by fetch in both the IsAuthorized + * and Refine contexts. * * https://graypaper.fluffylabs.dev/#/ab2cdbd/31fc0231fc02?v=0.7.2 */ @@ -144,6 +144,8 @@ const WORK_ITEM_SUMMARY_CODEC = codec.object({ extrinsicCount: codec.u16, payloadLength: codec.u32, }); +type WorkItemSummary = DescribedBy; +type WorkItemSummaryView = DescribedBy; export function encodeWorkItemSummary(item: WorkItem): BytesBlob { return Encoder.encodeObject(WORK_ITEM_SUMMARY_CODEC, { @@ -158,15 +160,30 @@ export function encodeWorkItemSummary(item: WorkItem): BytesBlob { }); } -/** Kind 11 — concatenation of `S(w)` for every item in the package. */ -export function encodeAllWorkItemSummaries(items: readonly WorkItem[]): BytesBlob { - return BytesBlob.blobFromParts(items.map((i) => encodeWorkItemSummary(i).raw)); +/** Encoded work package data for fetch, shared between `IsAuthorized` and `Refine` fetchers. */ +export type WorkPackageFetchData = { + /** Lazy view over the encoded work package. */ + packageView: WorkPackageView; + /** SequenceView over the concatenated S(w) summaries. */ + workItemSummaries: SequenceView; +}; + +/** Eagerly build the per-package fetch views. */ +export function buildWorkPackageFetchData(chainSpec: ChainSpec, workPackage: WorkPackage): WorkPackageFetchData { + const packageView = reencodeAsView(WorkPackage.Codec, workPackage, chainSpec); + + const summariesBlob = BytesBlob.blobFromParts(workPackage.items.map((i) => encodeWorkItemSummary(i).raw)); + + const workItemSummaries = new SequenceView( + Decoder.fromBytesBlob(summariesBlob), + WORK_ITEM_SUMMARY_CODEC, + workPackage.items.length, + ); + + return { packageView, workItemSummaries }; } /** Converts u64 value taken from a register into valid index of array of given `length`. */ export function u64ToArrayIndex(v: U64, len: number): number | null { - if (v >= BigInt(len)) { - return null; - } - return Number(v); + return v < BigInt(len) ? Number(v) : null; } diff --git a/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts b/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts index 5cb999bcd..97399937d 100644 --- a/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts +++ b/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts @@ -12,8 +12,13 @@ import { asKnownSize, FixedSizeArray } from "@typeberry/collections"; import { fullChainSpec, tinyChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; import { tryAsU16, tryAsU64 } from "@typeberry/numbers"; +import { buildWorkPackageFetchData } from "./fetch-externalities.js"; import { IsAuthorizedFetchExternalities } from "./is-authorized-fetch-externalities.js"; +function fetchDataFor(pkg: WorkPackage, chainSpec = tinyChainSpec) { + return buildWorkPackageFetchData(chainSpec, pkg); +} + function buildWorkItem(overrides: { service?: number; payloadLen?: number } = {}) { return WorkItem.create({ service: tryAsServiceId(overrides.service ?? 1), @@ -47,41 +52,41 @@ function buildPackage(items: WorkItem[] = [buildWorkItem({})]) { describe("IsAuthorizedFetchExternalities", () => { it("returns different constants for different chain specs", () => { - const tinyExt = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); - const fullExt = new IsAuthorizedFetchExternalities(fullChainSpec, buildPackage()); + const tinyExt = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(buildPackage(), tinyChainSpec)); + const fullExt = new IsAuthorizedFetchExternalities(fullChainSpec, fetchDataFor(buildPackage(), fullChainSpec)); assert.notStrictEqual(tinyExt.constants().length, 0); assert.notDeepStrictEqual(tinyExt.constants(), fullExt.constants()); }); it("returns encoded work package", () => { const pkg = buildPackage(); - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, pkg); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(pkg)); const expected = Encoder.encodeObject(WorkPackage.Codec, pkg, tinyChainSpec); assert.deepStrictEqual(ext.workPackage().raw, expected.raw); }); it("returns auth configuration and auth token from the package", () => { - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(buildPackage())); assert.deepStrictEqual(ext.authConfiguration().raw, new Uint8Array([4, 5, 6, 7])); assert.deepStrictEqual(ext.authToken().raw, new Uint8Array([1, 2, 3])); }); it("returns encoded refine context", () => { const pkg = buildPackage(); - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, pkg); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(pkg)); const expected = Encoder.encodeObject(RefineContext.Codec, pkg.context); assert.deepStrictEqual(ext.refineContext().raw, expected.raw); }); it("returns concatenated work item summaries with 62 bytes per item", () => { const items = [buildWorkItem({ service: 1 }), buildWorkItem({ service: 2, payloadLen: 5 })]; - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage(items)); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(buildPackage(items))); assert.strictEqual(ext.allWorkItems().length, 62 * items.length); }); it("returns a single work item summary (kind 12)", () => { const items = [buildWorkItem({ service: 1 }), buildWorkItem({ service: 2, payloadLen: 10 })]; - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage(items)); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(buildPackage(items))); const one = ext.oneWorkItem(tryAsU64(1)); assert.ok(one !== null); assert.strictEqual(one.length, 62); @@ -90,13 +95,13 @@ describe("IsAuthorizedFetchExternalities", () => { }); it("returns null for one work item when index is out of range", () => { - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(buildPackage())); assert.strictEqual(ext.oneWorkItem(tryAsU64(99)), null); }); it("returns the raw payload of a work item (kind 13)", () => { const items = [buildWorkItem({ service: 1, payloadLen: 2 }), buildWorkItem({ service: 2, payloadLen: 5 })]; - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage(items)); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(buildPackage(items))); const payload = ext.workItemPayload(tryAsU64(1)); assert.ok(payload !== null); assert.strictEqual(payload.length, 5); @@ -104,7 +109,7 @@ describe("IsAuthorizedFetchExternalities", () => { }); it("returns null for payload when index is out of range", () => { - const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, buildPackage()); + const ext = new IsAuthorizedFetchExternalities(tinyChainSpec, fetchDataFor(buildPackage())); assert.strictEqual(ext.workItemPayload(tryAsU64(99)), null); }); }); diff --git a/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts b/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts index b0ee6edde..a07a14457 100644 --- a/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts +++ b/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts @@ -1,23 +1,15 @@ -import { RefineContext } from "@typeberry/block/refine-context.js"; -import { WorkPackage } from "@typeberry/block/work-package.js"; import type { BytesBlob } from "@typeberry/bytes"; -import { Encoder } from "@typeberry/codec"; import type { ChainSpec } from "@typeberry/config"; import { general } from "@typeberry/jam-host-calls"; import type { U64 } from "@typeberry/numbers"; -import { - encodeAllWorkItemSummaries, - encodeWorkItemSummary, - getEncodedConstants, - u64ToArrayIndex, -} from "./fetch-externalities.js"; +import { getEncodedConstants, u64ToArrayIndex, type WorkPackageFetchData } from "./fetch-externalities.js"; export class IsAuthorizedFetchExternalities implements general.IIsAuthorizedFetch { readonly context = general.FetchContext.IsAuthorized; constructor( private readonly chainSpec: ChainSpec, - private readonly pkg: WorkPackage, + private readonly pkg: WorkPackageFetchData, ) {} constants(): BytesBlob { @@ -25,40 +17,36 @@ export class IsAuthorizedFetchExternalities implements general.IIsAuthorizedFetc } workPackage(): BytesBlob { - return Encoder.encodeObject(WorkPackage.Codec, this.pkg, this.chainSpec); + return this.pkg.packageView.encoded(); } authConfiguration(): BytesBlob { - return this.pkg.authConfiguration; + return this.pkg.packageView.authConfiguration.view(); } authToken(): BytesBlob { - return this.pkg.authToken; + return this.pkg.packageView.authToken.view(); } refineContext(): BytesBlob { - return Encoder.encodeObject(RefineContext.Codec, this.pkg.context); + return this.pkg.packageView.context.encoded(); } allWorkItems(): BytesBlob { - return encodeAllWorkItemSummaries(this.pkg.items); + return this.pkg.workItemSummaries.encoded(); } oneWorkItem(workItem: U64): BytesBlob | null { - const items = this.pkg.items; - const idx = u64ToArrayIndex(workItem, items.length); - if (idx === null) { - return null; - } - return encodeWorkItemSummary(items[idx]); + const idx = u64ToArrayIndex(workItem, this.pkg.workItemSummaries.length); + return idx === null ? null : (this.pkg.workItemSummaries.get(idx)?.encoded() ?? null); } workItemPayload(workItem: U64): BytesBlob | null { - const items = this.pkg.items; + const items = this.pkg.packageView.items.view(); const idx = u64ToArrayIndex(workItem, items.length); if (idx === null) { return null; } - return items[idx].payload; + return items.get(idx)?.view().payload.view() ?? null; } } diff --git a/packages/jam/transition/externalities/refine-fetch-externalities.test.ts b/packages/jam/transition/externalities/refine-fetch-externalities.test.ts index f60bdbf8d..1791d6f7d 100644 --- a/packages/jam/transition/externalities/refine-fetch-externalities.test.ts +++ b/packages/jam/transition/externalities/refine-fetch-externalities.test.ts @@ -4,8 +4,8 @@ import { describe, it } from "node:test"; import type { CodeHash } from "@typeberry/block"; import { tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/block"; import { RefineContext } from "@typeberry/block/refine-context.js"; -import type { WorkItemExtrinsic } from "@typeberry/block/work-item.js"; -import { WorkItem } from "@typeberry/block/work-item.js"; +import { ImportSpec, WorkItem, type WorkItemExtrinsic, WorkItemExtrinsicSpec } from "@typeberry/block/work-item.js"; +import { tryAsSegmentIndex } from "@typeberry/block/work-item-segment.js"; import { tryAsWorkItemsCount, WorkPackage } from "@typeberry/block/work-package.js"; import { Bytes, BytesBlob } from "@typeberry/bytes"; import { Encoder } from "@typeberry/codec"; @@ -15,6 +15,7 @@ import { HASH_SIZE } from "@typeberry/hash"; import { tryAsU16, tryAsU32, tryAsU64 } from "@typeberry/numbers"; import { asOpaqueType } from "@typeberry/utils"; import type { ImportedSegment, PerWorkItem } from "../../in-core/refine.js"; +import { buildWorkPackageFetchData } from "./fetch-externalities.js"; import { RefineFetchExternalities } from "./refine-fetch-externalities.js"; const asExtrinsic = (bytes: BytesBlob): WorkItemExtrinsic => asOpaqueType(bytes); @@ -27,14 +28,20 @@ function buildWorkItem(overrides: { extrinsicCount?: number; }) { const codeHash = Bytes.fill(HASH_SIZE, 7).asOpaque(); + const imports = Array.from({ length: overrides.importCount ?? 0 }, (_, i) => + ImportSpec.create({ treeRoot: Bytes.zero(HASH_SIZE), index: tryAsSegmentIndex(i) }), + ); + const extrinsicSpecs = Array.from({ length: overrides.extrinsicCount ?? 0 }, () => + WorkItemExtrinsicSpec.create({ hash: Bytes.zero(HASH_SIZE).asOpaque(), len: tryAsU32(0) }), + ); return WorkItem.create({ service: tryAsServiceId(overrides.service ?? 1), codeHash, payload: BytesBlob.blobFrom(new Uint8Array(overrides.payloadLen ?? 3).fill(0xab)), refineGasLimit: tryAsServiceGas(1_000_000), accumulateGasLimit: tryAsServiceGas(2_000_000), - importSegments: asKnownSize(new Array(overrides.importCount ?? 0)), - extrinsic: new Array(overrides.extrinsicCount ?? 0), + importSegments: asKnownSize(imports), + extrinsic: extrinsicSpecs, exportCount: tryAsU16(overrides.exportCount ?? 0), }); } @@ -70,8 +77,9 @@ function prepareRefineData( const chainSpec = opts.chainSpec ?? tinyChainSpec; const items = opts.items ?? [buildWorkItem({})]; const workPackage = buildWorkPackage(items); + const packageData = buildWorkPackageFetchData(chainSpec, workPackage); return new RefineFetchExternalities(chainSpec, { - workPackage, + packageData, currentWorkItemIndex: opts.currentWorkItemIndex ?? 0, imports: opts.imports ?? asKnownSize(items.map(() => [])), extrinsics: opts.extrinsics ?? asKnownSize(items.map(() => [])), diff --git a/packages/jam/transition/externalities/refine-fetch-externalities.ts b/packages/jam/transition/externalities/refine-fetch-externalities.ts index ecb7e24fc..6feffebe1 100644 --- a/packages/jam/transition/externalities/refine-fetch-externalities.ts +++ b/packages/jam/transition/externalities/refine-fetch-externalities.ts @@ -1,23 +1,16 @@ import type { EntropyHash } from "@typeberry/block"; -import { RefineContext } from "@typeberry/block/refine-context.js"; import type { WorkItemExtrinsic } from "@typeberry/block/work-item.js"; -import { WorkPackage } from "@typeberry/block/work-package.js"; import { Bytes, type BytesBlob } from "@typeberry/bytes"; -import { Encoder } from "@typeberry/codec"; import type { ChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; import { general } from "@typeberry/jam-host-calls"; import type { U64 } from "@typeberry/numbers"; import type { ImportedSegment, PerWorkItem } from "../../in-core/refine.js"; -import { - encodeAllWorkItemSummaries, - encodeWorkItemSummary, - getEncodedConstants, - u64ToArrayIndex, -} from "./fetch-externalities.js"; +import { getEncodedConstants, u64ToArrayIndex, type WorkPackageFetchData } from "./fetch-externalities.js"; export type RefineFetchData = { - workPackage: WorkPackage; + /** Pre-computed per-work-package encodings. */ + packageData: WorkPackageFetchData; /** Index of the work item currently being refined (`i` in GP). */ currentWorkItemIndex: number; /** Imports per work item (`ī`). */ @@ -83,45 +76,37 @@ export class RefineFetchExternalities implements general.IRefineFetch { } workPackage(): BytesBlob { - return Encoder.encodeObject(WorkPackage.Codec, this.data.workPackage, this.chainSpec); + return this.data.packageData.packageView.encoded(); } authConfiguration(): BytesBlob { - return this.data.workPackage.authConfiguration; + return this.data.packageData.packageView.authConfiguration.view(); } authToken(): BytesBlob { - return this.data.workPackage.authToken; + return this.data.packageData.packageView.authToken.view(); } refineContext(): BytesBlob { - return Encoder.encodeObject(RefineContext.Codec, this.data.workPackage.context); + return this.data.packageData.packageView.context.encoded(); } - /** - * Kind 11: concatenation of `S(w)` for every work item in the package. - * - * https://graypaper.fluffylabs.dev/#/ab2cdbd/31f40231f402?v=0.7.2 - */ allWorkItems(): BytesBlob { - return encodeAllWorkItemSummaries(this.data.workPackage.items); + return this.data.packageData.workItemSummaries.encoded(); } oneWorkItem(workItem: U64): BytesBlob | null { - const items = this.data.workPackage.items; - const idx = u64ToArrayIndex(workItem, items.length); - if (idx === null) { - return null; - } - return encodeWorkItemSummary(items[idx]); + const summaries = this.data.packageData.workItemSummaries; + const idx = u64ToArrayIndex(workItem, summaries.length); + return idx === null ? null : (summaries.get(idx)?.encoded() ?? null); } workItemPayload(workItem: U64): BytesBlob | null { - const items = this.data.workPackage.items; + const items = this.data.packageData.packageView.items.view(); const idx = u64ToArrayIndex(workItem, items.length); if (idx === null) { return null; } - return items[idx].payload; + return items.get(idx)?.view().payload.view() ?? null; } } From 653820aed0a10233490e7e0948fef6876d3ba045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Tue, 21 Apr 2026 09:32:58 +0200 Subject: [PATCH 4/6] Move externalities implementation to in-core --- packages/jam/in-core/externalities/index.ts | 3 + .../is-authorized-fetch.test.ts} | 6 +- .../externalities/is-authorized-fetch.ts} | 6 +- .../externalities/refine-fetch.test.ts} | 8 +- .../jam/in-core/externalities/refine-fetch.ts | 125 ++++++++++++++++++ packages/jam/in-core/in-core.ts | 5 +- packages/jam/in-core/is-authorized.ts | 2 +- packages/jam/in-core/refine.ts | 17 +-- .../jam/transition/externalities/index.ts | 2 - .../refine-fetch-externalities.ts | 112 ---------------- 10 files changed, 150 insertions(+), 136 deletions(-) create mode 100644 packages/jam/in-core/externalities/index.ts rename packages/jam/{transition/externalities/is-authorized-fetch-externalities.test.ts => in-core/externalities/is-authorized-fetch.test.ts} (96%) rename packages/jam/{transition/externalities/is-authorized-fetch-externalities.ts => in-core/externalities/is-authorized-fetch.ts} (90%) rename packages/jam/{transition/externalities/refine-fetch-externalities.test.ts => in-core/externalities/refine-fetch.test.ts} (97%) create mode 100644 packages/jam/in-core/externalities/refine-fetch.ts diff --git a/packages/jam/in-core/externalities/index.ts b/packages/jam/in-core/externalities/index.ts new file mode 100644 index 000000000..92c87dcdc --- /dev/null +++ b/packages/jam/in-core/externalities/index.ts @@ -0,0 +1,3 @@ +export * from "./is-authorized-fetch.js"; +export * from "./refine.js"; +export * from "./refine-fetch.js"; diff --git a/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts b/packages/jam/in-core/externalities/is-authorized-fetch.test.ts similarity index 96% rename from packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts rename to packages/jam/in-core/externalities/is-authorized-fetch.test.ts index 97399937d..e5b9c4e83 100644 --- a/packages/jam/transition/externalities/is-authorized-fetch-externalities.test.ts +++ b/packages/jam/in-core/externalities/is-authorized-fetch.test.ts @@ -12,8 +12,8 @@ import { asKnownSize, FixedSizeArray } from "@typeberry/collections"; import { fullChainSpec, tinyChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; import { tryAsU16, tryAsU64 } from "@typeberry/numbers"; -import { buildWorkPackageFetchData } from "./fetch-externalities.js"; -import { IsAuthorizedFetchExternalities } from "./is-authorized-fetch-externalities.js"; +import { buildWorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; +import { IsAuthorizedFetchExternalities } from "./is-authorized-fetch.js"; function fetchDataFor(pkg: WorkPackage, chainSpec = tinyChainSpec) { return buildWorkPackageFetchData(chainSpec, pkg); @@ -105,7 +105,7 @@ describe("IsAuthorizedFetchExternalities", () => { const payload = ext.workItemPayload(tryAsU64(1)); assert.ok(payload !== null); assert.strictEqual(payload.length, 5); - assert.ok(payload.raw.every((x) => x === 0xab)); + assert.ok(payload.raw.every((x: number) => x === 0xab)); }); it("returns null for payload when index is out of range", () => { diff --git a/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts b/packages/jam/in-core/externalities/is-authorized-fetch.ts similarity index 90% rename from packages/jam/transition/externalities/is-authorized-fetch-externalities.ts rename to packages/jam/in-core/externalities/is-authorized-fetch.ts index a07a14457..3413c1173 100644 --- a/packages/jam/transition/externalities/is-authorized-fetch-externalities.ts +++ b/packages/jam/in-core/externalities/is-authorized-fetch.ts @@ -2,7 +2,11 @@ import type { 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, u64ToArrayIndex, type WorkPackageFetchData } from "./fetch-externalities.js"; +import { + getEncodedConstants, + u64ToArrayIndex, + type WorkPackageFetchData, +} from "@typeberry/transition/externalities/fetch-externalities.js"; export class IsAuthorizedFetchExternalities implements general.IIsAuthorizedFetch { readonly context = general.FetchContext.IsAuthorized; diff --git a/packages/jam/transition/externalities/refine-fetch-externalities.test.ts b/packages/jam/in-core/externalities/refine-fetch.test.ts similarity index 97% rename from packages/jam/transition/externalities/refine-fetch-externalities.test.ts rename to packages/jam/in-core/externalities/refine-fetch.test.ts index 1791d6f7d..047463ed5 100644 --- a/packages/jam/transition/externalities/refine-fetch-externalities.test.ts +++ b/packages/jam/in-core/externalities/refine-fetch.test.ts @@ -13,10 +13,10 @@ import { asKnownSize, FixedSizeArray } from "@typeberry/collections"; import { type ChainSpec, fullChainSpec, tinyChainSpec } from "@typeberry/config"; import { HASH_SIZE } from "@typeberry/hash"; import { tryAsU16, tryAsU32, tryAsU64 } from "@typeberry/numbers"; +import { buildWorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; import { asOpaqueType } from "@typeberry/utils"; -import type { ImportedSegment, PerWorkItem } from "../../in-core/refine.js"; -import { buildWorkPackageFetchData } from "./fetch-externalities.js"; -import { RefineFetchExternalities } from "./refine-fetch-externalities.js"; +import type { ImportedSegment, PerWorkItem } from "./refine-fetch.js"; +import { RefineFetchExternalities } from "./refine-fetch.js"; const asExtrinsic = (bytes: BytesBlob): WorkItemExtrinsic => asOpaqueType(bytes); @@ -246,7 +246,7 @@ describe("RefineFetchExternalities", () => { const payload = ext.workItemPayload(tryAsU64(1)); assert.ok(payload !== null); assert.strictEqual(payload.length, 5); - assert.ok(payload.raw.every((x) => x === 0xab)); + assert.ok(payload.raw.every((x: number) => x === 0xab)); }); it("should return null for payload when index is out of range", () => { diff --git a/packages/jam/in-core/externalities/refine-fetch.ts b/packages/jam/in-core/externalities/refine-fetch.ts new file mode 100644 index 000000000..a14fafdb2 --- /dev/null +++ b/packages/jam/in-core/externalities/refine-fetch.ts @@ -0,0 +1,125 @@ +import type { EntropyHash, Segment, SegmentIndex } from "@typeberry/block"; +import type { WorkItemExtrinsic } from "@typeberry/block/work-item.js"; +import { Bytes, type BytesBlob } from "@typeberry/bytes"; +import type { KnownSizeArray } from "@typeberry/collections"; +import type { ChainSpec } from "@typeberry/config"; +import { HASH_SIZE } from "@typeberry/hash"; +import { general } from "@typeberry/jam-host-calls"; +import type { U64 } from "@typeberry/numbers"; +import { + getEncodedConstants, + u64ToArrayIndex, + type WorkPackageFetchData, +} from "@typeberry/transition/externalities/fetch-externalities.js"; + +/** A single decoded import segment passed into refine. */ +export type ImportedSegment = { + index: SegmentIndex; + data: Segment; +}; + +/** An array whose length matches the work-package's work-item count. */ +export type PerWorkItem = KnownSizeArray; + +export type RefineFetchData = { + /** Pre-computed per-work-package encodings. */ + packageData: WorkPackageFetchData; + /** Index of the work item currently being refined (`i` in GP). */ + currentWorkItemIndex: number; + /** Imports per work item (`ī`). */ + imports: PerWorkItem; + /** Extrinsics per work item (`x̄`). */ + extrinsics: PerWorkItem; + /** Authorizer trace produced by Is-Authorized (`r`). */ + authorizerTrace: BytesBlob; +}; + +export class RefineFetchExternalities implements general.IRefineFetch { + readonly context = general.FetchContext.Refine; + + constructor( + private readonly chainSpec: ChainSpec, + private readonly data: RefineFetchData, + ) {} + + constants(): BytesBlob { + return getEncodedConstants(this.chainSpec); + } + + /** + * Refine entropy is `H₀` (zero hash) per GP §B.3. + * + * https://graypaper.fluffylabs.dev/#/ab2cdbd/2fe0012fe201?v=0.7.2 + */ + entropy(): EntropyHash { + return Bytes.zero(HASH_SIZE).asOpaque(); + } + + authorizerTrace(): BytesBlob { + return this.data.authorizerTrace; + } + + workItemExtrinsic(workItem: U64 | null, index: U64): BytesBlob | null { + const itemIdx = + workItem === null ? this.data.currentWorkItemIndex : u64ToArrayIndex(workItem, this.data.extrinsics.length); + if (itemIdx === null) { + return null; + } + const perItem = this.data.extrinsics[itemIdx]; + const xIdx = u64ToArrayIndex(index, perItem.length); + if (xIdx === null) { + return null; + } + return perItem[xIdx]; + } + + workItemImport(workItem: U64 | null, index: U64): BytesBlob | null { + const itemIdx = + workItem === null ? this.data.currentWorkItemIndex : u64ToArrayIndex(workItem, this.data.imports.length); + if (itemIdx === null) { + return null; + } + const perItem = this.data.imports[itemIdx]; + const segIdx = u64ToArrayIndex(index, perItem.length); + if (segIdx === null) { + return null; + } + // `Segment` extends `BytesBlob`, so it can be returned directly. + return perItem[segIdx].data; + } + + workPackage(): BytesBlob { + return this.data.packageData.packageView.encoded(); + } + + authConfiguration(): BytesBlob { + return this.data.packageData.packageView.authConfiguration.view(); + } + + authToken(): BytesBlob { + return this.data.packageData.packageView.authToken.view(); + } + + refineContext(): BytesBlob { + return this.data.packageData.packageView.context.encoded(); + } + + allWorkItems(): BytesBlob { + return this.data.packageData.workItemSummaries.encoded(); + } + + oneWorkItem(workItem: U64): BytesBlob | null { + const summaries = this.data.packageData.workItemSummaries; + const idx = u64ToArrayIndex(workItem, summaries.length); + return idx === null ? null : (summaries.get(idx)?.encoded() ?? null); + } + + workItemPayload(workItem: U64): BytesBlob | null { + const items = this.data.packageData.packageView.items.view(); + const idx = u64ToArrayIndex(workItem, items.length); + if (idx === null) { + return null; + } + return items.get(idx)?.view().payload.view() ?? null; + } +} diff --git a/packages/jam/in-core/in-core.ts b/packages/jam/in-core/in-core.ts index e965d844b..9548698f6 100644 --- a/packages/jam/in-core/in-core.ts +++ b/packages/jam/in-core/in-core.ts @@ -13,10 +13,11 @@ import { Logger } from "@typeberry/logger"; import { tryAsU8, tryAsU16, tryAsU32 } from "@typeberry/numbers"; import { buildWorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; import { assertEmpty, Result } from "@typeberry/utils"; +import type { ImportedSegment, PerWorkItem } from "./externalities/index.js"; import { AuthorizationError, type AuthorizationOk, IsAuthorized } from "./is-authorized.js"; -import { type ImportedSegment, type PerWorkItem, Refine, type RefineItemResult } from "./refine.js"; +export type { ImportedSegment, PerWorkItem }; -export type { ImportedSegment, PerWorkItem, RefineItemResult } from "./refine.js"; +import { Refine, type RefineItemResult } from "./refine.js"; export type RefineResult = { report: WorkReport; diff --git a/packages/jam/in-core/is-authorized.ts b/packages/jam/in-core/is-authorized.ts index 016de140a..8b30793ed 100644 --- a/packages/jam/in-core/is-authorized.ts +++ b/packages/jam/in-core/is-authorized.ts @@ -9,8 +9,8 @@ import { PvmExecutor, ReturnStatus } from "@typeberry/executor"; import type { Blake2b } from "@typeberry/hash"; import type { State } from "@typeberry/state"; import type { WorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; -import { IsAuthorizedFetchExternalities } from "@typeberry/transition/externalities/is-authorized-fetch-externalities.js"; import { Result } from "@typeberry/utils"; +import { IsAuthorizedFetchExternalities } from "./externalities/index.js"; export enum AuthorizationError { /** BAD: authorizer code not found (service or preimage missing). */ diff --git a/packages/jam/in-core/refine.ts b/packages/jam/in-core/refine.ts index fb5910c09..87bbd0551 100644 --- a/packages/jam/in-core/refine.ts +++ b/packages/jam/in-core/refine.ts @@ -1,7 +1,6 @@ import { type CoreIndex, type Segment, - type SegmentIndex, type ServiceGas, type ServiceId, tryAsCoreIndex, @@ -13,29 +12,25 @@ 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 type { WorkPackageFetchData } from "@typeberry/transition/externalities/fetch-externalities.js"; -import { RefineFetchExternalities } from "@typeberry/transition/externalities/refine-fetch-externalities.js"; import { assertNever, Result } from "@typeberry/utils"; -import { RefineExternalitiesImpl } from "./externalities/refine.js"; +import { + type ImportedSegment, + type PerWorkItem, + RefineExternalitiesImpl, + RefineFetchExternalities, +} from "./externalities/index.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, diff --git a/packages/jam/transition/externalities/index.ts b/packages/jam/transition/externalities/index.ts index c2b85dc83..c32b5d110 100644 --- a/packages/jam/transition/externalities/index.ts +++ b/packages/jam/transition/externalities/index.ts @@ -1,5 +1,3 @@ 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/refine-fetch-externalities.ts b/packages/jam/transition/externalities/refine-fetch-externalities.ts index 6feffebe1..e69de29bb 100644 --- a/packages/jam/transition/externalities/refine-fetch-externalities.ts +++ b/packages/jam/transition/externalities/refine-fetch-externalities.ts @@ -1,112 +0,0 @@ -import type { EntropyHash } from "@typeberry/block"; -import type { WorkItemExtrinsic } from "@typeberry/block/work-item.js"; -import { Bytes, type BytesBlob } from "@typeberry/bytes"; -import type { ChainSpec } from "@typeberry/config"; -import { HASH_SIZE } from "@typeberry/hash"; -import { general } from "@typeberry/jam-host-calls"; -import type { U64 } from "@typeberry/numbers"; -import type { ImportedSegment, PerWorkItem } from "../../in-core/refine.js"; -import { getEncodedConstants, u64ToArrayIndex, type WorkPackageFetchData } from "./fetch-externalities.js"; - -export type RefineFetchData = { - /** Pre-computed per-work-package encodings. */ - packageData: WorkPackageFetchData; - /** Index of the work item currently being refined (`i` in GP). */ - currentWorkItemIndex: number; - /** Imports per work item (`ī`). */ - imports: PerWorkItem; - /** Extrinsics per work item (`x̄`). */ - extrinsics: PerWorkItem; - /** Authorizer trace produced by Is-Authorized (`r`). */ - authorizerTrace: BytesBlob; -}; - -export class RefineFetchExternalities implements general.IRefineFetch { - readonly context = general.FetchContext.Refine; - - constructor( - private readonly chainSpec: ChainSpec, - private readonly data: RefineFetchData, - ) {} - - constants(): BytesBlob { - return getEncodedConstants(this.chainSpec); - } - - /** - * Refine entropy is `H₀` (zero hash) per GP §B.3. - * - * https://graypaper.fluffylabs.dev/#/ab2cdbd/2fe0012fe201?v=0.7.2 - */ - entropy(): EntropyHash { - return Bytes.zero(HASH_SIZE).asOpaque(); - } - - authorizerTrace(): BytesBlob { - return this.data.authorizerTrace; - } - - workItemExtrinsic(workItem: U64 | null, index: U64): BytesBlob | null { - const itemIdx = - workItem === null ? this.data.currentWorkItemIndex : u64ToArrayIndex(workItem, this.data.extrinsics.length); - if (itemIdx === null) { - return null; - } - const perItem = this.data.extrinsics[itemIdx]; - const xIdx = u64ToArrayIndex(index, perItem.length); - if (xIdx === null) { - return null; - } - return perItem[xIdx]; - } - - workItemImport(workItem: U64 | null, index: U64): BytesBlob | null { - const itemIdx = - workItem === null ? this.data.currentWorkItemIndex : u64ToArrayIndex(workItem, this.data.imports.length); - if (itemIdx === null) { - return null; - } - const perItem = this.data.imports[itemIdx]; - const segIdx = u64ToArrayIndex(index, perItem.length); - if (segIdx === null) { - return null; - } - // `Segment` extends `BytesBlob`, so it can be returned directly. - return perItem[segIdx].data; - } - - workPackage(): BytesBlob { - return this.data.packageData.packageView.encoded(); - } - - authConfiguration(): BytesBlob { - return this.data.packageData.packageView.authConfiguration.view(); - } - - authToken(): BytesBlob { - return this.data.packageData.packageView.authToken.view(); - } - - refineContext(): BytesBlob { - return this.data.packageData.packageView.context.encoded(); - } - - allWorkItems(): BytesBlob { - return this.data.packageData.workItemSummaries.encoded(); - } - - oneWorkItem(workItem: U64): BytesBlob | null { - const summaries = this.data.packageData.workItemSummaries; - const idx = u64ToArrayIndex(workItem, summaries.length); - return idx === null ? null : (summaries.get(idx)?.encoded() ?? null); - } - - workItemPayload(workItem: U64): BytesBlob | null { - const items = this.data.packageData.packageView.items.view(); - const idx = u64ToArrayIndex(workItem, items.length); - if (idx === null) { - return null; - } - return items.get(idx)?.view().payload.view() ?? null; - } -} From 525515a7f56e588ea201403f9d4e01cb162b9a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Tue, 21 Apr 2026 11:48:47 +0200 Subject: [PATCH 5/6] Address review --- .../jam/in-core/externalities/refine-fetch.test.ts | 7 ++++--- packages/jam/in-core/in-core.ts | 2 +- packages/jam/in-core/is-authorized.test.ts | 12 ++++++------ packages/jam/in-core/is-authorized.ts | 4 +--- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/jam/in-core/externalities/refine-fetch.test.ts b/packages/jam/in-core/externalities/refine-fetch.test.ts index 047463ed5..1a0ca8dcc 100644 --- a/packages/jam/in-core/externalities/refine-fetch.test.ts +++ b/packages/jam/in-core/externalities/refine-fetch.test.ts @@ -5,7 +5,7 @@ import type { CodeHash } from "@typeberry/block"; import { tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/block"; import { RefineContext } from "@typeberry/block/refine-context.js"; import { ImportSpec, WorkItem, type WorkItemExtrinsic, WorkItemExtrinsicSpec } from "@typeberry/block/work-item.js"; -import { tryAsSegmentIndex } from "@typeberry/block/work-item-segment.js"; +import { SEGMENT_BYTES, tryAsSegmentIndex } from "@typeberry/block/work-item-segment.js"; import { tryAsWorkItemsCount, WorkPackage } from "@typeberry/block/work-package.js"; import { Bytes, BytesBlob } from "@typeberry/bytes"; import { Encoder } from "@typeberry/codec"; @@ -164,10 +164,10 @@ describe("RefineFetchExternalities", () => { it("should return an import segment by work item index and segment index", () => { const items = [buildWorkItem({}), buildWorkItem({ service: 2 })]; - const segBytes = new Uint8Array(16).fill(0x55); + const segBytes = new Uint8Array(SEGMENT_BYTES).fill(0x55); const imports: PerWorkItem = asKnownSize([ [], - [{ index: 0 as never, data: Bytes.fromBlob(segBytes, 16) as never }], + [{ index: tryAsSegmentIndex(0), data: Bytes.fromBlob(segBytes, SEGMENT_BYTES) }], ]); const ext = prepareRefineData({ items, imports }); @@ -180,6 +180,7 @@ describe("RefineFetchExternalities", () => { const ext = prepareRefineData(); assert.strictEqual(ext.workItemImport(tryAsU64(10), tryAsU64(0)), null); assert.strictEqual(ext.workItemImport(null, tryAsU64(10)), null); + assert.strictEqual(ext.workItemImport(tryAsU64(0), tryAsU64(10)), null); }); it("should return encoded work package", () => { diff --git a/packages/jam/in-core/in-core.ts b/packages/jam/in-core/in-core.ts index 9548698f6..0c1692b83 100644 --- a/packages/jam/in-core/in-core.ts +++ b/packages/jam/in-core/in-core.ts @@ -111,7 +111,7 @@ export class InCore { const packageFetchData = buildWorkPackageFetchData(this.chainSpec, workPackageAndHash.data); // Check authorization - const authResult = await this.isAuthorized.invoke(state, core, workPackageAndHash.data, packageFetchData); + const authResult = await this.isAuthorized.invoke(state, core, packageFetchData); if (authResult.isError) { return Result.error( RefineError.AuthorizationError, diff --git a/packages/jam/in-core/is-authorized.test.ts b/packages/jam/in-core/is-authorized.test.ts index 99b8a7af3..dedf9cc3f 100644 --- a/packages/jam/in-core/is-authorized.test.ts +++ b/packages/jam/in-core/is-authorized.test.ts @@ -106,8 +106,8 @@ describe("IsAuthorized", () => { const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); const token = BytesBlob.blobFromString("hello"); - const { pkg, fetchData } = buildPackageAndFetchData(authCodeHash, token, token); - const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), pkg, fetchData); + const { fetchData } = buildPackageAndFetchData(authCodeHash, token, token); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), fetchData); assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); @@ -132,7 +132,7 @@ describe("IsAuthorized", () => { const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); const empty = buildPackageAndFetchData(authCodeHash, BytesBlob.empty(), BytesBlob.empty()); - const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), empty.pkg, empty.fetchData); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), empty.fetchData); assert.strictEqual(result.isOk, true, `Expected OK but got error: ${result.isError ? result.details() : ""}`); const outputStr = Buffer.from(result.ok.authorizationOutput.raw).toString("utf8"); @@ -149,7 +149,7 @@ describe("IsAuthorized", () => { BytesBlob.blobFromString("wrong"), BytesBlob.blobFromString("right"), ); - const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), mismatch.pkg, mismatch.fetchData); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), mismatch.fetchData); assert.strictEqual(result.isError, true); assert.strictEqual(result.error, AuthorizationError.PvmFailed); @@ -164,7 +164,7 @@ describe("IsAuthorized", () => { const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); const missing = buildPackageAndFetchData(authCodeHash, BytesBlob.empty(), BytesBlob.empty()); - const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), missing.pkg, missing.fetchData); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), missing.fetchData); assert.strictEqual(result.isError, true); assert.strictEqual(result.error, AuthorizationError.CodeNotFound); @@ -197,7 +197,7 @@ describe("IsAuthorized", () => { const isAuthorized = new IsAuthorized(spec, PvmBackend.BuiltIn, blake2b); const emptyPreimage = buildPackageAndFetchData(authCodeHash, BytesBlob.empty(), BytesBlob.empty()); - const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), emptyPreimage.pkg, emptyPreimage.fetchData); + const result = await isAuthorized.invoke(state, tryAsCoreIndex(0), emptyPreimage.fetchData); 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 index 8b30793ed..e689ad913 100644 --- a/packages/jam/in-core/is-authorized.ts +++ b/packages/jam/in-core/is-authorized.ts @@ -1,7 +1,6 @@ import { type CoreIndex, type ServiceGas, 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 type { WorkPackage } from "@typeberry/block/work-package.js"; import { BytesBlob } from "@typeberry/bytes"; import { codec, Encoder } from "@typeberry/codec"; import type { ChainSpec, PvmBackend } from "@typeberry/config"; @@ -46,10 +45,9 @@ export class IsAuthorized { async invoke( state: State, coreIndex: CoreIndex, - workPackage: WorkPackage, packageFetchData: WorkPackageFetchData, ): Promise> { - const { authCodeHost, authCodeHash, authConfiguration } = workPackage; + const { authCodeHost, authCodeHash, authConfiguration } = packageFetchData.packageView.materialize(); // Look up the authorizer code from the auth code host service const service = state.getService(authCodeHost); From b7f7c213caefb833bd001ca481b11fbe57d7acf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Tue, 21 Apr 2026 12:32:04 +0200 Subject: [PATCH 6/6] Don't materialize the whole package. --- packages/jam/in-core/is-authorized.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/jam/in-core/is-authorized.ts b/packages/jam/in-core/is-authorized.ts index e689ad913..8b750d3dd 100644 --- a/packages/jam/in-core/is-authorized.ts +++ b/packages/jam/in-core/is-authorized.ts @@ -47,7 +47,10 @@ export class IsAuthorized { coreIndex: CoreIndex, packageFetchData: WorkPackageFetchData, ): Promise> { - const { authCodeHost, authCodeHash, authConfiguration } = packageFetchData.packageView.materialize(); + const packageView = packageFetchData.packageView; + const authCodeHost = packageView.authCodeHost.materialize(); + const authCodeHash = packageView.authCodeHash.materialize(); + const authConfiguration = packageView.authConfiguration.materialize(); // Look up the authorizer code from the auth code host service const service = state.getService(authCodeHost);