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
13 changes: 10 additions & 3 deletions bin/jam/args.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type PvmBackend, PvmBackendNames } from "@typeberry/config";
import { DEFAULT_CONFIG, DEV_CONFIG, NODE_DEFAULTS } from "@typeberry/config-node";
import { DEFAULT_CONFIG, DEV_TINY_CONFIG, NODE_DEFAULTS } from "@typeberry/config-node";
import { logger } from "@typeberry/node";
import { isU16, type U16 } from "@typeberry/numbers";
import { version } from "@typeberry/utils";
Expand Down Expand Up @@ -28,7 +28,7 @@ Options:
--${ARGS.NAME} Override node name. Affects networking key and db location.
[default: ${NODE_DEFAULTS.name}]
--${ARGS.CONFIG} Configuration directives. If specified more than once, they are evaluated and merged from left to right.
A configuration directive can be a path to a config file, an inline JSON object, a pseudo-jq query or one of predefined configs ['${DEV_CONFIG}', '${DEFAULT_CONFIG}'].
A configuration directive can be a path to a config file, an inline JSON object, a pseudo-jq query or one of predefined configs ['${DEV_TINY_CONFIG}', '${DEFAULT_CONFIG}'].
Pseudo-jq queries are a way to modify the config using a subset of jq syntax.
Example: --${ARGS.CONFIG}=dev --${ARGS.CONFIG}=.chain_spec+={"bootnodes": []} -- will modify only the bootnodes property of the chain spec (merge).
Example: --${ARGS.CONFIG}=dev --${ARGS.CONFIG}=.chain_spec={"bootnodes": []} -- will replace the entire chain spec property with the provided JSON object.
Expand Down Expand Up @@ -150,7 +150,14 @@ export function parseArgs(input: string[], withRelPath: (v: string) => string):
return { command: Command.Run, args: data };
}
case Command.Dev: {
const data = parseSharedOptions(args, [DEV_CONFIG]);
const shared = parseSharedOptions(args, [DEV_TINY_CONFIG]);
// Dev mode always needs a base dev config. If the user only provided jq-query
// modifiers (e.g. `--config=.database_base_path=...`), keep the tiny dev config
// as the base and layer the modifiers on top — otherwise the query would be
// applied to an empty config and fail. A provided base config / file replaces
// it as usual.
const hasBaseConfig = shared.config.some((c) => !c.startsWith("."));
const data = hasBaseConfig ? shared : { ...shared, config: [DEV_TINY_CONFIG, ...shared.config] };
const indexOrAll = args._.shift();
if (indexOrAll === undefined) {
throw new Error("Missing dev-validator index.");
Expand Down
2 changes: 1 addition & 1 deletion bin/jam/build-for-npm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ flatten_worker() {
cd $DIST_FOLDER
flatten_worker importer bootstrap-importer
flatten_worker jam-network bootstrap-network
flatten_worker block-authorship bootstrap-generator
flatten_worker block-authorship bootstrap-authorship

# copy worker wasm files
cp **/*.wasm ./ || true # ignore overwrite errors
Expand Down
161 changes: 161 additions & 0 deletions bin/jam/helpers/generate-full-genesis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/usr/bin/env tsx
// biome-ignore-all lint/suspicious/noConsole: bin file

// Generate a "full"-flavor dev config JSON with 1023 trivial-seed validators.
// Output goes to stdout; redirect to e.g. packages/configs/typeberry-dev-full.json
// then run with:
// npm start -- --config packages/configs/typeberry-dev-full.json dev all --fast-forward

import {
Block,
DisputesExtrinsic,
Extrinsic,
Header,
reencodeAsView,
tryAsPerEpochBlock,
tryAsPerValidator,
tryAsTimeSlot,
tryAsValidatorIndex,
} from "@typeberry/block";
import { Bytes, BytesBlob } from "@typeberry/bytes";
import { Encoder } from "@typeberry/codec";
import { asKnownSize } from "@typeberry/collections";
import { fullChainSpec } from "@typeberry/config";
import { BLS_KEY_BYTES, initWasm } from "@typeberry/crypto";
import {
deriveBandersnatchPublicKey,
deriveBandersnatchSecretKey,
deriveEd25519PublicKey,
deriveEd25519SecretKey,
trivialSeed,
} from "@typeberry/crypto/key-derivation.js";
import { Blake2b } from "@typeberry/hash";
import { tryAsU32 } from "@typeberry/numbers";
import bandersnatchVrf from "@typeberry/safrole/bandersnatch-vrf.js";
import { BandernsatchWasm } from "@typeberry/safrole/bandersnatch-wasm.js";
import { InMemoryState, SafroleSealingKeysData, VALIDATOR_META_BYTES, ValidatorData } from "@typeberry/state";
import { StateEntries } from "@typeberry/state-merkleization";
import { TransitionHasher } from "@typeberry/transition";
import { asOpaqueType } from "@typeberry/utils";

async function main() {
await initWasm();
const blake2b = await Blake2b.createHasher();
const bandersnatch = await BandernsatchWasm.new();
const spec = fullChainSpec;
const n = spec.validatorsCount;

console.error(`Deriving ${n} validator keys...`);
const validators: ValidatorData[] = [];
for (let i = 0; i < n; i++) {
const seed = trivialSeed(tryAsU32(i));
const bandersnatchSecret = deriveBandersnatchSecretKey(seed, blake2b);
const ed25519Secret = deriveEd25519SecretKey(seed, blake2b);
const bandersnatchPub = deriveBandersnatchPublicKey(bandersnatchSecret);
const ed25519Pub = await deriveEd25519PublicKey(ed25519Secret);
validators.push(
ValidatorData.create({
bandersnatch: bandersnatchPub,
ed25519: ed25519Pub,
bls: Bytes.zero(BLS_KEY_BYTES).asOpaque(),
metadata: Bytes.zero(VALIDATOR_META_BYTES).asOpaque(),
}),
);
if ((i + 1) % 100 === 0) {
console.error(` ${i + 1}/${n}`);
}
}

console.error("Computing ring commitment...");
const ringRootResult = await bandersnatchVrf.getRingCommitment(
bandersnatch,
validators.map((v) => v.bandersnatch),
);
if (ringRootResult.isError) {
throw new Error(`Failed to compute ring commitment: ${ringRootResult.error}`);
}
const epochRoot = ringRootResult.ok;

console.error("Building genesis state...");
const state = InMemoryState.empty(spec);
const perValidator = tryAsPerValidator(validators, spec);
state.designatedValidatorData = perValidator;
state.nextValidatorData = perValidator;
state.currentValidatorData = perValidator;
state.previousValidatorData = perValidator;
state.epochRoot = epochRoot;
// Fallback sealing keys: cycle bandersnatch keys across epoch slots. The first
// epoch transition will recompute this via safrole; we only need a valid
// SafroleSealingKeysData here so the genesis state encodes successfully.
state.sealingKeySeries = SafroleSealingKeysData.keys(
tryAsPerEpochBlock(
Array.from({ length: spec.epochLength }, (_, i) => validators[i % n].bandersnatch),
spec,
),
);

console.error("Serialising state entries...");
const stateEntries = StateEntries.serializeInMemory(spec, blake2b, state);
const stateRoot = stateEntries.getRootHash(blake2b);
console.error(`Genesis state root: ${stateRoot}`);

console.error("Building genesis header...");
const hasher = await TransitionHasher.create();
const extrinsic = Extrinsic.create({
tickets: asOpaqueType(asKnownSize([])),
preimages: [],
guarantees: asOpaqueType(asKnownSize([])),
assurances: asOpaqueType(asKnownSize([])),
disputes: DisputesExtrinsic.create({ verdicts: [], culprits: [], faults: [] }),
});
const extrinsicView = reencodeAsView(Extrinsic.Codec, extrinsic, spec);
const extrinsicHash = hasher.extrinsic(extrinsicView).hash;

const header = Header.create({
parentHeaderHash: Bytes.zero(32).asOpaque(),
priorStateRoot: Bytes.zero(32).asOpaque(),
extrinsicHash,
timeSlotIndex: tryAsTimeSlot(0),
epochMarker: null,
ticketsMarker: null,
bandersnatchBlockAuthorIndex: tryAsValidatorIndex(0xffff),
entropySource: Bytes.zero(96).asOpaque(),
offendersMarker: [],
seal: Bytes.zero(96).asOpaque(),
});
const encodedHeader = Encoder.encodeObject(Header.Codec, header, spec);

// Sanity check: roundtrip the genesis block through codec under fullChainSpec
// so we catch any size/shape mismatches now instead of at node startup.
reencodeAsView(Block.Codec, Block.create({ header, extrinsic }), spec);

console.error("Encoding JSON...");
const stateMap: Record<string, string> = {};
for (const [key, value] of stateEntries) {
stateMap[hexNoPrefix(key.raw)] = hexNoPrefix(value.raw);
}

const out = {
$schema: "https://fluffylabs.dev/typeberry/schemas/config-v1.schema.json",
version: 1,
flavor: "full",
authorship: {},
chain_spec: {
id: "typeberry-dev-full",
bootnodes: [],
genesis_header: hexNoPrefix(encodedHeader.raw),
genesis_state: stateMap,
},
};
process.stdout.write(JSON.stringify(out, null, 2));
process.stdout.write("\n");
}

function hexNoPrefix(bytes: Uint8Array): string {
return BytesBlob.blobFrom(bytes).toString().slice(2);
}

main().catch((err) => {
console.error("Error:", err);
process.exit(1);
});
File renamed without changes.
9 changes: 8 additions & 1 deletion bin/jam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"main": "index.ts",
"bin": "./index.ts",
"dependencies": {
"@typeberry/block": "*",
"@typeberry/bytes": "*",
"@typeberry/codec": "*",
"@typeberry/collections": "*",
"@typeberry/config": "*",
"@typeberry/config-node": "*",
"@typeberry/crypto": "*",
Expand All @@ -18,7 +21,11 @@
"@typeberry/networking": "*",
"@typeberry/node": "*",
"@typeberry/numbers": "*",
"@typeberry/safrole": "*",
"@typeberry/state": "*",
"@typeberry/state-merkleization": "*",
"@typeberry/telemetry": "*",
"@typeberry/transition": "*",
"@typeberry/utils": "*",
"@typeberry/workers-api": "*",
"minimist": "1.2.8"
Expand All @@ -29,7 +36,7 @@
"build": "./build-for-npm.sh",
"test": "tsx --test $(find . -type f -name '*.test.ts' | tr '\\n' ' ')",
"test:e2e": "JAM_LOG=trace tsx --test test/e2e.ts",
"tiny-network": "tsx ./tiny-network.ts"
"tiny-network": "tsx ./helpers/tiny-network.ts"
},
"author": "Fluffy Labs",
"license": "MPL-2.0",
Expand Down
4 changes: 2 additions & 2 deletions bin/jam/test/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const TARGET_BLOCK = 6;

const logger = Logger.new(import.meta.filename, "jam:e2e");

const bestBlockPattern = /🧊 Best block:.+#(\d+)/;
const bestBlockPattern = /🧊 Best:.+#(\d+)/;

test("JAM Node dev blocks with In Memory", { timeout: TEST_TIMEOUT }, async () => {
let jamProcess: ChildProcess | null = null;
Expand Down Expand Up @@ -127,7 +127,7 @@ async function collectLogsUntilBlock(
pattern: RegExp,
targetBlock: number,
): Promise<string[]> {
const blockPattern = /🧊 Best block:.+#(\d+)/;
const blockPattern = /🧊 Best:.+#(\d+)/;
const matchedLines: string[] = [];
let currentBlock = 0;

Expand Down
3 changes: 2 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"!**/benchmarks/**/output/**/*",
"!**/test-vectors/**/*",
"!**/packages/misc/builder/pkg.ts",
"!**/web/docs"
"!**/web/docs",
"!**/packages/configs/typeberry-dev-full.json"
]
}
}
8 changes: 8 additions & 0 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions packages/configs/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import defaultConfig from "./typeberry-default.json" with { type: "json" };
import devConfig from "./typeberry-dev.json" with { type: "json" };
import devFullConfig from "./typeberry-dev-full.json" with { type: "json" };
import devTinyConfig from "./typeberry-dev-tiny.json" with { type: "json" };

