Skip to content
Open
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
340 changes: 195 additions & 145 deletions docs/production-bootstrap.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions packages/cluster-tool-shared/src/cluster/ClusterState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export interface ClusterStateNode {
ports: ClusterStateNodePorts
/** Producer account names scheduled on this node (empty for pure operator nodes). */
producers: string[]
/** Batch-operator account this node acts for, when `role === operator`. */
batchOperatorAccount: string | null
/** Underwriter account this node acts for, when `role === operator`. */
underwriterAccount: string | null
/** Batch-operator provisioning label this node acts for, when `role === operator`. */
batchOperatorLabel: string | null
/** Underwriter provisioning label this node acts for, when `role === operator`. */
underwriterLabel: string | null
}

/**
Expand Down
14 changes: 7 additions & 7 deletions packages/cluster-tool-shared/tests/cluster/ClusterState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ describe("ClusterStateNode / ClusterState shape", () => {
nodePath: "/cluster/data/bios",
ports: { http: 8888, p2p: 9876 },
producers: ["defproducera"],
batchOperatorAccount: null,
underwriterAccount: null
batchOperatorLabel: null,
underwriterLabel: null
}

const operatorNode: ClusterStateNode = {
Expand All @@ -29,8 +29,8 @@ describe("ClusterStateNode / ClusterState shape", () => {
nodePath: "/cluster/data/node_01",
ports: { http: 8889, p2p: 9877 },
producers: [],
batchOperatorAccount: "batchop1",
underwriterAccount: null
batchOperatorLabel: "batchop1",
underwriterLabel: null
}

const state: ClusterState = {
Expand All @@ -50,9 +50,9 @@ describe("ClusterStateNode / ClusterState shape", () => {
])
})

it("distinguishes a batch operator from an underwriter via batchOperatorAccount", () => {
expect(operatorNode.batchOperatorAccount).toBe("batchop1")
expect(operatorNode.underwriterAccount).toBeNull()
it("distinguishes a batch operator from an underwriter via batchOperatorLabel", () => {
expect(operatorNode.batchOperatorLabel).toBe("batchop1")
expect(operatorNode.underwriterLabel).toBeNull()
})

it("survives a JSON round-trip with no data loss (secret-free persistence)", () => {
Expand Down
22 changes: 13 additions & 9 deletions packages/cluster-tool/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Hash, KeyType, PrivateKey } from "@wireio/sdk-core"

/**
* Cross-cutting harness constants — development keys, system-account names,
* token / ROA parameters, contract paths, plugin sets, account-name generators,
* and the emissions config defaults. Ported from the former
* token / ROA parameters, contract paths, plugin sets, operator-label
* generators, and the emissions config defaults. Ported from the former
* the former `cluster/constants.ts`; network ports are NOT here — they live on
* `config/BindConfigProvider.ts`, which owns binding.
*/
Expand Down Expand Up @@ -238,26 +238,30 @@ export namespace Constants {
"sysio::outpost_solana_client_plugin"
] as const

/** Lowercase ASCII alphabet — single-character operator-name suffixes (wraps via modulo). */
/** Lowercase ASCII alphabet — single-character operator-label suffixes (wraps via modulo). */
const LowercaseAlphabet = "abcdefghijklmnopqrstuvwxyz"

