Skip to content
Draft
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
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,13 @@ secret-id pattern), plus `--external-outpost-config <file>` (bootstrap the depot
against already-deployed REMOTE ETH+SOL outposts — no local anvil/validator),
`--bind-config <file>` (a complete `BindConfig` used verbatim, or a partial
override merged over the resolved defaults; a remote anvil/solana address
requires `--external-outpost-config`), and `--enable-mock-reserves` (default
requires `--external-outpost-config`), `--enable-mock-reserves` (default
off — seed the 8 mock (chain, token) PRIMARY reserves at bootstrap; a real /
external depot leaves these unseeded); `package` writes one `<node>.<ext>` per
external depot leaves these unseeded), and `--solana-epoch-warp` (default off —
warp the solana-test-validator past Solana epoch 3 for the liqsol
staking-yield pipeline gate; the warp puts the Solana clock ~80 min ahead of
real time, so only `flow-yield-distribution` opts in via its scenario
`defaults`); `package` writes one `<node>.<ext>` per
node under `<cluster>/packages/` (a hand-off artifact for a multihost environment
with distinct compute + storage — S3/EC2, GCS, or any other, loosely coupled).
`create-external-config` clones a CREATED, STOPPED local cluster into a deployable
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ command comes first).
| `--terminate-window-ms` | | — | termination evaluation window in ms |
| `--bind-all` | | `false` | bind every daemon to `0.0.0.0` instead of loopback |
| `--enable-mock-reserves` | | `false` | seed the 8 mock (chain, token) PRIMARY reserves at bootstrap |
| `--solana-epoch-warp` | | `false` | warp the solana-test-validator past Solana epoch 3 (liqsol staking-yield pipeline gate; puts the Solana clock ~80 min ahead of real time) |
| `--bind-*` | | auto | per-daemon address/port pins (`--bind-anvil-port`, `--bind-nodeop-ports-bios-http`, …); unpinned ports are auto-assigned collision-free |
| `--bind-config` | | — | a `BindConfig` JSON file: a complete config is used verbatim (no port probing — remote addresses stay put), a partial one is merged over the resolved defaults (CLI `--bind-*` > file > defaults) |
| `--external-outpost-config` | | — | an `ExternalOutpostConfig` JSON file: bootstrap the depot against already-deployed REMOTE ETH+SOL outposts (skips the local anvil/validator + outpost deploys) |
Expand Down
40 changes: 40 additions & 0 deletions packages/cluster-tool-shared/src/config/ClusterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,35 @@ export type ClusterExecutablePaths = z.infer<
typeof ClusterExecutablePathsSchema
>

/**
* Ethereum-chain-specific cluster configuration. Empty today — the dedicated
* nesting point for current and future Ethereum-only options, mirroring
* {@link ClusterConfigSolanaSchema}.
*/
export const ClusterConfigEthereumSchema = z.object({})
/** Ethereum-chain-specific cluster configuration — the shape of {@link ClusterConfigEthereumSchema}. */
export type ClusterConfigEthereum = z.infer<typeof ClusterConfigEthereumSchema>

/**
* Solana-chain-specific cluster configuration — the dedicated nesting point
* for current and future Solana-only options.
*/
export const ClusterConfigSolanaSchema = z.object({
/**
* Warp the solana-test-validator past Solana epoch 3 at launch, so a flow
* driving the liqsol yield pipeline can run — `dev_seed_staker_yield` is
* gated on `Clock.epoch >= 3` (`MIN_SEED_EPOCH`: the credited epoch is
* `Clock.epoch - 2` and must be ≥ the launch epoch). Off for every flow
* except `flow-yield-distribution`, which opts in via its scenario
* `defaults`: the warp puts the Solana chain clock ~80 minutes ahead of real
* time — a non-production clock condition no other flow needs, so none may
* silently inherit it.
*/
epochWarp: z.boolean().default(false)
})
/** Solana-chain-specific cluster configuration — the shape of {@link ClusterConfigSolanaSchema}. */
export type ClusterConfigSolana = z.infer<typeof ClusterConfigSolanaSchema>

