Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 25 additions & 7 deletions packages/jam/block/work-item-segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,35 @@ import type { Bytes } from "@typeberry/bytes";
import { tryAsU16, type U16 } from "@typeberry/numbers";
import { asOpaqueType, type Opaque } from "@typeberry/utils";

/** `W_E`: The basic size of erasure-coded pieces in octets. See equation H.6. */
/**
* `W_E`: The basic size of erasure-coded pieces in octets. See equation H.6.
* https://graypaper.fluffylabs.dev/#/ab2cdbd/449600449700?v=0.7.2
*/
export const W_E = 684;

/** `W_S`: The size of an exported segment in erasure-coded pieces in octets. */
export const W_S = 6;
/**
* `W_P`: The size of an exported segment in erasure-coded pieces in octets.
* https://graypaper.fluffylabs.dev/#/ab2cdbd/44b10044b200?v=0.7.2
*/
export const W_P = 6;

/** `W_M`: The maximum number of entries in a work-package manifest. */
export const MAX_NUMBER_OF_SEGMENTS = 2048; // 2**11
/**
* `W_M`: The maximum number of imports in a work-package manifest.
* https://graypaper.fluffylabs.dev/#/ab2cdbd/44ad0044ae00?v=0.7.2
*/
export const MAX_NUMBER_OF_IMPORTS_WP = 3072;

/** `W_E * W_S`: Exported segment size in bytes. */
export const SEGMENT_BYTES = W_E * W_S;
/**
* `W_X`: The maximum number of exports in a work-package manifest.
* https://graypaper.fluffylabs.dev/#/ab2cdbd/44be0044bf00?v=0.7.2
*/
export const MAX_NUMBER_OF_EXPORTS_WP = 3072;

/**
* `W_G = W_E * W_P`: Exported segment size in bytes.
* https://graypaper.fluffylabs.dev/#/ab2cdbd/449a00449b00?v=0.7.2
Comment thread
DrEverr marked this conversation as resolved.
*/
export const SEGMENT_BYTES = W_E * W_P;
export type SEGMENT_BYTES = typeof SEGMENT_BYTES;

/** Exported segment data. */
Expand Down
12 changes: 8 additions & 4 deletions packages/jam/block/work-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type Opaque, WithDebug } from "@typeberry/utils";
import { codecKnownSizeArray } from "./codec-utils.js";
import type { ServiceGas, ServiceId } from "./common.js";
import type { CodeHash } from "./hash.js";
import { MAX_NUMBER_OF_SEGMENTS, type SegmentIndex } from "./work-item-segment.js";
import { MAX_NUMBER_OF_IMPORTS_WP, type SegmentIndex } from "./work-item-segment.js";

type WorkItemExtrinsicHash = Opaque<OpaqueHash, "ExtrinsicHash">;

Expand Down Expand Up @@ -126,13 +126,17 @@ export class WorkItem extends WithDebug {
codeHash: codec.bytes(HASH_SIZE).asOpaque<CodeHash>(),
refineGasLimit: codec.u64.asOpaque<ServiceGas>(),
accumulateGasLimit: codec.u64.asOpaque<ServiceGas>(),
// TODO: [MaSo] It should be validated to not exceed W_X
// https://graypaper.fluffylabs.dev/#/ab2cdbd/1a0b011a1c01?v=0.7.2
exportCount: codec.u16,
payload: codec.blob,
importSegments: codecKnownSizeArray(ImportSpec.Codec, {
minLength: 0,
maxLength: MAX_NUMBER_OF_SEGMENTS,
typicalLength: MAX_NUMBER_OF_SEGMENTS,
maxLength: MAX_NUMBER_OF_IMPORTS_WP,
typicalLength: MAX_NUMBER_OF_IMPORTS_WP,
}),
// TODO: [MaSo] It should be validated to not exceed T = 128
// https://graypaper.fluffylabs.dev/#/ab2cdbd/1a0b011a1c01?v=0.7.2
extrinsic: codec.sequenceVarLen(WorkItemExtrinsicSpec.Codec),
});

