Skip to content

Commit 10faa03

Browse files
committed
feat: initial genesis merging
1 parent 0ce1866 commit 10faa03

17 files changed

+962
-218
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ Usage: network-bootstrapper [options] [command]
1010
Utilities for configuring Besu-based networks.
1111
1212
Options:
13-
-h, --help display help for command
13+
-h, --help display help for command
1414
1515
Commands:
16-
generate [options] Generate node identities, configure consensus, and emit a
17-
Besu genesis.
18-
help [command] display help for command
16+
generate [options] Generate node identities, configure consensus, and
17+
emit a Besu genesis.
18+
compile-genesis [options] Merge per-account allocation ConfigMaps into a Besu
19+
genesis file.
20+
help [command] display help for command
1921
2022
2123
Usage: network-bootstrapper generate [options]

src/cli/allocations.test.ts renamed to src/cli/commands/bootstrap/bootstrap.allocations.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { join } from "node:path";
55

66
import { getAddress } from "viem";
77

8-
import { loadAllocations } from "./allocations.ts";
8+
import { loadAllocations } from "./bootstrap.allocations.ts";
99

1010
const INVALID_JSON_ERROR = /Allocations file is not valid JSON/;
1111
const INVALID_ADDRESS_ERROR = /Invalid address/;

src/cli/allocations.ts renamed to src/cli/commands/bootstrap/bootstrap.allocations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getAddress, isAddress, isHex } from "viem";
22
import { z } from "zod";
33

4-
import type { BesuAllocAccount } from "../genesis/besu-genesis.service.ts";
4+
import type { BesuAllocAccount } from "../../../genesis/besu-genesis.service.ts";
55

66
type HexLiteral = `0x${string}`;
77

src/cli/colors.test.ts renamed to src/cli/commands/bootstrap/bootstrap.colors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from "bun:test";
22

3-
import { accent, label, muted, success } from "./colors.ts";
3+
import { accent, label, muted, success } from "./bootstrap.colors.ts";
44

55
const SAMPLE = "hello";
66
const RESET = "\x1b[0m";
File renamed without changes.

src/cli/build-command.test.ts renamed to src/cli/commands/bootstrap/bootstrap.command.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
22

33
import { getAddress } from "viem";
4-
import { ARTIFACT_DEFAULTS } from "../constants/artifact-defaults.ts";
5-
import type { BesuAllocAccount } from "../genesis/besu-genesis.service.ts";
6-
import { ALGORITHM } from "../genesis/besu-genesis.service.ts";
7-
import type { GeneratedNodeKey } from "../keys/node-key-factory.ts";
8-
import type { BootstrapDependencies, CliOptions } from "./build-command.ts";
9-
import { createCliCommand, runBootstrap } from "./build-command.ts";
10-
import type { OutputPayload, OutputType } from "./output.ts";
11-
import { outputResult as realOutputResult } from "./output.ts";
4+
import { ARTIFACT_DEFAULTS } from "../../../constants/artifact-defaults.ts";
5+
import type { BesuAllocAccount } from "../../../genesis/besu-genesis.service.ts";
6+
import { ALGORITHM } from "../../../genesis/besu-genesis.service.ts";
7+
import type { GeneratedNodeKey } from "../../../keys/node-key-factory.ts";
8+
import type { BootstrapDependencies, CliOptions } from "./bootstrap.command.ts";
9+
import { createCliCommand, runBootstrap } from "./bootstrap.command.ts";
10+
import type { OutputPayload, OutputType } from "./bootstrap.output.ts";
11+
import { outputResult as realOutputResult } from "./bootstrap.output.ts";
1212

1313
const VALIDATOR_LABEL = "validator nodes";
1414
const VALIDATOR_RETURN = 2;