/**
* Batch-operator account name for an index — `batchop.<letter>`.
* Batch-operator provisioning label for an index — `batchop.<letter>`. The
* label is the operator's `ClusterKeyStore` key AND its `sysio.roa::newuser`
* sponsor nonce — the CHAIN account name is node-owner-generated
* (`wireno.<suffix>`), not this value.
*
* @param index - Zero-based operator index.
* @returns The account name (e.g. `batchOperatorAccountName(0)` → `"batchop.a"`).
* @returns The label (e.g. `batchOperatorLabel(0)` → `"batchop.a"`).
*/
export function batchOperatorAccountName(index: number): string {
export function batchOperatorLabel(index: number): string {
return `batchop.${LowercaseAlphabet[index % LowercaseAlphabet.length]}`
}

/**
* Underwriter account name for an index — `uwrit.<letter>`.
* Underwriter provisioning label for an index — `uwrit.<letter>`. Same
* label-vs-generated-account split as {@link batchOperatorLabel}.
*
* @param index - Zero-based underwriter index.
* @returns The account name (e.g. `underwriterAccountName(1)` → `"uwrit.b"`).
* @returns The label (e.g. `underwriterLabel(1)` → `"uwrit.b"`).
*/
export function underwriterAccountName(index: number): string {
export function underwriterLabel(index: number): string {
return `uwrit.${LowercaseAlphabet[index % LowercaseAlphabet.length]}`
}

Expand Down
8 changes: 5 additions & 3 deletions packages/cluster-tool/src/cluster/ClusterState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export interface ClusterKeysNodeEntry {
* `underwriterArgs`) build directly from them on relaunch.
*/
export interface ClusterKeysOperatorEntry {
/** WIRE account name the operator was provisioned under. */
/** Deterministic provisioning label — the `ClusterKeyStore` key. */
label: string
/** WIRE account name ON CHAIN (node-owner-generated for batch/underwriter operators). */
account: string
/** Operator role (batch operator / underwriter / producer). */
type: OperatorType
Expand Down Expand Up @@ -135,8 +137,8 @@ export namespace ClusterState {
nodePath: node.nodePath,
ports: { http: node.ports.http, p2p: node.ports.p2p },
producers: [...node.producers],
batchOperatorAccount: node.batchOperatorAccount,
underwriterAccount: node.underwriterAccount
batchOperatorLabel: node.batchOperatorLabel,
underwriterLabel: node.underwriterLabel
}))
return {
createdAt: new Date().toISOString(),
Expand Down
33 changes: 17 additions & 16 deletions packages/cluster-tool/src/config/NodeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ interface NodeDescriptor {
name: string
ports: BindConfigNodeopPorts
producers: readonly string[]
batchOperatorAccount: string | null
underwriterAccount: string | null
batchOperatorLabel: string | null
underwriterLabel: string | null
}

/**
Expand All @@ -86,8 +86,8 @@ export class NodeConfig {
readonly ports: BindConfigNodeopPorts,
readonly producers: readonly string[],
readonly peerEndpoints: readonly string[],
readonly batchOperatorAccount: string | null = null,
readonly underwriterAccount: string | null = null
readonly batchOperatorLabel: string | null = null,
readonly underwriterLabel: string | null = null
) {
this.ini = new NodeConfigIniRenderer(this)
this.logging = new NodeConfigLoggingRenderer(this)
Expand All @@ -102,8 +102,9 @@ export class NodeConfig {
* Plan every node in the cluster from its resolved binding: a bios node, one
* producer node per `bind.nodeop.ports.producers[]` (with the defproducer
* names round-robin-distributed), and one operator node per batch-op /
* underwriter port pair. Peer endpoints are every other node's advertised
* p2p endpoint.
* underwriter port pair (associated by its provisioning LABEL — the chain
* account is generated at provisioning time and resolved from the key
* store). Peer endpoints are every other node's advertised p2p endpoint.
*
* @param cluster - The resolved cluster config.
* @returns The planned nodes, bios first.
Expand All @@ -120,8 +121,8 @@ export class NodeConfig {
name: NodeConfig.BiosName,
ports: nodeopPorts.bios,
producers: [NodeConfig.BiosProducer],
batchOperatorAccount: null,
underwriterAccount: null
batchOperatorLabel: null,
underwriterLabel: null
}
]

Expand All @@ -134,8 +135,8 @@ export class NodeConfig {
producers: producerNames.filter(
(_, i) => producerNodeCount > 0 && i % producerNodeCount === k
),
batchOperatorAccount: null,
underwriterAccount: null
batchOperatorLabel: null,
underwriterLabel: null
})
)

Expand All @@ -147,8 +148,8 @@ export class NodeConfig {
name: nodeName(opIndex - 1),
ports,
producers: [],
batchOperatorAccount: Constants.batchOperatorAccountName(k),
underwriterAccount: null
batchOperatorLabel: Constants.batchOperatorLabel(k),
underwriterLabel: null
})
)
nodeopPorts.underwriters.forEach((ports, k) =>
Expand All @@ -158,8 +159,8 @@ export class NodeConfig {
name: nodeName(opIndex - 1),
ports,
producers: [],
batchOperatorAccount: null,
underwriterAccount: Constants.underwriterAccountName(k)
batchOperatorLabel: null,
underwriterLabel: Constants.underwriterLabel(k)
})
)

Expand All @@ -175,8 +176,8 @@ export class NodeConfig {
descriptors
.filter(other => other.name !== d.name)
.map(other => `${advertise}:${other.ports.p2p}`),
d.batchOperatorAccount,
d.underwriterAccount
d.batchOperatorLabel,
d.underwriterLabel
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,13 @@ export class NodeConfigIniRenderer implements Renderer {
kv("max-clients", extraArgs.maxClients),
kv("connection-cleanup-period", extraArgs.connectionCleanupPeriod),
kv("http-max-response-time-ms", extraArgs.httpMaxResponseTimeMs),
// The operator's `batch-operator-account` / `underwriter-account` is
// NOT rendered here: the chain account is node-owner-generated at
// provisioning time, so it rides the daemon CLI args
// (`OperatorDaemonTool`) resolved from the key store at start.
...(isOperator
? [kv("read-mode", WireClient.FinalityType.irreversible)]
: []),
...(isOperator && node.batchOperatorAccount
? [kv("batch-operator-account", node.batchOperatorAccount)]
: []),
...(isOperator && node.underwriterAccount
? [kv("underwriter-account", node.underwriterAccount)]
: []),
...NodeConfigIniRenderer.HttpInsecureLines,
""
]
Expand Down
18 changes: 9 additions & 9 deletions packages/cluster-tool/src/orchestration/ClusterBuildDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ export namespace ClusterBuildDefaults {
const config = cluster.context.config,
producers = range(config.producerCount).map(index => producerName(index)),
batchOperators = range(config.batchOperatorCount).map(index =>
Constants.batchOperatorAccountName(index)
Constants.batchOperatorLabel(index)
),
underwriters = range(config.underwriterCount).map(index =>
Constants.underwriterAccountName(index)
Constants.underwriterLabel(index)
),
producerNodes = NodeConfig.plan(config).filter(
node => node.role === NodeRole.producer
Expand Down Expand Up @@ -296,8 +296,8 @@ export namespace ClusterBuildDefaults {
"Producers",
"Provision producer operators (account + node-shared identity)",
{},
producers.map((account, index) => ({
account,
producers.map((label, index) => ({
label,
type: OperatorType.PRODUCER,
producerNodeIndex: producerNodeCount > 0 ? index % producerNodeCount : 0
}))
Expand Down Expand Up @@ -696,17 +696,17 @@ export namespace ClusterBuildDefaults {
"Provision the bootstrapped batch operators + underwriters",
{},
[
...batchOperators.map((account, index) => ({
account,
...batchOperators.map((label, index) => ({
label,
type: OperatorType.BATCH,
ethereumHdIndex: index + 1,
isBootstrapped: true,
// Fee-payer funding for the daemon's per-epoch SOL deliveries (ETH
// needs none — anvil prefunds the operator HD accounts).
airdropSolanaLamports: BatchOperatorAirdropLamports
})),
...underwriters.map((account, index) => ({
account,
...underwriters.map((label, index) => ({
label,
type: OperatorType.UNDERWRITER,
ethereumHdIndex: config.batchOperatorCount + index + 1,
isBootstrapped: false
Expand All @@ -724,7 +724,7 @@ export namespace ClusterBuildDefaults {
.filter(node => node.role === NodeRole.operator)
.forEach(node => {
const actor =
node.batchOperatorAccount != null
node.batchOperatorLabel != null
? Actor.BatchOperator
: Actor.Underwriter
ClusterBuildPhase.create<C>(
Expand Down
34 changes: 20 additions & 14 deletions packages/cluster-tool/src/orchestration/outputs/ClusterKeyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,36 +50,42 @@ export class ClusterKeyStore {
return nodeKeys
}

/** Add or replace a provisioned operator account (chainable). */
/** Add or replace a provisioned operator account, keyed by its `label` (chainable). */
setOperator(operator: OperatorAccount): this {
this.operatorMap.set(operator.account, operator)
this.operatorMap.set(operator.label, operator)
return this
}

/** A provisioned operator account, or nothing when absent (see {@link assertOperator}). */
operator(account: string): OperatorAccount {
return this.operatorMap.get(account)
/** A provisioned operator account by `label`, or nothing when absent (see {@link assertOperator}). */
operator(label: string): OperatorAccount {
return this.operatorMap.get(label)
}

/**
* A provisioned operator account — throws when the account hasn't been
* provisioned (its materialize step hasn't run).
* A provisioned operator account by `label` — throws when the operator hasn't
* been provisioned (its materialize step hasn't run).
*
* @param account - The operator's WIRE account name.
* @returns The operator's account + keys.
* @param label - The operator's deterministic provisioning label.
* @returns The operator's label + account + keys.
*/
assertOperator(account: string): OperatorAccount {
const operator = this.operatorMap.get(account)
assertOperator(label: string): OperatorAccount {
const operator = this.operatorMap.get(label)
Assert.ok(
operator != null,
`ClusterKeyStore: operator "${account}" has not been provisioned (no materialize step ran for it)`
`ClusterKeyStore: operator "${label}" has not been provisioned (no materialize step ran for it)`
)
return operator
}

/** Every provisioned operator of `type`, in provisioning order. */
/**
* Every provisioned operator of `type`, sorted by `label`. Sorting (not
* insertion order) keeps the listing deterministic — provisioning phases run
* in parallel, so completion order varies run to run.
*/
operatorsByType(type: OperatorType): OperatorAccount[] {
return this.operators.filter(operator => operator.type === type)
return this.operators
.filter(operator => operator.type === type)
.sort((a, b) => a.label.localeCompare(b.label))
}
}

Expand Down
23 changes: 17 additions & 6 deletions packages/cluster-tool/src/orchestration/outputs/OperatorAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ import type {
} from "../../types/KeyPair.js"

/**
* One provisioned operator's identity — its WIRE account + type-appropriate key
* set. `type` is the proto {@link OperatorType} (the SAME classification the
* depot's `sysio.opreg` carries) and drives which keys the account holds and
* which on-chain steps provision it. Every provisioned account (bootstrap or
* flow) accumulates into the ONE `ClusterKeyStore` (`ctx.keyStore`), the single
* place keys are accessed from.
* One provisioned operator's identity — its deterministic provisioning `label`,
* its WIRE `account`, and its type-appropriate key set. `type` is the proto
* {@link OperatorType} (the SAME classification the depot's `sysio.opreg`
* carries) and drives which keys the account holds and which on-chain steps
* provision it. Every provisioned account (bootstrap or flow) accumulates into
* the ONE `ClusterKeyStore` (`ctx.keyStore`), the single place keys are
* accessed from, keyed by `label`.
*
* - `label` — the deterministic handle the harness keys the operator by
* (`batchop.a`, `uwrit.a`, a flow's `depositor`). For batch operators /
* underwriters it is ALSO the `sysio.roa::newuser` sponsor nonce their chain
* account is created under; for producers it equals `account`.
* - `account` — the operator's WIRE account name ON CHAIN. Producers keep the
* deterministic `defproducer*` names; batch operators / underwriters get a
* node-owner-sponsored generated name (`wireno.<generated>`) recorded by the
* sponsored-creation step — until that step runs, `account` provisionally
* holds `label`.
* - `wire` (K1) — every operator: the WIRE account's controller key. Batch
* operators / underwriters get a UNIQUE generated K1 (imported into the kiod
* wallet so `account@active` signs); a producer carries its NODE's key —
Expand All @@ -28,6 +38,7 @@ import type {
* absent for types that don't use them (strictNullChecks-off: no `| null` ceremony).
*/
export interface OperatorAccount {
readonly label: string
readonly account: string
readonly type: OperatorType
readonly wire: WireKeyPair
Expand Down
Loading