export const configs = {
default: defaultConfig,
dev: devConfig,
devTiny: devTinyConfig,
devFull: devFullConfig,
};
29 changes: 29 additions & 0 deletions packages/configs/typeberry-dev-full.json

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions packages/core/bytes/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ export class Bytes<T extends number> extends BytesBlob {
asOpaque<R>(): Opaque<Bytes<T>, TokenOf<R, Bytes<T>>> {
return asOpaqueType<Bytes<T>, TokenOf<R, Bytes<T>>>(this);
}

toStringTruncated() {
if (this.raw.length > 8) {
const start = bytesToHexString(this.raw.subarray(0, 2));
const end = bytesToHexString(this.raw.subarray(this.raw.length - 2));
return `${start}...${end.substring(2)}`;
}
return `${this.toString()}`;
}
}

function byteFromString(s: string): number {
Expand Down
18 changes: 12 additions & 6 deletions packages/core/utils/debug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,29 @@ describe("utils::lazyInspect", () => {

describe("utils::memoryUsage", () => {
it("should report all memory fields", () => {
const usage = memoryUsage();
const usage = memoryUsage(true);
for (const field of ["rss=", "heap=", "external=", "arrayBuffers="]) {
assert.ok(usage.includes(field), `expected "${field}" in "${usage}"`);
}
});
it("should report all memory fields without details", () => {
const usage = memoryUsage(false);
for (const field of ["rss=", "heap="]) {
assert.ok(usage.includes(field), `expected "${field}" in "${usage}"`);
}
});
});

describe("utils::memoryTracker", () => {
it("should not include a delta on the first call", () => {
const tracker = memoryTracker();
assert.ok(!tracker().includes("Δrss"));
const tracker = memoryTracker(true);
assert.ok(!tracker.toString().includes("Δrss"));
});

it("should include a delta on subsequent calls", () => {
const tracker = memoryTracker();
tracker();
const second = tracker();
const tracker = memoryTracker(true);
tracker.toString();
const second = tracker.toString();
assert.ok(second.includes("Δrss="), `expected delta in "${second}"`);
assert.ok(second.includes("ΔarrayBuffers="), `expected delta in "${second}"`);
});
Expand Down
Loading
Loading