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
98 changes: 86 additions & 12 deletions src/core/rpc/__tests__/zks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,29 +75,69 @@ describe('rpc/zks.normalizeGenesis', () => {
genesis_root: '0x' + '55'.repeat(32),
};

it('normalizes tuples and camel-cases field names', () => {
it('normalizes tuples and camel-cases field names (raw additional_storage)', () => {
const normalized = normalizeGenesis(sample);

expect(normalized).toEqual({
initialContracts: [
{ address: sample.initial_contracts[0][0], bytecode: sample.initial_contracts[0][1] },
{ address: sample.initial_contracts[1][0], bytecode: sample.initial_contracts[1][1] },
],
additionalStorage: [
{ key: sample.additional_storage[0][0], value: sample.additional_storage[0][1] },
{
format: 'raw',
key: sample.additional_storage[0][0],
value: sample.additional_storage[0][1],
},
],
executionVersion: sample.execution_version,
genesisRoot: sample.genesis_root,
});
});

it('throws ZKsyncError on malformed response', () => {
try {
normalizeGenesis(null);
throw new Error('expected to throw');
} catch (e) {
expect(isZKsyncError(e)).toBe(true);
expect(String(e)).toContain('Malformed genesis response');
}
it('normalizes pretty additional_storage map format', () => {
const addr = '0x' + 'aa'.repeat(20);
const slot = '0x' + '33'.repeat(32);
const val = '0x' + '44'.repeat(32);

const prettySample = {
...sample,
additional_storage: {
[addr]: {
[slot]: val,
},
},
};

const normalized = normalizeGenesis(prettySample);

expect(normalized.additionalStorage).toEqual([
{
format: 'pretty',
address: addr,
key: slot,
value: val,
},
]);
});

it('falls back to additional_storage_raw when additional_storage is missing', () => {
const fallbackSample = {
initial_contracts: sample.initial_contracts,
additional_storage_raw: [['0x' + '33'.repeat(32), '0x' + '44'.repeat(32)]],
execution_version: sample.execution_version,
genesis_root: sample.genesis_root,
};

const normalized = normalizeGenesis(fallbackSample);

expect(normalized.additionalStorage).toEqual([
{
format: 'raw',
key: fallbackSample.additional_storage_raw[0][0],
value: fallbackSample.additional_storage_raw[0][1],
},
]);
});
});

Expand Down Expand Up @@ -222,26 +262,60 @@ describe('rpc/zks.getReceiptWithL2ToL1', () => {
});

describe('rpc/zks.getGenesis', () => {
it('returns normalized genesis data', async () => {
it('returns normalized genesis data (raw additional_storage)', async () => {
const raw = {
initial_contracts: [['0x' + '11'.repeat(20), '0x' + 'aa'.repeat(4)]],
additional_storage: [['0x' + '22'.repeat(32), '0x' + '33'.repeat(32)]],
execution_version: 9,
genesis_root: '0x' + '44'.repeat(32),
};

const rpc = createZksRpc(fakeTransport({ zks_getGenesis: raw }));
const out = await rpc.getGenesis();

expect(out).toEqual({
initialContracts: [
{ address: raw.initial_contracts[0][0], bytecode: raw.initial_contracts[0][1] },
],
additionalStorage: [
{ key: raw.additional_storage[0][0], value: raw.additional_storage[0][1] },
{
format: 'raw',
key: raw.additional_storage[0][0],
value: raw.additional_storage[0][1],
},
],
executionVersion: raw.execution_version,
genesisRoot: raw.genesis_root,
});
});
it('returns normalized genesis data (pretty additional_storage)', async () => {
const addr = '0x' + 'aa'.repeat(20);
const slot = '0x' + '22'.repeat(32);
const val = '0x' + '33'.repeat(32);

const raw = {
initial_contracts: [['0x' + '11'.repeat(20), '0x' + 'aa'.repeat(4)]],
additional_storage: {
[addr]: {
[slot]: val,
},
},
execution_version: 9,
genesis_root: '0x' + '44'.repeat(32),
};

const rpc = createZksRpc(fakeTransport({ zks_getGenesis: raw }));
const out = await rpc.getGenesis();

expect(out.additionalStorage).toEqual([
{
format: 'pretty',
address: addr,
key: slot,
value: val,
},
]);
});
});

describe('rpc/zks.getBlockMetadataByNumber', () => {
Expand Down
7 changes: 3 additions & 4 deletions src/core/rpc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,9 @@ export type GenesisContractDeployment = {
bytecode: Hex;
};

export type GenesisStorageEntry = {
key: Hex;
value: Hex;
};
export type GenesisStorageEntry =
| { format: 'raw'; key: Hex; value: Hex } // key = hashed_key
| { format: 'pretty'; address: Address; key: Hex; value: Hex }; // key = slot

export type GenesisInput = {
initialContracts: GenesisContractDeployment[];
Expand Down
79 changes: 67 additions & 12 deletions src/core/rpc/zks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ function ensureBigInt(
});
}

function isRecord(x: unknown): x is Record<string, unknown> {
return !!x && typeof x === 'object' && !Array.isArray(x);
}

function normalizeContractTuple(tuple: unknown, index: number): GenesisContractDeployment {
if (!Array.isArray(tuple) || tuple.length < 2) {
throw createError('RPC', {
Expand All @@ -191,7 +195,11 @@ function normalizeContractTuple(tuple: unknown, index: number): GenesisContractD
};
}

function normalizeStorageTuple(tuple: unknown, index: number): GenesisStorageEntry {
// Normalizes a "raw" storage entry tuple: [key, value]
function normalizeRawStorageTuple(
tuple: unknown,
index: number,
): Extract<GenesisStorageEntry, { format: 'raw' }> {
if (!Array.isArray(tuple) || tuple.length < 2) {
throw createError('RPC', {
resource: 'zksrpc' as Resource,
Expand All @@ -203,11 +211,67 @@ function normalizeStorageTuple(tuple: unknown, index: number): GenesisStorageEnt

const [keyRaw, valueRaw] = tuple as [unknown, unknown];
return {
format: 'raw' as const,
key: ensureHex(keyRaw, 'additional_storage.key', { index }),
value: ensureHex(valueRaw, 'additional_storage.value', { index }),
};
}

// Normalizes additional storage entries from either "raw" or "pretty" format.
function normalizeAdditionalStorage(
value: unknown,
record: Record<string, unknown>,
): GenesisStorageEntry[] {
const effective = value ?? record['additional_storage_raw'];

// Raw tuple format: [[key, value], ...]
if (Array.isArray(effective)) {
return effective.map((entry, index) => {
const kv = normalizeRawStorageTuple(entry, index);
return { format: 'raw' as const, key: kv.key, value: kv.value };
});
}

// Pretty format: { [address]: { [slot]: value } }
if (isRecord(effective)) {
const out: GenesisStorageEntry[] = [];
for (const [addrRaw, slotsRaw] of Object.entries(effective)) {
const address = ensureHex(addrRaw, 'additional_storage.address', {});

if (!isRecord(slotsRaw)) {
throw createError('RPC', {
resource: 'zksrpc' as Resource,
operation: 'zksrpc.normalizeGenesis',
message: 'Malformed genesis response: additional_storage[address] must be an object map.',
context: { address, valueType: typeof slotsRaw },
});
}

for (const [slotRaw, valRaw] of Object.entries(slotsRaw)) {
out.push({
format: 'pretty' as const,
address,
key: ensureHex(slotRaw, 'additional_storage.key', { address }),
value: ensureHex(valRaw, 'additional_storage.value', { address, key: slotRaw }),
});
}
}
return out;
}

throw createError('RPC', {
resource: 'zksrpc' as Resource,
operation: 'zksrpc.normalizeGenesis',
message:
'Malformed genesis response: additional_storage must be an array (raw) or an object map (pretty).',
context: {
valueType: typeof effective,
hasAdditionalStorage: 'additional_storage' in record,
hasAdditionalStorageRaw: 'additional_storage_raw' in record,
},
});
}

// Normalizes the genesis response into camel-cased fields and typed entries.
export function normalizeGenesis(raw: unknown): GenesisInput {
try {
Expand All @@ -232,23 +296,14 @@ export function normalizeGenesis(raw: unknown): GenesisInput {
});
}

const storageRaw = record['additional_storage'];
if (!Array.isArray(storageRaw)) {
throw createError('RPC', {
resource: 'zksrpc' as Resource,
operation: 'zksrpc.normalizeGenesis',
message: 'Malformed genesis response: additional_storage must be an array.',
context: { valueType: typeof storageRaw },
});
}

const executionVersion = ensureNumber(record['execution_version'], 'execution_version');
const genesisRoot = ensureHex(record['genesis_root'], 'genesis_root', {});

const initialContracts = contractsRaw.map((entry, index) =>
normalizeContractTuple(entry, index),
);
const additionalStorage = storageRaw.map((entry, index) => normalizeStorageTuple(entry, index));

const additionalStorage = normalizeAdditionalStorage(record['additional_storage'], record);

return {
initialContracts,
Expand Down