src/cli/build-command.ts renamed to src/cli/commands/bootstrap/bootstrap.command.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
import { Command, InvalidArgumentError } from "commander";
2-
import { ARTIFACT_DEFAULTS } from "../constants/artifact-defaults.ts";
2+
import { ARTIFACT_DEFAULTS } from "../../../constants/artifact-defaults.ts";
33
import {
44
ALGORITHM,
55
type Algorithm,
66
BesuGenesisService,
7-
} from "../genesis/besu-genesis.service.ts";
8-
import { NodeKeyFactory } from "../keys/node-key-factory.ts";
9-
import { loadAllocations } from "./allocations.ts";
10-
import { type HexAddress, promptForGenesisConfig } from "./genesis-prompts.ts";
7+
} from "../../../genesis/besu-genesis.service.ts";
8+
import { NodeKeyFactory } from "../../../keys/node-key-factory.ts";
9+
import { createCompileGenesisCommand } from "../compile-genesis/compile-genesis.command.ts";
10+
import { loadAllocations } from "./bootstrap.allocations.ts";
11+
import {
12+
type HexAddress,
13+
promptForGenesisConfig,
14+
} from "./bootstrap.genesis-prompts.ts";
1115
import {
1216
outputResult as defaultOutputResult,
1317
type IndexedNode,
1418
type OutputPayload,
1519
type OutputType,
16-
} from "./output.ts";
20+
} from "./bootstrap.output.ts";
1721
import {
1822
createCountParser,
1923
promptForCount,
2024
promptForText,
21-
} from "./prompt-helpers.ts";
25+
} from "./bootstrap.prompt-helpers.ts";
2226

2327
type CliOptions = {
2428
allocations?: string;
@@ -600,6 +604,9 @@ const createCliCommand = (
600604
await runBootstrap(sanitizedOptions, deps);
601605
});
602606

607+
// Register subcommands from their own modules to keep the bootstrap surface composable.
608+
command.addCommand(createCompileGenesisCommand());
609+
603610
return command;
604611
};
605612

src/cli/genesis-prompts.test.ts renamed to src/cli/commands/bootstrap/bootstrap.genesis-prompts.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import {
66
type BesuAllocAccount,
77
type BesuGenesis,
88
type BesuGenesisService,
9-
} from "../genesis/besu-genesis.service.ts";
9+
} from "../../../genesis/besu-genesis.service.ts";
1010
import {
1111
type HexAddress,
1212
type PromptOverrides,
1313
promptForGenesisConfig,
14-
} from "./genesis-prompts.ts";
14+
} from "./bootstrap.genesis-prompts.ts";
1515

1616
const CHAIN_ID_RESPONSE = 42;
1717
const BLOCK_TIME_RESPONSE = 5;

src/cli/genesis-prompts.ts renamed to src/cli/commands/bootstrap/bootstrap.genesis-prompts.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import {
77
type BesuGenesis,
88
type BesuGenesisService,
99
type BesuNetworkConfig,
10-
} from "../genesis/besu-genesis.service.ts";
11-
import { accent } from "./colors.ts";
10+
} from "../../../genesis/besu-genesis.service.ts";
11+
import { accent } from "./bootstrap.colors.ts";
1212
import {
1313
ABORT_MESSAGE,
1414
ABORT_OPTION,
1515
promptForBigIntString,
1616
promptForInteger,
17-
} from "./prompt-helpers.ts";
17+
} from "./bootstrap.prompt-helpers.ts";
1818

1919
const HEX_RADIX = 16;
2020

src/cli/output.test.ts renamed to src/cli/commands/bootstrap/bootstrap.output.test.ts

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@ import { join } from "node:path";
44

55
import { type CoreV1Api, KubeConfig } from "@kubernetes/client-node";
66