/**
* THE canonical cluster configuration — the plain JSON shape persisted to
* `cluster-config.json` (`ClusterFiles.ConfigFilename`) and flowed through
Expand Down Expand Up @@ -159,6 +188,17 @@ export const ClusterConfigSchema = z.object({
ethereumPath: z.string(),
/** wire-solana repo root. */
solanaPath: z.string(),
/**
* Ethereum-chain-specific configuration (empty today — the nesting point for
* future Ethereum-only options). Schema-defaulted so pre-existing configs
* stay loadable.
*/
ethereum: ClusterConfigEthereumSchema.default({}),
/**
* Solana-chain-specific configuration (`epochWarp`, …). Schema-defaulted so
* pre-existing configs stay loadable.
*/
solana: ClusterConfigSolanaSchema.default({ epochWarp: false }),
/** Resolved network binding for every daemon. */
bind: BindConfigSchema,
/** Resolved binary locations. */
Expand Down
22 changes: 22 additions & 0 deletions packages/cluster-tool-shared/tests/config/ClusterConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ describe("ClusterConfig shape", () => {
terminateWindowMs: null,
ethereumPath: "/eth",
solanaPath: "/sol",
ethereum: {},
solana: { epochWarp: false },
bind: {
kiod: { address: "127.0.0.1", port: 8900 },
nodeop: {
Expand Down Expand Up @@ -126,6 +128,26 @@ describe("ClusterConfig shape", () => {
expect(rehydrated.enableMockReserves).toBe(false)
})

it("defaults the per-chain sections (ethereum/solana) for a legacy config", () => {
const parsed = JSON.parse(ClusterConfigSchemaCodec.serialize(config))
delete parsed.ethereum
delete parsed.solana
const rehydrated = ClusterConfigSchemaCodec.deserialize(
JSON.stringify(parsed)
)
expect(rehydrated.ethereum).toEqual({})
expect(rehydrated.solana).toEqual({ epochWarp: false })
})

it("fills epochWarp inside a present-but-partial solana section", () => {
const parsed = JSON.parse(ClusterConfigSchemaCodec.serialize(config))
parsed.solana = {}
const rehydrated = ClusterConfigSchemaCodec.deserialize(
JSON.stringify(parsed)
)
expect(rehydrated.solana.epochWarp).toBe(false)
})

it("defaults the epoch-group + termination overrides to null for a legacy config", () => {
const parsed = JSON.parse(ClusterConfigSchemaCodec.serialize(config))
delete parsed.operatorsPerEpoch
Expand Down
1 change: 1 addition & 0 deletions packages/cluster-tool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ the path flags.
| `--terminate-max-consecutive-misses` / `--terminate-max-percent-misses24h` / `--terminate-window-ms` | | — | termination tuning |
| `--bind-all` | | `false` | bind every daemon to `0.0.0.0` instead of loopback |
| `--enable-mock-reserves` | | `false` | seed the 8 mock (chain, token) PRIMARY reserves at bootstrap |
| `--solana-epoch-warp` | | `false` | warp the solana-test-validator past Solana epoch 3 (liqsol staking-yield pipeline gate; puts the Solana clock ~80 min ahead of real time) |
| `--bind-*` | | auto | per-daemon address/port pins (`--bind-anvil-port`, …); unpinned ports are auto-assigned collision-free |
| `--bind-config <file>` | | — | a `BindConfig` JSON: complete → verbatim (no probing), partial → merged over resolved defaults (CLI > file > defaults) |
| `--external-outpost-config <file>` | | — | bootstrap the depot against already-deployed REMOTE ETH+SOL outposts |
Expand Down
7 changes: 7 additions & 0 deletions packages/cluster-tool/src/cli/ClusterBuildOptionsArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,13 @@ export function buildOptionShape(
OptionLeafType.number,
"cooldown epochs after the measured window"
),
// ── per-chain options (nested → `--solana-epoch-warp` style flags) ──
solana: {
epochWarp: leaf(
false,
"warp the solana-test-validator past Solana epoch 3 (liqsol staking-yield pipeline gate)"
)
},
// ── termination tuning ──
terminateMaxConsecutiveMisses: optionalLeaf(
OptionLeafType.number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export interface SolanaValidatorProgram {
name: string
programId: string
soFile: string
/**
* When set, the program is added to genesis as an UPGRADEABLE program
* (`--upgradeable-program … <upgradeAuthority>`) so a `ProgramData` account
* exists with this pubkey as its upgrade authority — required by the
* integrated liqsol `initialize_global_config`. When omitted, the program is
* loaded non-upgradeable (`--bpf-program`).
*/
upgradeAuthority?: string
}

/** Caller options for the solana-test-validator. */
Expand Down Expand Up @@ -70,6 +78,15 @@ export interface SolanaValidatorOptions {
binary?: string
/** Programs to deploy on startup (`--bpf-program`). */
programs?: SolanaValidatorProgram[]
/**
* Warp the validator's genesis clock past Solana epoch 3
* (`--slots-per-epoch` {@link SolanaValidatorProcess.EpochWarpSlotsPerEpoch}
* + `--warp-slot` {@link SolanaValidatorProcess.EpochWarpSlot}). Off by
* default — the warp puts the Solana chain clock ~80 minutes ahead of real
* time, so only a cluster that needs the liqsol staking-yield epoch gate
* opts in; see `ClusterConfig.solana.epochWarp` (`ClusterConfigSolana`).
*/
epochWarp?: boolean
/** Additional CLI flags. */
extraArgs?: string[]
}
Expand Down Expand Up @@ -115,6 +132,7 @@ export class SolanaValidatorProcess extends ManagedProcess {
SolanaValidatorProcess.DefaultLimitLedgerSizeShreds,
binary,
programs: options.programs ?? [],
epochWarp: options.epochWarp ?? false,
extraArgs: options.extraArgs ?? []
}
return new SolanaValidatorProcess(manager, config)
Expand Down Expand Up @@ -152,11 +170,24 @@ export class SolanaValidatorProcess extends ManagedProcess {
String(this.config.limitLedgerSizeShreds),
...(verbose ? [] : ["--quiet"]),
...(this.config.ledgerPath ? ["--ledger", this.config.ledgerPath] : []),
...this.config.programs.flatMap(program => [
"--bpf-program",
program.programId,
program.soFile
]),
...this.config.programs.flatMap(program =>
program.upgradeAuthority
? [
"--upgradeable-program",
program.programId,
program.soFile,
program.upgradeAuthority
]
: ["--bpf-program", program.programId, program.soFile]
),
...(this.config.epochWarp
? [
"--slots-per-epoch",
String(SolanaValidatorProcess.EpochWarpSlotsPerEpoch),
"--warp-slot",
String(SolanaValidatorProcess.EpochWarpSlot)
]
: []),
...this.config.extraArgs
]
}
Expand Down Expand Up @@ -279,6 +310,26 @@ export namespace SolanaValidatorProcess {
* traffic reaches it; lowering it re-introduces mid-run history loss.
*/
export const DefaultLimitLedgerSizeShreds = 200_000_000
/**
* `--slots-per-epoch` used when {@link SolanaValidatorOptions.epochWarp} is
* set — epochs stretched so a whole flow (the `dev_seed_staker_yield`
* seeding plus the `flush_staking_yield` crank) lands inside ONE Solana
* epoch: the emitted reward's `external_epoch_ref` derives from the Solana
* epoch, so a mid-flow rollover would move the ref out from under the
* depot's dedupe check.
*/
export const EpochWarpSlotsPerEpoch = 4_096
/**
* `--warp-slot` target used when {@link SolanaValidatorOptions.epochWarp} is
* set — just past the epoch-3 boundary (3 × 4096 = 12 288), satisfying the
* `dev_seed_staker_yield` `MIN_SEED_EPOCH = 3` gate (the credited epoch is
* `Clock.epoch - 2` and must be ≥ the launch epoch). It MUST land inside
* epoch 3 exactly: a single-node test-validator can build epoch 3's leader
* schedule from genesis stakes and keep producing, but warping straight
* into epoch 4+ leaves it unable to derive the schedule and it never
* produces a block.
*/
export const EpochWarpSlot = 12_300
/** Subpath (under the cluster data dir) for the validator ledger. */
export const LedgerSubpath = "solana-ledger"
}
13 changes: 13 additions & 0 deletions packages/cluster-tool/src/config/ClusterBuildOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ export interface LoggingOptions {
fileFormat?: ClusterConfigLoggingFileFormat
}

/** Caller-facing Solana-chain options (the `Options` half of `ClusterConfigSolana`). */
export interface SolanaOptions {
/**
* Warp the solana-test-validator past Solana epoch 3 at launch
* (`--solana-epoch-warp`). Default `false` at every layer — only
* `flow-yield-distribution` opts in via its scenario `defaults`; the warp
* puts the Solana chain clock ~80 minutes ahead of real time.
*/
epochWarp?: boolean
}

/**
* Everything a caller may set when standing up a cluster (CLI or flow). All
* fields optional; `ClusterConfigProvider.resolve` fills the rest. `bind` / `report` /
Expand Down Expand Up @@ -41,6 +52,8 @@ export interface ClusterBuildOptions {
epochRetentionEnvelopeLogCount?: number
warmupEpochs?: number
cooldownEpochs?: number
// per-chain options (`ClusterConfig.ethereum` / `ClusterConfig.solana`)
solana?: SolanaOptions
// network binding
bindAll?: boolean
bind?: BindOptions
Expand Down
10 changes: 10 additions & 0 deletions packages/cluster-tool/src/config/ClusterConfigProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export namespace ClusterConfigProvider {
export const DefaultBatchOperatorCount = 3
export const DefaultUnderwriterCount = 1
export const DefaultEpochDurationSec = 90
/**
* Default for `ClusterConfig.solana.epochWarp` — OFF. Only
* `flow-yield-distribution` opts in (via its scenario `defaults`); the warp
* puts the Solana chain clock ~80 minutes ahead of real time — a
* non-production clock condition no other flow needs — so it is never the
* cluster-wide default.
*/
export const DefaultSolanaEpochWarp = false

/**
* Resolve defaults → validate → return a ready config (the only forward
Expand Down Expand Up @@ -113,6 +121,8 @@ export namespace ClusterConfigProvider {
terminateWindowMs: options.terminateWindowMs ?? null,
ethereumPath: assertOption(options.ethereumPath, "ethereumPath"),
solanaPath: assertOption(options.solanaPath, "solanaPath"),
ethereum: {},
solana: { epochWarp: options.solana?.epochWarp ?? DefaultSolanaEpochWarp },
bind,
executables,
report,
Expand Down
Loading