Expand Down Expand Up @@ -175,7 +179,7 @@ export class WorkItem extends WithDebug {
/** `a`: accumulate execution gas limit */
public readonly accumulateGasLimit: ServiceGas,
/** `i`: sequence of imported data segments, which identify a prior exported segment. */
public readonly importSegments: KnownSizeArray<ImportSpec, `Less than ${typeof MAX_NUMBER_OF_SEGMENTS}`>,
public readonly importSegments: KnownSizeArray<ImportSpec, `Less than ${typeof MAX_NUMBER_OF_IMPORTS_WP}`>,
/** `x`: sequence of blob hashes and lengths to be introduced in this block */
public readonly extrinsic: WorkItemExtrinsicSpec[],
/** `e`: number of data segments exported by this work item. */
Expand Down
91 changes: 90 additions & 1 deletion packages/jam/in-core/externalities/refine.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import { type PreimageHash, type ServiceId, tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/block";
import {
MAX_NUMBER_OF_EXPORTS_WP,
type PreimageHash,
SEGMENT_BYTES,
type Segment,
type ServiceId,
tryAsServiceGas,
tryAsServiceId,
tryAsTimeSlot,
} from "@typeberry/block";
import { Bytes, BytesBlob } from "@typeberry/bytes";
import { HashDictionary } from "@typeberry/collections";
import { tinyChainSpec } from "@typeberry/config";
import { HASH_SIZE } from "@typeberry/hash";
import { SegmentExportError } from "@typeberry/jam-host-calls";
import { tryAsU32, tryAsU64 } from "@typeberry/numbers";
import { InMemoryService, InMemoryState, PreimageItem, ServiceAccountInfo, type State } from "@typeberry/state";
import { RefineExternalitiesImpl, type RefineExternalitiesParams } from "./refine.js";

function createSegment(byte = 0xab): Segment {
return Bytes.fill(SEGMENT_BYTES, byte);
}

function createSmallSegment(bytes: number[]): Segment {
const data = new Uint8Array(SEGMENT_BYTES);
data.set(bytes);
return Bytes.fromBlob(data, SEGMENT_BYTES);
}

/**
* Create a mock State that has specified services with preimages.
*/
Expand Down Expand Up @@ -60,6 +80,7 @@ function createExt(overrides: Partial<RefineExternalitiesParams> = {}) {
return RefineExternalitiesImpl.create({
currentServiceId: tryAsServiceId(42),
lookupState: overrides.lookupState ?? defaultState,
exportOffset: overrides.exportOffset ?? 0,
...overrides,
});
}
Expand Down Expand Up @@ -123,4 +144,72 @@ describe("RefineExternalitiesImpl", () => {
assert.strictEqual(r2?.raw[0], 0x02);
});
});

