diff --git a/packages/jammin-sdk/config/config-validator.test.ts b/packages/jammin-sdk/config/config-validator.test.ts index 44c7016..0274c54 100644 --- a/packages/jammin-sdk/config/config-validator.test.ts +++ b/packages/jammin-sdk/config/config-validator.test.ts @@ -520,6 +520,295 @@ describe("Validate Build Config", () => { }); }); + describe("Preimage Blobs Config Validation", () => { + test("Should parse valid preimage_blobs config", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_blobs: { + "0x0000000000000000000000000000000000000000000000000000000000000001": "0xdeadbeef", + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890": "0xadadadadad", + }, + }, + }, + }, + }; + + const result = validateBuildConfig(config); + expect(result.deployment?.services?.["auth-service"]?.preimageBlobs).toEqual({ + "0x0000000000000000000000000000000000000000000000000000000000000001": "0xdeadbeef", + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890": "0xadadadadad", + }); + }); + + test("Should reject preimage_blobs with hash missing 0x prefix", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_blobs: { + "0000000000000000000000000000000000000000000000000000000000000001": "0x1234", + }, + }, + }, + }, + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + + test("Should reject preimage_blobs with blob missing 0x prefix", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_blobs: { + "0x0000000000000000000000000000000000000000000000000000000000000001": "deadbeef", + }, + }, + }, + }, + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + + test("Should reject preimage_blobs with blob consisting of an uneven number of characters", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_blobs: { + "0x0000000000000000000000000000000000000000000000000000000000000001": "0xabc", + }, + }, + }, + }, + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + + test("Should reject preimage_blobs where hash is not 32 bytes long", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_blobs: { + "0x00000000000000000000000000000001": "0xdeadbeef", // 16 bytes instead of 32 + }, + }, + }, + }, + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + }); + + describe("Preimage Requests Config Validation", () => { + test("Should parse valid preimage_requests config", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_requests: { + "0x0000000000000000000000000000000000000000000000000000000000000001": [100, 200, 300], + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890": [42], + }, + }, + }, + }, + }; + + const result = validateBuildConfig(config); + expect(result.deployment?.services?.["auth-service"]?.preimageRequests).toEqual({ + "0x0000000000000000000000000000000000000000000000000000000000000001": [100, 200, 300], + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890": [42], + }); + }); + + test("Should accept preimage_requests with empty array", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_requests: { + "0x0000000000000000000000000000000000000000000000000000000000000001": [], + }, + }, + }, + }, + }; + + const result = validateBuildConfig(config); + expect(result.deployment?.services?.["auth-service"]?.preimageRequests).toEqual({ + "0x0000000000000000000000000000000000000000000000000000000000000001": [], + }); + }); + + test("Should accept preimage_requests with 1, 2, and 3 time slots", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_requests: { + "0x0000000000000000000000000000000000000000000000000000000000000001": [1], + "0x0000000000000000000000000000000000000000000000000000000000000002": [1, 2], + "0x0000000000000000000000000000000000000000000000000000000000000003": [1, 2, 3], + }, + }, + }, + }, + }; + + const result = validateBuildConfig(config); + const preimageRequests = result.deployment?.services?.["auth-service"]?.preimageRequests; + expect(preimageRequests?.["0x0000000000000000000000000000000000000000000000000000000000000001"]).toEqual([1]); + expect(preimageRequests?.["0x0000000000000000000000000000000000000000000000000000000000000002"]).toEqual([1, 2]); + expect(preimageRequests?.["0x0000000000000000000000000000000000000000000000000000000000000003"]).toEqual([ + 1, 2, 3, + ]); + }); + + test("Should reject preimage_requests with more than 3 time slots", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_requests: { + "0x0000000000000000000000000000000000000000000000000000000000000001": [1, 2, 3, 4], + }, + }, + }, + }, + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + + test("Should reject preimage_requests with non-integer time slot values", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_requests: { + "0x0000000000000000000000000000000000000000000000000000000000000001": [100.5, 200], + }, + }, + }, + }, + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + + test("Should reject preimage_requests with negative time slot values", () => { + const config = { + services: [ + { + path: "./services/auth.ts", + name: "auth-service", + sdk: "jam-sdk-0.1.26", + }, + ], + deployment: { + spawn: "local", + services: { + "auth-service": { + preimage_requests: { + "0x0000000000000000000000000000000000000000000000000000000000000001": [-10, 0, 100], + }, + }, + }, + }, + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + }); + describe("Service Info Config Validation", () => { test("Should parse valid info config with all fields", () => { const config = { diff --git a/packages/jammin-sdk/config/config-validator.ts b/packages/jammin-sdk/config/config-validator.ts index 7dd85fd..56badf8 100644 --- a/packages/jammin-sdk/config/config-validator.ts +++ b/packages/jammin-sdk/config/config-validator.ts @@ -9,6 +9,11 @@ const MAX_U64 = 18_446_744_073_709_551_615n; const u64Schema = () => z.union([z.bigint().min(0n).max(MAX_U64), z.number().int().min(0)]).transform((val) => BigInt(val)); const u32Schema = () => z.number().int().min(0).max(MAX_U32); +const preimageHashSchema = () => + z + .string() + .regex(/^0x[0-9a-fA-F]+$/) + .length(66); // jammin.build.yml schema @@ -30,41 +35,52 @@ const ServiceConfigSchema = z.object({ ), }); -const ServiceDeploymentConfigSchema = z.object({ - id: u32Schema().optional(), - storage: z.record(z.string(), z.string()).optional(), - info: z - .object({ - balance: u64Schema().optional(), - accumulate_min_gas: u64Schema().optional(), - on_transfer_min_gas: u64Schema().optional(), - storage_utilisation_bytes: u64Schema().optional(), - gratis_storage: u64Schema().optional(), - storage_utilisation_count: u32Schema().optional(), - created: u32Schema().optional(), - last_accumulation: u32Schema().optional(), - parent_service: u32Schema().optional(), - }) - .transform((info) => { - // snake to camel case - if (!info) { - return undefined; - } - const transformed = { - balance: info.balance, - accumulateMinGas: info.accumulate_min_gas, - onTransferMinGas: info.on_transfer_min_gas, - storageUtilisationBytes: info.storage_utilisation_bytes, - gratisStorage: info.gratis_storage, - storageUtilisationCount: info.storage_utilisation_count, - created: info.created, - lastAccumulation: info.last_accumulation, - parentService: info.parent_service, - }; - return transformed; - }) - .optional(), -}); +const ServiceDeploymentConfigSchema = z + .object({ + id: u32Schema().optional(), + storage: z.record(z.string(), z.string()).optional(), + preimage_blobs: z.record(preimageHashSchema(), z.string().regex(/^0x([0-9a-fA-F]{2})+$/)).optional(), + preimage_requests: z.record(preimageHashSchema(), z.array(u32Schema()).max(3)).optional(), + info: z + .object({ + balance: u64Schema().optional(), + accumulate_min_gas: u64Schema().optional(), + on_transfer_min_gas: u64Schema().optional(), + storage_utilisation_bytes: u64Schema().optional(), + gratis_storage: u64Schema().optional(), + storage_utilisation_count: u32Schema().optional(), + created: u32Schema().optional(), + last_accumulation: u32Schema().optional(), + parent_service: u32Schema().optional(), + }) + .transform((info) => { + // snake to camel case + if (!info) { + return undefined; + } + const transformed = { + balance: info.balance, + accumulateMinGas: info.accumulate_min_gas, + onTransferMinGas: info.on_transfer_min_gas, + storageUtilisationBytes: info.storage_utilisation_bytes, + gratisStorage: info.gratis_storage, + storageUtilisationCount: info.storage_utilisation_count, + created: info.created, + lastAccumulation: info.last_accumulation, + parentService: info.parent_service, + }; + return transformed; + }) + .optional(), + }) + .transform((config) => { + const { preimage_blobs, preimage_requests, ...rest } = config; + return { + ...rest, + preimageBlobs: preimage_blobs, + preimageRequests: preimage_requests, + }; + }); const DeploymentConfigSchema = z.object({ spawn: z.string().min(1), diff --git a/packages/jammin-sdk/config/types/config.ts b/packages/jammin-sdk/config/types/config.ts index 8f63fdb..1e37423 100644 --- a/packages/jammin-sdk/config/types/config.ts +++ b/packages/jammin-sdk/config/types/config.ts @@ -39,6 +39,10 @@ export interface ServiceDeploymentConfig { id?: number; /** Storage key-value pairs */ storage?: Record; + /** Preimage blobs map: 32-byte preimage hash (0x hex) -> blob (0x hex) */ + preimageBlobs?: Record; + /** Preimage requests map: 32-byte preimage hash (0x hex) -> integer time slots */ + preimageRequests?: Record; /** Optional service account info overrides */ info?: ServiceAccountInfoConfig; } diff --git a/packages/jammin-sdk/genesis-state-generator.test.ts b/packages/jammin-sdk/genesis-state-generator.test.ts index 592a7fc..7dfc034 100644 --- a/packages/jammin-sdk/genesis-state-generator.test.ts +++ b/packages/jammin-sdk/genesis-state-generator.test.ts @@ -1,41 +1,225 @@ import { describe, expect, test } from "bun:test"; -import { BytesBlob } from "@typeberry/lib/bytes"; -import { generateGenesis, type ServiceBuildOutput, toJip4Schema } from "./genesis-state-generator"; -import { ServiceId } from "./types"; +import { Bytes, BytesBlob } from "@typeberry/lib/bytes"; +import { HashDictionary } from "@typeberry/lib/collections"; +import { Blake2b, HASH_SIZE } from "@typeberry/lib/hash"; +import { type StorageKey, tryAsLookupHistorySlots } from "@typeberry/lib/state"; +import { asOpaqueType } from "@typeberry/lib/utils"; +import { generateGenesis, generateState, toJip4Schema } from "./genesis-state-generator"; +import { Gas, ServiceId, Slot, U32, U64 } from "./types"; +import type { ServiceBuildOutput } from "./utils/generate-service-output"; + +const blake2b = await Blake2b.createHasher(); describe("genesis-generator", () => { - const services: ServiceBuildOutput[] = [ - { - id: ServiceId(42), - code: BytesBlob.parseBlob( - "0x5000156a616d2d626f6f7473747261702d7365727669636506302e312e32370a4170616368652d322e30012550617269747920546563686e6f6c6f67696573203c61646d696e407061726974792e696f3e20350028000002000020000800", - ), - }, - { - id: ServiceId(7), - code: BytesBlob.parseBlob( - "0x509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f8235742f", - ), - }, - ]; - - test("should generate genesis file", async () => { - // when - const genesis = toJip4Schema(generateGenesis(services)); - - // then - expect(genesis.id).toBe("testnet"); - expect(genesis.bootnodes).toEqual([]); - expect(genesis.genesis_header).toBeDefined(); - expect(genesis.genesis_state).toBeDefined(); - expect(Array.isArray(genesis.genesis_state)).toBe(true); - expect(genesis.genesis_state.length).toBeGreaterThan(0); - - const stateValues = genesis.genesis_state.map((entry) => entry[1]); - const serviceCodeHex = services[0]?.code.toString().substring(2); - const serviceCodeHex1 = services[1]?.code.toString().substring(2); - - expect(stateValues.some((v) => v?.includes(serviceCodeHex ?? ""))).toBe(true); - expect(stateValues.some((v) => v?.includes(serviceCodeHex1 ?? ""))).toBe(true); + const basicService: ServiceBuildOutput = { + id: ServiceId(42), + code: BytesBlob.parseBlob( + "0x5000156a616d2d626f6f7473747261702d7365727669636506302e312e32370a4170616368652d322e30012550617269747920546563686e6f6c6f67696573203c61646d696e407061726974792e696f3e20350028000002000020000800", + ), + }; + + const largerCodeService: ServiceBuildOutput = { + id: ServiceId(7), + code: BytesBlob.parseBlob( + "0x509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f82357426f2313559a271d6782dc00197b379f79cbe3c6a1e72f61f7b592c509f8235742f", + ), + }; + + describe("generateGenesis", () => { + test("should generate genesis with basic services", async () => { + const services = [basicService, largerCodeService]; + + const genesis = toJip4Schema(generateGenesis(services)); + + expect(genesis.id).toBe("testnet"); + expect(genesis.bootnodes).toEqual([]); + expect(genesis.genesis_header).toBeDefined(); + expect(genesis.genesis_state).toBeDefined(); + expect(Array.isArray(genesis.genesis_state)).toBe(true); + expect(genesis.genesis_state.length).toBeGreaterThan(0); + + const stateValues = genesis.genesis_state.map((entry) => entry[1]); + const serviceCodeHex = services[0]?.code.toString().substring(2); + const serviceCodeHex1 = services[1]?.code.toString().substring(2); + + expect(stateValues.some((v) => v?.includes(serviceCodeHex ?? ""))).toBe(true); + expect(stateValues.some((v) => v?.includes(serviceCodeHex1 ?? ""))).toBe(true); + }); + + test("should generate genesis with empty services array", async () => { + const genesis = toJip4Schema(generateGenesis([])); + + expect(genesis.id).toBe("testnet"); + expect(genesis.genesis_header).toBeDefined(); + expect(genesis.genesis_state).toBeDefined(); + }); + }); + + describe("generateState with storage", () => { + test("should store and retrieve storage items", async () => { + const storageKey1Str = "key1"; + const storageKey2Str = "key2"; + const storageValue1Str = "value1"; + const storageValue2Str = "value2"; + + const storageKey1: StorageKey = asOpaqueType(BytesBlob.blobFromString(storageKey1Str)); + const storageKey2: StorageKey = asOpaqueType(BytesBlob.blobFromString(storageKey2Str)); + const expectedValue1 = BytesBlob.blobFromString(storageValue1Str); + const expectedValue2 = BytesBlob.blobFromString(storageValue2Str); + + const serviceWithStorage: ServiceBuildOutput = { + ...basicService, + id: ServiceId(100), + storage: { + [storageKey1Str]: storageValue1Str, + [storageKey2Str]: storageValue2Str, + }, + }; + + const state = generateState([serviceWithStorage]); + + const service = state.services.get(ServiceId(100)); + expect(service).toBeDefined(); + + const retrievedValue1 = service?.getStorage(storageKey1); + const retrievedValue2 = service?.getStorage(storageKey2); + + expect(retrievedValue1?.toString()).toBe(expectedValue1.toString()); + expect(retrievedValue2?.toString()).toBe(expectedValue2.toString()); + }); + + test("should calculate storage utilisation correctly", async () => { + const serviceWithStorage: ServiceBuildOutput = { + ...basicService, + id: ServiceId(102), + storage: { + key: "value", + }, + }; + + const state = generateState([serviceWithStorage]); + + const service = state.services.get(ServiceId(102)); + expect(service).toBeDefined(); + + const info = service?.getInfo(); + expect(info?.storageUtilisationCount).toBe(U32(3)); + expect(info?.storageUtilisationBytes).toEqual(U64(217)); + }); + }); + + describe("generateState with custom service info", () => { + test("should apply custom balance", async () => { + const customBalance = U64(5_000_000n); + const serviceWithInfo: ServiceBuildOutput = { + ...basicService, + id: ServiceId(200), + info: { + balance: customBalance, + }, + }; + + const state = generateState([serviceWithInfo]); + + const serviceAccount = state.services.get(ServiceId(200)); + expect(serviceAccount).toBeDefined(); + expect(serviceAccount?.getInfo().balance).toEqual(customBalance); + }); + + test("should apply custom gas settings", async () => { + const customAccumulateGas = Gas(100n); + const customOnTransferGas = Gas(50n); + const serviceWithGas: ServiceBuildOutput = { + ...basicService, + id: ServiceId(201), + info: { + accumulateMinGas: customAccumulateGas, + onTransferMinGas: customOnTransferGas, + }, + }; + + const state = generateState([serviceWithGas]); + + const serviceAccount = state.services.get(ServiceId(201)); + expect(serviceAccount).toBeDefined(); + expect(serviceAccount?.getInfo().accumulateMinGas).toEqual(customAccumulateGas); + expect(serviceAccount?.getInfo().onTransferMinGas).toEqual(customOnTransferGas); + }); + }); + + describe("generateState with preimage blobs and preimage requests", () => { + test("should include additional preimage blobs", async () => { + const preimageBlob = BytesBlob.parseBlob("0xaabbccdd"); + const preimageHash = blake2b.hashBytes(preimageBlob).asOpaque(); + + const serviceWithPreimages: ServiceBuildOutput = { + ...basicService, + id: ServiceId(300), + preimageBlobs: HashDictionary.fromEntries([[preimageHash, preimageBlob]]), + preimageRequests: new Map([[preimageHash, tryAsLookupHistorySlots([Slot(0)])]]), + }; + + const state = generateState([serviceWithPreimages]); + + expect(state).toBeDefined(); + const serviceAccount = state.services.get(ServiceId(300)); + expect(serviceAccount).toBeDefined(); + }); + + test("should handle multiple slots in preimage requests", async () => { + const preimageBlob = BytesBlob.parseBlob("0x11223344"); + const preimageHash = blake2b.hashBytes(preimageBlob).asOpaque(); + + const serviceWithMultiSlots: ServiceBuildOutput = { + ...basicService, + id: ServiceId(301), + preimageBlobs: HashDictionary.fromEntries([[preimageHash, preimageBlob]]), + preimageRequests: new Map([[preimageHash, tryAsLookupHistorySlots([Slot(0), Slot(1), Slot(2)])]]), + }; + + const state = generateState([serviceWithMultiSlots]); + + expect(state).toBeDefined(); + const serviceAccount = state.services.get(ServiceId(301)); + expect(serviceAccount).toBeDefined(); + }); + + test("should throw error when preimage blob is missing for preimage request entry", async () => { + const missingHash = Bytes.parseBytes( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + HASH_SIZE, + ).asOpaque(); + + const serviceWithMissingPreimage: ServiceBuildOutput = { + ...basicService, + id: ServiceId(302), + preimageBlobs: HashDictionary.fromEntries([]), // Empty - no preimage for the hash + preimageRequests: new Map([[missingHash, tryAsLookupHistorySlots([Slot(0)])]]), + }; + + expect(() => generateState([serviceWithMissingPreimage])).toThrow("Preimage blob not found for hash"); + }); + + test("should calculate storage utilisation correctly with additional preimages", async () => { + const preimageBlob = BytesBlob.parseBlob("0xaabbccdd"); + const preimageHash = blake2b.hashBytes(preimageBlob).asOpaque(); + + const serviceWithPreimages: ServiceBuildOutput = { + ...basicService, + id: ServiceId(303), + preimageBlobs: HashDictionary.fromEntries([[preimageHash, preimageBlob]]), + preimageRequests: new Map([[preimageHash, tryAsLookupHistorySlots([Slot(0)])]]), + }; + + const state = generateState([serviceWithPreimages]); + + const service = state.services.get(ServiceId(303)); + expect(service).toBeDefined(); + + const info = service?.getInfo(); + + expect(info?.storageUtilisationCount).toBe(U32(4)); + expect(info?.storageUtilisationBytes).toEqual(U64(260)); + }); }); }); diff --git a/packages/jammin-sdk/genesis-state-generator.ts b/packages/jammin-sdk/genesis-state-generator.ts index aece000..20ad724 100644 --- a/packages/jammin-sdk/genesis-state-generator.ts +++ b/packages/jammin-sdk/genesis-state-generator.ts @@ -32,6 +32,9 @@ export type Genesis = JipChainSpec; // https://graypaper.fluffylabs.dev/#/ab2cdbd/11e00111f001?v=0.7.2 const BASE_STORAGE_BYTES = 34n; +/** https://graypaper.fluffylabs.dev/#/ab2cdbd/11cc0111ce01?v=0.7.2 */ +const LOOKUP_HISTORY_ENTRY_BYTES = 81n; + // Base ServiceInfo const BASE_SERVICE: ServiceAccountInfo = { // actual codeHash of a given blob @@ -123,13 +126,60 @@ export function generateState(services: ServiceBuildOutput[]): InMemoryState { update.storage?.set(serviceId, storageUpdates); } - const calculatedStorageBytes = sumU64( + let calculatedStorageBytes = sumU64( BASE_SERVICE.storageUtilisationBytes, U64(service.code.length), U64(storageBytes), ).value; - const calculatedStorageCount = sumU32(BASE_SERVICE.storageUtilisationCount, U32(storageCount)).value; + let calculatedStorageCount = sumU32(BASE_SERVICE.storageUtilisationCount, U32(storageCount)).value; + + // add preimage blobs and preimage requests + if (service.preimageRequests) { + const preimageUpdates = update.preimages?.get(serviceId) ?? []; + + for (const [hash, slots] of service.preimageRequests) { + const preimageBlob = service.preimageBlobs?.get(hash); + + if (!preimageBlob) { + throw new Error(`Preimage blob not found for hash ${hash}`); + } + + // request preimage + const lookupHistory = new LookupHistoryItem(hash, U32(preimageBlob.length), tryAsLookupHistorySlots([])); + preimageUpdates.push(UpdatePreimage.updateOrAdd({ lookupHistory })); + + // update storage utilisation + calculatedStorageBytes = sumU64( + calculatedStorageBytes, + U64(LOOKUP_HISTORY_ENTRY_BYTES + BigInt(preimageBlob.length)), + ).value; + calculatedStorageCount = sumU32(calculatedStorageCount, U32(2)).value; + + // provide + if (slots.length >= 1) { + if (preimageBlob) { + preimageUpdates.push( + UpdatePreimage.provide({ + preimage: PreimageItem.create({ hash, blob: preimageBlob }), + providedFor: serviceId, + slot: slots[0], + }), + ); + } + } + + // update lookup history + if (slots.length > 1) { + const lookupHistory = new LookupHistoryItem(hash, U32(preimageBlob.length), slots); + preimageUpdates.push(UpdatePreimage.updateOrAdd({ lookupHistory })); + } + } + + if (preimageUpdates.length > 0) { + update.preimages?.set(serviceId, preimageUpdates); + } + } // create service update.updated?.set( diff --git a/packages/jammin-sdk/utils/generate-service-output.ts b/packages/jammin-sdk/utils/generate-service-output.ts index 25755e9..fb6876b 100644 --- a/packages/jammin-sdk/utils/generate-service-output.ts +++ b/packages/jammin-sdk/utils/generate-service-output.ts @@ -1,16 +1,26 @@ import { join, resolve } from "node:path"; -import { type ServiceId as ServiceIdType, tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/lib/block"; -import { BytesBlob } from "@typeberry/lib/bytes"; +import { + type preimage, + type ServiceId as ServiceIdType, + tryAsServiceGas, + tryAsServiceId, + tryAsTimeSlot, +} from "@typeberry/lib/block"; +import { Bytes, BytesBlob } from "@typeberry/lib/bytes"; +import { HashDictionary } from "@typeberry/lib/collections"; +import { HASH_SIZE } from "@typeberry/lib/hash"; import { tryAsU32, tryAsU64 } from "@typeberry/lib/numbers"; -import type { ServiceAccountInfo } from "@typeberry/lib/state"; +import { type LookupHistorySlots, type ServiceAccountInfo, tryAsLookupHistorySlots } from "@typeberry/lib/state"; import { loadBuildConfig } from "../config/config-loader.js"; -import { ServiceId } from "../types.js"; +import { ServiceId, Slot } from "../types.js"; export interface ServiceBuildOutput { id: ServiceIdType; code: BytesBlob; storage?: Record; info?: Partial; + preimageBlobs?: HashDictionary; + preimageRequests?: Map; } /** @@ -43,7 +53,16 @@ export async function loadServices(projectRoot: string = process.cwd()): Promise const jamFilePath = join(projectRoot, "dist", `${service.name}.jam`); const deployConfig = serviceDeployConfigs[service.name]; const serviceId = deployConfig?.id ?? getNextAvailableId(); - outputs.push(await generateServiceOutput(jamFilePath, serviceId, deployConfig?.storage, deployConfig?.info)); + outputs.push( + await generateServiceOutput( + jamFilePath, + serviceId, + deployConfig?.storage, + deployConfig?.info, + deployConfig?.preimageBlobs, + deployConfig?.preimageRequests, + ), + ); } return outputs; @@ -64,6 +83,8 @@ export async function generateServiceOutput( lastAccumulation?: number; parentService?: number; }, + preimageBlobs?: Record, + preimageRequests?: Record, ): Promise { const absolutePath = resolve(jamFilePath); const fileBytes = await Bun.file(absolutePath).bytes(); @@ -83,10 +104,26 @@ export async function generateServiceOutput( }).filter(([_, value]) => value !== undefined), ); + const preimageBlobsDict = HashDictionary.fromEntries( + Object.entries(preimageBlobs ?? {}).map(([hash, blob]) => [ + Bytes.parseBytes(hash, HASH_SIZE).asOpaque(), + BytesBlob.blobFromString(blob), + ]), + ); + + const preimageRequestsMap = new Map( + Object.entries(preimageRequests ?? {}).map(([hash, slots]) => [ + Bytes.parseBytes(hash, HASH_SIZE).asOpaque(), + tryAsLookupHistorySlots(slots.map((slot) => Slot(slot))), + ]), + ); + return { id: ServiceId(serviceId), code, storage, info: serviceAccountInfo, + preimageBlobs: preimageBlobsDict, + preimageRequests: preimageRequestsMap, }; }