7-
import { ARTIFACT_DEFAULTS } from "../constants/artifact-defaults.ts";
8-
import type { IndexedNode, OutputPayload, OutputType } from "./output.ts";
7+
import { ARTIFACT_DEFAULTS } from "../../../constants/artifact-defaults.ts";
8+
import {
9+
ALGORITHM,
10+
type BesuAllocAccount,
11+
BesuGenesisService,
12+
} from "../../../genesis/besu-genesis.service.ts";
13+
import type {
14+
IndexedNode,
15+
OutputPayload,
16+
OutputType,
17+
} from "./bootstrap.output.ts";
918
import {
1019
outputResult,
1120
printFaucet,
1221
printGenesis,
1322
printGroup,
14-
} from "./output.ts";
23+
} from "./bootstrap.output.ts";
1524

1625
let output = "";
1726
let originalWrite: typeof process.stdout.write;
@@ -77,7 +86,7 @@ const PUBLIC_KEY_HEX_LENGTH = 128;
7786
const HEX_RADIX = 16;
7887
const SAMPLE_VALIDATOR_INDEX = 1;
7988
const SAMPLE_FAUCET_INDEX = 99;
80-
const EXPECTED_CONFIGMAP_COUNT = 7;
89+
const EXPECTED_CONFIGMAP_COUNT = 8;
8190
const EXPECTED_SECRET_COUNT = 2;
8291
const HEX_PREFIX_PATTERN = /^0x/;
8392
const TEST_CHAIN_ID = 1;
@@ -217,10 +226,37 @@ const staticNodeUri = (
217226

218227
const sampleValidator = sampleNode(SAMPLE_VALIDATOR_INDEX);
219228
const sampleFaucet = sampleNode(SAMPLE_FAUCET_INDEX);
229+
const allocationTarget = sampleNode(SAMPLE_FAUCET_INDEX + 1);
230+
231+
const SAMPLE_EXTRA_ALLOCATION: BesuAllocAccount = {
232+
balance: "0x1234",
233+
code: "0x6000",
234+
storage: {
235+
"0x1": "0x2",
236+
},
237+
};
238+
239+
const toAllocationConfigMapName = (address: string): string =>
240+
`alloc-${address.slice(2).toLowerCase()}`;
241+
242+
const genesisService = new BesuGenesisService(TEST_CHAIN_ID);
243+
const sampleGenesis = genesisService.generate(
244+
ALGORITHM.QBFT,
245+
{
246+
chainId: TEST_CHAIN_ID,
247+
faucetWalletAddress: sampleFaucet.address,
248+
gasLimit: "0x1",
249+
gasPrice: 0,
250+
secondsPerBlock: 2,
251+
},
252+
{
253+
[allocationTarget.address]: SAMPLE_EXTRA_ALLOCATION,
254+
}
255+
);
220256

221257
const samplePayload: OutputPayload = {
222258
faucet: sampleFaucet,
223-
genesis: { config: { chainId: TEST_CHAIN_ID }, extraData: "0xabc" },
259+
genesis: sampleGenesis,
224260
validators: [sampleValidator],
225261
staticNodes: [
226262
staticNodeUri(
@@ -253,6 +289,9 @@ describe("outputResult", () => {
253289

254290
await outputResult("file", samplePayload);
255291

292+
expect(output).toContain("[bootstrap] Output mode: file");
293+
expect(output).toContain("[bootstrap] Writing besu-genesis.json");
294+
256295
const directories = await readdir("out");
257296
expect(directories.length).toBe(1);
258297
const targetDir = directories[0];
@@ -300,6 +339,7 @@ describe("outputResult", () => {
300339
namespace: string;
301340
name: string;
302341
data: Record<string, string>;
342+
immutable?: boolean;
303343
}> = [];
304344
const createdSecrets: Array<{
305345
namespace: string;
@@ -336,6 +376,7 @@ describe("outputResult", () => {
336376
namespace,
337377
name: body?.metadata?.name ?? "",
338378
data: body?.data ?? {},
379+
immutable: body?.immutable,
339380
});
340381
return Promise.resolve();
341382
},
@@ -364,6 +405,10 @@ describe("outputResult", () => {
364405

365406
await outputResult("kubernetes", samplePayload);
366407

408+
expect(output).toContain("[bootstrap] Output mode: kubernetes");
409+
expect(output).toContain("[bootstrap] ConfigMap → besu-genesis");
410+
expect(output).toContain("[bootstrap] Secret → besu-faucet-private-key");
411+
367412
expect(listedConfigNamespaces).toEqual(["test-namespace"]);
368413
expect(listedSecretNamespaces).toEqual(["test-namespace"]);
369414
expect(createdConfigMaps).toHaveLength(EXPECTED_CONFIGMAP_COUNT);
@@ -374,6 +419,9 @@ describe("outputResult", () => {
374419
expect(mapNames).toContain("besu-faucet-address");
375420
expect(mapNames).toContain("besu-faucet-pubkey");
376421
expect(mapNames).toContain("besu-static-nodes");
422+
expect(mapNames).toContain(
423+
toAllocationConfigMapName(allocationTarget.address)
424+
);
377425
expect(mapNames).not.toContain("besu-faucet-enode");
378426
const secretNames = createdSecrets.map((entry) => entry.name).sort();
379427
expect(secretNames).toEqual([
@@ -391,6 +439,44 @@ describe("outputResult", () => {
391439
expect(
392440
JSON.parse(staticNodesConfig?.data?.["static-nodes.json"] ?? "[]")
393441
).toEqual(samplePayload.staticNodes);
442+
const genesisConfig = createdConfigMaps.find(
443+
(entry) => entry.name === "besu-genesis"
444+
);
445+
expect(genesisConfig?.immutable).toBe(true);
446+
const parsedGenesis = JSON.parse(
447+
genesisConfig?.data?.["genesis.json"] ?? "{}"
448+
) as {
449+
config: { chainId: number };
450+
alloc?: Record<string, BesuAllocAccount>;
451+
};
452+
expect(parsedGenesis.config.chainId).toBe(TEST_CHAIN_ID);
453+
const allocations = parsedGenesis.alloc;
454+
if (!allocations) {
455+
throw new Error("expected allocations payload");
456+
}
457+
const faucetAlloc = allocations[sampleFaucet.address];
458+
if (!faucetAlloc) {
459+
throw new Error("expected faucet allocation entry");
460+
}
461+
const expectedFaucetAlloc = sampleGenesis.alloc[sampleFaucet.address];
462+
if (!expectedFaucetAlloc) {
463+
throw new Error("sample genesis missing faucet entry");
464+
}
465+
expect(faucetAlloc.balance).toBe(expectedFaucetAlloc.balance);
466+
const placeholderAlloc = allocations[allocationTarget.address];
467+
if (!placeholderAlloc) {
468+
throw new Error("expected placeholder allocation entry");
469+
}
470+
expect(placeholderAlloc.balance).toBe("0x0");
471+
const allocationConfig = createdConfigMaps.find(
472+
(entry) =>
473+
entry.name === toAllocationConfigMapName(allocationTarget.address)
474+
);
475+
expect(allocationConfig?.immutable).toBe(true);
476+
const parsedAllocation = JSON.parse(
477+
allocationConfig?.data?.["alloc.json"] ?? "{}"
478+
) as BesuAllocAccount;
479+
expect(parsedAllocation).toEqual(SAMPLE_EXTRA_ALLOCATION);
394480
} finally {
395481
(KubeConfig.prototype as any).loadFromCluster = originalLoad;
396482
(KubeConfig.prototype as any).makeApiClient = originalMake;
@@ -429,7 +515,11 @@ describe("outputResult", () => {
429515
"custom-genesis",
430516
"custom-static",
431517
"custom-validator-0-address",
518+
"custom-validator-0-enode",
519+
"custom-validator-0-pubkey",
432520
"custom-faucet-address",
521+
"custom-faucet-pubkey",
522+
toAllocationConfigMapName(allocationTarget.address),
433523
];
434524
const expectedSecrets = [
435525
"custom-faucet-private-key",

0 commit comments

Comments
 (0)