describe("exportSegment", () => {
it("should export a segment and return its index", () => {
const ext = createExt();
const segment = createSegment(0x01);
const result = ext.exportSegment(segment);

assert.strictEqual(result.isOk, true);
assert.strictEqual(result.ok, 0); // first export at offset 0
assert.strictEqual(ext.getExportedSegments().length, 1);
});

it("should return sequential indices for multiple exports", () => {
const ext = createExt();

const r1 = ext.exportSegment(createSegment(0x01));
const r2 = ext.exportSegment(createSegment(0x02));
const r3 = ext.exportSegment(createSegment(0x03));

assert.strictEqual(r1.isOk, true);
assert.strictEqual(r1.ok, 0);
assert.strictEqual(r2.isOk, true);
assert.strictEqual(r2.ok, 1);
assert.strictEqual(r3.isOk, true);
assert.strictEqual(r3.ok, 2);
assert.strictEqual(ext.getExportedSegments().length, 3);
});

it("should apply exportOffset to segment indices", () => {
const ext = createExt({ exportOffset: 100 });
const result = ext.exportSegment(createSegment());

assert.strictEqual(result.isOk, true);
assert.strictEqual(result.ok, 100);
});

it("should return SegmentExportError when MAX_NUMBER_OF_EXPORTS_WP exceeded", () => {
const ext = createExt({ exportOffset: MAX_NUMBER_OF_EXPORTS_WP });
const result = ext.exportSegment(createSegment());

assert.strictEqual(result.isError, true);
assert.strictEqual(result.error, SegmentExportError);
});

it("should return SegmentExportError at exactly MAX_NUMBER_OF_EXPORTS_WP - 1 + 1", () => {
const ext = createExt({ exportOffset: MAX_NUMBER_OF_EXPORTS_WP - 1 });

// This one should succeed (index = MAX_NUMBER_OF_EXPORTS_WP - 1)
const r1 = ext.exportSegment(createSegment(0x01));
assert.strictEqual(r1.isOk, true);
assert.strictEqual(r1.ok, MAX_NUMBER_OF_EXPORTS_WP - 1);

// This one should fail
const r2 = ext.exportSegment(createSegment(0x02));
assert.strictEqual(r2.isError, true);
assert.strictEqual(r2.error, SegmentExportError);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("should store exact segment data", () => {
const ext = createExt();
const segment = createSmallSegment([1, 2, 3, 4, 5]);
ext.exportSegment(segment);

const exported = ext.getExportedSegments();
assert.strictEqual(exported.length, 1);
assert.deepStrictEqual(exported[0].raw.subarray(0, 5), new Uint8Array([1, 2, 3, 4, 5]));
});
});
});
56 changes: 42 additions & 14 deletions packages/jam/in-core/externalities/refine.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import type { Segment, SegmentIndex, ServiceId } from "@typeberry/block";
import {
MAX_NUMBER_OF_EXPORTS_WP,
type Segment,
type SegmentIndex,
type ServiceId,
tryAsSegmentIndex,
} from "@typeberry/block";
import type { BytesBlob } from "@typeberry/bytes";
import type { Blake2bHash } from "@typeberry/hash";
import type {
MachineId,
MachineResult,
MemoryOperation,
NoMachineError,
PagesError,
PeekPokeError,
ProgramCounter,
RefineExternalities,
import {
type MachineId,
type MachineResult,
type MemoryOperation,
type NoMachineError,
type PagesError,
type PeekPokeError,
type ProgramCounter,
type RefineExternalities,
SegmentExportError,
ZeroVoidError,
type ZeroVoidError,
} from "@typeberry/jam-host-calls";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import type { U64 } from "@typeberry/numbers";
import type { HostCallMemory, HostCallRegisters } from "@typeberry/pvm-host-calls";
import type { BigGas } from "@typeberry/pvm-interface";
import type { ProgramDecoderError } from "@typeberry/pvm-interpreter";
import type { State } from "@typeberry/state";
import type { OK, Result } from "@typeberry/utils";
import { type OK, Result } from "@typeberry/utils";

/**
* Parameters required to create a RefineExternalitiesImpl.
Expand All @@ -28,13 +34,19 @@ export type RefineExternalitiesParams = {
currentServiceId: ServiceId;
/** State at the lookup anchor block, used for historical preimage lookups. */
lookupState: State;
/** Export offset -- sum of exports from prior work items in this package. */
exportOffset: number;
};

export class RefineExternalitiesImpl implements RefineExternalities {
/** Service being refined (used as default for historicalLookup). */
private readonly currentServiceId: ServiceId;
/** State at the lookup anchor for preimage lookups. */
private readonly lookupState: State;
/** Segments exported by this work item during refinement. */
private readonly exportedSegments: Segment[] = [];
/** Offset for segment indexing (sum of exports from prior items). */
private readonly exportOffset: number;

static create(params: RefineExternalitiesParams) {
return new RefineExternalitiesImpl(params);
Expand All @@ -43,6 +55,11 @@ export class RefineExternalitiesImpl implements RefineExternalities {
private constructor(params: RefineExternalitiesParams) {
this.currentServiceId = params.currentServiceId;
this.lookupState = params.lookupState;
this.exportOffset = params.exportOffset;
}

getExportedSegments(): readonly Segment[] {
return this.exportedSegments;
}

machineExpunge(_machineIndex: MachineId): Promise<Result<ProgramCounter, NoMachineError>> {
Expand Down Expand Up @@ -98,8 +115,19 @@ export class RefineExternalitiesImpl implements RefineExternalities {
throw new Error("Method not implemented.");
}

exportSegment(_segment: Segment): Result<SegmentIndex, SegmentExportError> {
throw new Error("Method not implemented.");
exportSegment(segment: Segment): Result<SegmentIndex, SegmentExportError> {
// https://graypaper.fluffylabs.dev/#/ab2cdbd/335d03335d03?v=0.7.2
const currentIndex = this.exportOffset + this.exportedSegments.length;
if (currentIndex >= MAX_NUMBER_OF_EXPORTS_WP) {
return Result.error(
SegmentExportError,
() =>
`Maximum number of exported segments exceeded (offset: ${this.exportOffset}, exported: ${this.exportedSegments.length})`,
);
}
// https://graypaper.fluffylabs.dev/#/ab2cdbd/337303337303?v=0.7.2
this.exportedSegments.push(segment);
return Result.ok(tryAsSegmentIndex(currentIndex));
}

historicalLookup(serviceId: ServiceId | null, hash: Blake2bHash): Promise<BytesBlob | null> {
Expand Down
24 changes: 19 additions & 5 deletions packages/jam/in-core/in-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type RefineResult = {

export type RefineItemResult = {
result: WorkResult;
exports: Segment[];
exports: readonly Segment[];
};

export enum RefineError {
Expand Down Expand Up @@ -171,13 +171,24 @@ export class InCore {
logger.log`[core:${core}] Authorized. Proceeding with work items verification. Anchor=${context.anchor}`;

// Verify the work items
let exportOffset = 0;
const refineResults: Awaited<ReturnType<InCore["refineItem"]>>[] = [];
for (const [idx, item] of items.entries()) {
logger.info`[core:${core}][i:${idx}] Refining item for service ${item.service}.`;

refineResults.push(
await this.refineItem(state, lookupState, idx, item, imports, extrinsics, core, workPackageHash),
const result = await this.refineItem(
state,
lookupState,
idx,
item,
imports,
extrinsics,
core,
workPackageHash,
exportOffset,
);
refineResults.push(result);
exportOffset += result.exports.length;
}

// amalgamate the work report now
Expand Down Expand Up @@ -268,6 +279,7 @@ export class InCore {
allExtrinsics: PerWorkItem<WorkItemExtrinsic[]>,
coreIndex: CoreIndex,
workPackageHash: WorkPackageHash,
exportOffset: number,
): Promise<RefineItemResult> {
const payloadHash = this.blake2b.hashBytes(item.payload);
const baseResult = {
Expand Down Expand Up @@ -311,6 +323,7 @@ export class InCore {
extrinsics: allExtrinsics,
currentServiceId: item.service,
lookupState,
exportOffset,
});

const executor = await PvmExecutor.createRefineExecutor(item.service, code, externalities, this.pvmBackend);
Expand All @@ -325,8 +338,7 @@ export class InCore {

const execResult = await executor.run(args, item.refineGasLimit);

// TODO [ToDr] get exports from externalities
const exports: Segment[] = [];
const exports = externalities.refine.getExportedSegments();
if (exports.length !== item.exportCount) {
return {
exports,
Expand Down Expand Up @@ -422,6 +434,7 @@ export class InCore {
extrinsics: PerWorkItem<WorkItemExtrinsic[]>;
currentServiceId: ServiceId;
lookupState: State;
exportOffset: number;
}): RefineHostCallExternalities {
// TODO [ToDr] Pass all required fetch data
const fetchExternalities = FetchExternalities.createForRefine(
Expand All @@ -434,6 +447,7 @@ export class InCore {
const refine = RefineExternalitiesImpl.create({
currentServiceId: args.currentServiceId,
lookupState: args.lookupState,
exportOffset: args.exportOffset,
});

return {
Expand Down
1 change: 1 addition & 0 deletions packages/jam/in-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@typeberry/database": "*",
"@typeberry/executor": "*",
"@typeberry/hash": "*",
"@typeberry/jam-host-calls": "*",
"@typeberry/logger": "*",
"@typeberry/numbers": "*",
"@typeberry/state": "*",
Expand Down
Loading
Loading