@mysten-incubation/devstack 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/dashboard-ui/assets/{grpc-CpkDu4SA.js → grpc-s7Ztk9wv.js} +1 -1
- package/dashboard-ui/assets/{index-jLPRmjst.js → index-DrOd0m4F.js} +2 -2
- package/dashboard-ui/index.html +1 -1
- package/dist/contracts/routable.d.mts +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/orchestrators/boot.mjs +2 -1
- package/dist/orchestrators/boot.mjs.map +1 -1
- package/dist/orchestrators/router/file-provider.mjs +11 -8
- package/dist/orchestrators/router/file-provider.mjs.map +1 -1
- package/dist/orchestrators/router/service.mjs +2 -2
- package/dist/orchestrators/router/service.mjs.map +1 -1
- package/dist/plugins/deepbook/index.d.mts +36 -6
- package/dist/plugins/deepbook/index.mjs +52 -16
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/seal/codegen.d.mts +5 -0
- package/dist/plugins/seal/codegen.mjs +53 -10
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/index.d.mts +64 -19
- package/dist/plugins/seal/index.mjs +26 -36
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/seal/mode/fork-known.d.mts +0 -2
- package/dist/plugins/seal/mode/fork-known.mjs +11 -22
- package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
- package/dist/plugins/seal/mode/live.d.mts +36 -13
- package/dist/plugins/seal/mode/live.mjs +104 -43
- package/dist/plugins/seal/mode/live.mjs.map +1 -1
- package/dist/plugins/seal/mode/local-keygen.mjs +2 -1
- package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
- package/dist/plugins/seal/registry-publish.d.mts +15 -3
- package/dist/plugins/seal/registry-publish.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +10 -10
- package/dist/plugins/walrus/client-services.d.mts +1 -0
- package/dist/plugins/walrus/client-services.mjs +115 -0
- package/dist/plugins/walrus/client-services.mjs.map +1 -0
- package/dist/plugins/walrus/deploy.mjs +11 -5
- package/dist/plugins/walrus/deploy.mjs.map +1 -1
- package/dist/plugins/walrus/errors.d.mts +1 -1
- package/dist/plugins/walrus/errors.mjs.map +1 -1
- package/dist/plugins/walrus/index.d.mts +138 -9
- package/dist/plugins/walrus/index.mjs +72 -20
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/plugins/walrus/mode/known-deploy.d.mts +5 -1
- package/dist/plugins/walrus/mode/known-deploy.mjs +14 -11
- package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
- package/dist/plugins/walrus/mode/local-cluster.d.mts +14 -1
- package/dist/plugins/walrus/mode/local-cluster.mjs +54 -12
- package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
- package/dist/plugins/walrus/routable.mjs +12 -15
- package/dist/plugins/walrus/routable.mjs.map +1 -1
- package/dist/plugins/walrus/snapshot.mjs +3 -2
- package/dist/plugins/walrus/snapshot.mjs.map +1 -1
- package/dist/plugins/walrus/storage-nodes.mjs +31 -33
- package/dist/plugins/walrus/storage-nodes.mjs.map +1 -1
- package/dist/substrate/runtime/config-validation.mjs +1 -1
- package/images/walrus/Dockerfile +17 -4
- package/images/walrus/deploy-walrus.sh +7 -4
- package/images/walrus/run-walrus-client-service.sh +103 -0
- package/package.json +2 -2
- package/dist/plugins/seal/service.mjs +0 -17
- package/dist/plugins/seal/service.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-nodes.mjs","names":[],"sources":["../../../src/plugins/walrus/storage-nodes.ts"],"sourcesContent":["// Walrus storage-node committee lifecycle.\n//\n// Distilled-doc reference (06-walrus.md §\"Capabilities CONSUMED\" +\n// §\"Lifecycle phase 4\"): N storage-node containers, pinned IPs on a\n// per-stack /24 docker network. Each node carries one Traefik route\n// via the `Routable` capability; the router binds host port 9185 once\n// globally and dispatches by `Host:` header.\n//\n// Substrate-side responsibilities:\n// - `ContainerRuntime.ensureContainer` per node — content-addressed\n// image, label tuple, recreate policy.\n// - Parallel `Effect.all({concurrency: 'unbounded'})` over [0..N) —\n// concurrent boot.\n// - Per-node stop finalizer fires in a *forked parallel scope* so\n// teardown collapses to ~max(grace) instead of summing graces\n// (distilled-doc invariant 21 — \"StopFinalizerScope MUST be a\n// forked parallel scope\").\n//\n// What we DON'T own here:\n// - Plugin row narration.\n// - The faucet-strategy registration — `faucet-strategy.ts`.\n// - The deploy one-shot — `deploy.ts`.\n\nimport { Duration, Effect, Scope } from 'effect';\n\nimport type {\n\tContainerHandle,\n\tContainerRuntime,\n\tImageRef,\n} from '../../contracts/container-runtime.ts';\nimport {\n\tProbeTimeoutError,\n\texitCodeProbeResult,\n\twaitForProbe,\n} from '../../substrate/runtime/probes.ts';\nimport { setCurrentPluginPhase } from '../../substrate/runtime/current-plugin.ts';\nimport { ensureManagedContainer } from '../../substrate/runtime/managed-container.ts';\nimport { HOST_GATEWAY_EXTRA_HOSTS } from '../../substrate/runtime/host-gateway.ts';\nimport { walrusDeployMountPaths } from './deploy-paths.ts';\nimport { walrusPluginError, type WalrusPluginError } from './errors.ts';\n\n/** Default per-node grace window. Storage nodes maintain RocksDB-\n * backed state; need >10s to flush and checkpoint on `docker stop`.\n * Distilled-doc invariant 22. */\nexport const DEFAULT_NODE_STOP_GRACE_SECONDS = 20;\n\n/** Default per-node ready-probe timeout. */\nexport const DEFAULT_NODE_READY_TIMEOUT_MS = 60_000;\n\n/** Distilled-doc invariant 9: the on-chain `public_port` value\n * matches the global router entrypoint port. One canonical\n * constant — opportunity #14 in the distilled doc was to deduplicate\n * this across three files. */\nexport const WALRUS_ROUTER_PORT = 9185;\n\n/** Default container API port — each storage node binds this inside\n * the container. Same value as the router port (the storage node's\n * in-container bind matches the on-chain public_port). */\nexport const DEFAULT_CONTAINER_API_PORT = WALRUS_ROUTER_PORT;\n\n/** Per-node descriptor — what the plugin resolved value surfaces\n * for downstream consumers (the `@mysten/walrus` SDK reads this).\n *\n * Note: no `publicKey` field. The per-node BLS12-381 keypair lives\n * inside the per-node keystore the deploy one-shot emitted; the SDK\n * consumer reads the public key off `packageConfig` rather than this\n * routing-handle descriptor. */\nexport interface WalrusStorageNode {\n\treadonly nodeIndex: number;\n\treadonly nodeId: string;\n\treadonly publicHostname: string;\n\treadonly rpcUrl: string;\n}\n\n/** Storage-node startup config — the plugin's local-cluster mode\n * passes this in once per cluster boot. */\nexport interface StorageNodesSpec {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\t/** One image ref PER node (`images[i]` for node `i`; length MUST equal\n\t * `nodeCount`). Each node runs under its own tag so its committed writable\n\t * layer (RocksDB slivers) promotes to a DISTINCT image on snapshot restore;\n\t * a single shared tag collapses N committed layers -> 1 on the restore\n\t * image-promote, orphaning N-1 nodes' blobs. */\n\treadonly images: ReadonlyArray<ImageRef>;\n\treadonly nodeCount: number;\n\t/** Pre-allocated /24 prefix from `subnetForStack`. Example:\n\t * `'10.42.7'` (the third octet is hashed per stack). */\n\treadonly subnetPrefix: string;\n\treadonly containerApiPort: number;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly deployHostMountPath: string;\n\t/** On-disk per-stack root from `StackPathsService.stackRoot`. The\n\t * walrus bind-mount is computed by `walrusDeployMountPaths` as\n\t * `<stackRoot> -> <mountTarget>`; the deploy output dir MUST be a\n\t * descendant. Threaded explicitly to remove the previous\n\t * `dirname(dirname(dirname(...)))` walk-up footgun. */\n\treadonly stackRoot: string;\n\t/** Fingerprint of the deploy outputs mounted into each node. */\n\treadonly deployConfigHash: string;\n\treadonly stopGraceSeconds?: number;\n\treadonly readyTimeoutMs?: number;\n}\n\n/** Result of starting the committee — descriptors used by the\n * plugin's resolved value. */\nexport interface StorageNodesAcquired {\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n}\n\n/** Distilled-doc invariant 1: pinned IPs start at `<prefix>.10`.\n * Centralized so the deploy one-shot's `WALRUS_LISTENING_IPS` env\n * var and the per-node `--ip` pin compute from one source. */\nexport const WALRUS_NODE_IP_BASE = 10;\n\n/** Compute the public hostname for one storage node — the\n * `routerHostname(identity, 'walrus-node-' + i)` shape\n * (06-walrus.md §\"Routes registered\"). Main stack omits the stack\n * segment; non-main inserts it after the service label. */\nexport const computePublicHostname = (app: string, stack: string, nodeIndex: number): string => {\n\tconst base = `walrus-node-${nodeIndex}.${app}.localhost`;\n\treturn stack === 'main' ? base : `walrus-node-${nodeIndex}.${stack}.${app}.localhost`;\n};\n\nexport const storageNodeConfigHash = (parts: {\n\treadonly deployConfigHash: string;\n\treadonly nodeIndex: number;\n\treadonly deploySourceHostPath: string;\n\treadonly deployMountTarget: string;\n\treadonly containerApiPort: number;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n}): string =>\n\t[\n\t\tparts.deployConfigHash,\n\t\t`node=${parts.nodeIndex}`,\n\t\t`mount=${parts.deploySourceHostPath}->${parts.deployMountTarget}`,\n\t\t`api=${parts.containerApiPort}`,\n\t\t`net=${parts.walrusNetworkName},${parts.suiNetworkName}`,\n\t].join('|');\n\nexport const buildWalrusNetworkName = (app: string, stack: string, walrusName: string): string =>\n\t`devstack-${app}-${stack}-walrus-${walrusName}-net`;\n\n/** Per-node TCP ready-probe interval. */\nconst NODE_READY_PROBE_INTERVAL_MS = 500;\n\n/** Walrus storage-node REST health endpoint (TLS disabled — plain HTTP\n * behind the router; see images/walrus/deploy-walrus.sh which rebinds\n * `rest_api_address` on 0.0.0.0 and sets `disable_tls: true`). The node\n * serves `GET /v1/health` once its API listener binds; the JSON body's\n * `nodeStatus` field reports the committee-sync state. */\nconst NODE_HEALTH_PATH = '/v1/health';\n\n/** Recovery / not-yet-serving markers in a `/v1/health` body. A node that\n * just recreated (fresh `docker run` from a committed/restored image — the\n * snapshot capture/restore resume) comes back up and reports a recovery\n * status while it re-syncs its committee/epoch; writing a blob to it before\n * it catches up fails the write quorum with \"Too many failures while\n * writing blob to nodes\". The boot ready-gate blocks until NONE of these\n * markers is present so the node is genuinely serving. */\nconst NODE_RECOVERY_MARKERS: ReadonlyArray<RegExp> = [\n\t/\"nodeStatus\"\\s*:\\s*\"Recovery/i,\n\t/\"nodeStatus\"\\s*:\\s*\"Standby\"/i,\n\t/\"nodeStatus\"\\s*:\\s*\"Starting\"/i,\n\t/\"isInRecovery\"\\s*:\\s*true/i,\n\t/\"inRecovery\"\\s*:\\s*true/i,\n];\n\n/** Classify a `/v1/health` 2xx body as write-ready. Biased toward NOT\n * false-negativing the boot (a too-strict classifier would time the boot\n * out on a field-name change upstream): a node whose health endpoint\n * returns 2xx is treated as serving UNLESS the body explicitly reports a\n * recovery / standby / starting status. The explicit `Active` serving\n * status is the fast-path. This gates EVERY acquire path (up / restart /\n * restore-converge / capture-resume) on write-readiness, not just\n * process-up — the snapshot survival-matrix fix. */\nconst isWriteReadyHealthBody = (body: string): boolean => {\n\tconst trimmed = body.trim();\n\tif (/\"nodeStatus\"\\s*:\\s*\"Active\"/i.test(trimmed)) return true;\n\t// Empty body from a 2xx with no payload: the listener answered but gave\n\t// no status — treat as not-yet-ready so the gate keeps polling briefly.\n\tif (trimmed.length === 0) return false;\n\treturn !NODE_RECOVERY_MARKERS.some((marker) => marker.test(trimmed));\n};\n\n/** Start the N-node committee.\n *\n * Wiring (this body is the load-bearing path):\n *\n * 1. Defense-in-depth nodeCount guard.\n * 2. Fork a parallel-strategy child scope off the caller's scope so\n * every node's `docker stop` finalizer fires concurrently on\n * outer teardown — collapses shutdown wall-time from `N×grace` to\n * `~max(grace)` (distilled-doc invariant 21).\n * 3. For each `i ∈ [0..N)`:\n * a. `ensureContainer` on the wrapper image with pinned IP,\n * label tuple, primary network attach (walrus net), recreate\n * on config-change.\n * b. TCP ready probe against `<publicHostname>:<routerPort>`\n * with bounded retries until the outer `readyTimeoutMs`.\n * 4. Run [3] under `Effect.all({concurrency: 'unbounded'})` — boot\n * parallelism collapses to ~max(perNode).\n *\n * WHAT THE SUBSTRATE STILL OWES:\n * - Secondary network attach (`Docker.networkConnect(suiNet)`).\n * `EnsureContainerSpec.networkAttach` accepts an array, so passing\n * `[walrusNet, suiNet]` would dual-home in one shot, but the\n * runtime adapter currently treats the first entry as the primary\n * and post-attaches the rest; the per-node pinned IP only attaches\n * to the primary. The dual-home is documented inline below and\n * relies on that adapter behavior. Storage nodes do not request\n * faucet funds during boot; the secondary network is retained for\n * future Sui-side in-network calls.\n * - Per-node `networkAlias` (`walrus-node-<i>.localhost`). The\n * contract doesn't expose alias plumbing yet; peer containers dial by\n * container name, which docker DNS publishes. The architecture\n * revision is tracked in `index.ts`.\n * - Stop-grace duration propagation. The contract's `stop` takes a\n * Duration but `ensureContainer` doesn't yet accept a default\n * grace; the runtime adapter defaults to 10s. RocksDB needs 20s\n * (distilled-doc invariant 22) — see opportunity below.\n */\nexport const startStorageNodes = (\n\truntime: ContainerRuntime,\n\tspec: StorageNodesSpec,\n): Effect.Effect<StorageNodesAcquired, WalrusPluginError, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tif (spec.nodeCount < 1) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalrusPluginError(\n\t\t\t\t\t'storage-node',\n\t\t\t\t\t`storage-nodes: nodeCount must be >= 1 (got ${spec.nodeCount})`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tif (spec.images.length !== spec.nodeCount) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalrusPluginError(\n\t\t\t\t\t'storage-node',\n\t\t\t\t\t`storage-nodes: images length (${spec.images.length}) must equal nodeCount (${spec.nodeCount}) — one distinct per-node image tag is required for restore-safe commits`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Fork a parallel stop-scope. Per-node `ensureContainer` finalizers\n\t\t// fire here on outer teardown; closing the parallel scope races\n\t\t// the per-container `docker stop`s instead of summing them.\n\t\t//\n\t\t// Cascade semantics (verified against effect-v4 `Scope.ts`\n\t\t// `fork` docs — \"Closing the parent closes the child with the\n\t\t// same exit value\"): when ANY `bootOne` fails (e.g. a ready-\n\t\t// probe times out on node i), `Effect.all` interrupts the rest;\n\t\t// the failure propagates up through the encompassing\n\t\t// `Effect.scoped` boundary which closes `outerScope`; that\n\t\t// cascade-closes `nodeStopScope`, running EVERY already-acquired\n\t\t// per-node `docker stop` finalizer in parallel. The probe is\n\t\t// intentionally OUTSIDE `Scope.provide(nodeStopScope)` — its\n\t\t// failures do NOT need their own finalizer attached to the\n\t\t// stop scope; the just-started container's finalizer is already\n\t\t// there from the `ensureNode` acquire that preceded the probe.\n\t\tconst outerScope = yield* Effect.scope;\n\t\tconst nodeStopScope = yield* Scope.fork(outerScope, 'parallel');\n\n\t\tconst stopGraceSeconds = spec.stopGraceSeconds ?? DEFAULT_NODE_STOP_GRACE_SECONDS;\n\t\tconst readyTimeout = Duration.millis(spec.readyTimeoutMs ?? DEFAULT_NODE_READY_TIMEOUT_MS);\n\t\tconst deployMount = walrusDeployMountPaths({\n\t\t\tstackRoot: spec.stackRoot,\n\t\t\tdeployOutputDirHostPath: spec.deployHostMountPath,\n\t\t\tmountTarget: '/opt/walrus/runtime',\n\t\t});\n\n\t\tconst indices = Array.from({ length: spec.nodeCount }, (_, i) => i);\n\n\t\tconst bootOne = (i: number): Effect.Effect<WalrusStorageNode, WalrusPluginError, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst containerIp = `${spec.subnetPrefix}.${WALRUS_NODE_IP_BASE + i}`;\n\t\t\t\tconst containerName = `devstack-${spec.app}-${spec.stack}-walrus-${spec.walrusName}-node-${i}`;\n\t\t\t\tconst nodeHostname = `dryrun-node-${i}`;\n\t\t\t\tconst publicHostname = computePublicHostname(spec.app, spec.stack, i);\n\n\t\t\t\tyield* setCurrentPluginPhase(`creating storage-node-${i} container ${containerName}`);\n\t\t\t\t// Acquire under the forked parallel stop scope so the\n\t\t\t\t// finalizer (docker stop) joins the per-node fan-out race.\n\t\t\t\tconst ensureNode: Effect.Effect<ContainerHandle, WalrusPluginError, Scope.Scope> =\n\t\t\t\t\tensureManagedContainer<WalrusPluginError>({\n\t\t\t\t\t\truntime,\n\t\t\t\t\t\tidentity: { app: spec.app, stack: spec.stack },\n\t\t\t\t\t\tplugin: 'walrus',\n\t\t\t\t\t\trole: `storage-node-${i}`,\n\t\t\t\t\t\tspec: {\n\t\t\t\t\t\t\tname: containerName,\n\t\t\t\t\t\t\t// Node `i`'s own image tag — distinct per node so its\n\t\t\t\t\t\t\t// committed RocksDB layer survives snapshot restore.\n\t\t\t\t\t\t\timage: spec.images[i]!,\n\t\t\t\t\t\t\trecreate: 'on-config-change',\n\t\t\t\t\t\t\tconfigHash: storageNodeConfigHash({\n\t\t\t\t\t\t\t\tdeployConfigHash: spec.deployConfigHash,\n\t\t\t\t\t\t\t\tnodeIndex: i,\n\t\t\t\t\t\t\t\tdeploySourceHostPath: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\tdeployMountTarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\tcontainerApiPort: spec.containerApiPort,\n\t\t\t\t\t\t\t\twalrusNetworkName: spec.walrusNetworkName,\n\t\t\t\t\t\t\t\tsuiNetworkName: spec.suiNetworkName,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tenv: {\n\t\t\t\t\t\t\t\tHOSTNAME: nodeHostname,\n\t\t\t\t\t\t\t\tDEPLOY_OUTPUT_DIR: deployMount.outputDirInContainer,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tstopGraceSeconds,\n\t\t\t\t\t\t\t// Primary network owns the pinned `--ip`; the\n\t\t\t\t\t\t\t// secondary attach gives docker DNS for the sui\n\t\t\t\t\t\t\t// network. The adapter treats [0] as primary.\n\t\t\t\t\t\t\tnetworkAttach: [spec.walrusNetworkName, spec.suiNetworkName],\n\t\t\t\t\t\t\t// Storage nodes dial sui's host-bound RPC + faucet via\n\t\t\t\t\t\t\t// `host.docker.internal`. See\n\t\t\t\t\t\t\t// substrate/runtime/host-gateway.ts for the platform\n\t\t\t\t\t\t\t// rationale (Docker Desktop auto-wires; native Linux\n\t\t\t\t\t\t\t// Docker requires the explicit `--add-host`).\n\t\t\t\t\t\t\textraHosts: HOST_GATEWAY_EXTRA_HOSTS,\n\t\t\t\t\t\t\tmounts: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t// Mount the stack root and point the entrypoint at\n\t\t\t\t\t\t\t\t\t// the deploy child. Docker Desktop can reject\n\t\t\t\t\t\t\t\t\t// freshly-created nested bind sources immediately\n\t\t\t\t\t\t\t\t\t// after a wiped stack recreates them.\n\t\t\t\t\t\t\t\t\tsource: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\t\ttarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\t\treadonly: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmapError: (cause) =>\n\t\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t\t'storage-node',\n\t\t\t\t\t\t\t\t`walrus storage-node-${i} (ip=${containerIp}) ensureContainer failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t});\n\t\t\t\tconst handle: ContainerHandle = yield* Scope.provide(ensureNode, nodeStopScope);\n\n\t\t\t\t// WRITE-READY gate. A two-stage probe execed inside the\n\t\t\t\t// container against its own listener:\n\t\t\t\t//\n\t\t\t\t// 1. TCP `nc -z` — proves the in-container REST server bound\n\t\t\t\t// and is accepting connections (process-up).\n\t\t\t\t// 2. HTTP `GET /v1/health` — proves the node finished\n\t\t\t\t// re-syncing its committee/epoch and reports a SERVING\n\t\t\t\t// `nodeStatus` (write-ready). A node that just recreated\n\t\t\t\t// (the snapshot capture/restore resume = fresh `docker run`\n\t\t\t\t// from a committed/restored image) binds its port quickly\n\t\t\t\t// but stays in recovery for a while; writing a blob before\n\t\t\t\t// it is serving fails the write quorum with \"Too many\n\t\t\t\t// failures while writing blob to nodes\". Gating on\n\t\t\t\t// write-readiness HERE (in the boot ready-gate) means every\n\t\t\t\t// acquire path — up / restart / restore-converge /\n\t\t\t\t// capture-resume — inherits it, fixing the snapshot survival\n\t\t\t\t// matrix without a bespoke per-flow wait.\n\t\t\t\t//\n\t\t\t\t// `exec` only fails on daemon-level errors; a non-zero exit code\n\t\t\t\t// (port not yet bound, health not yet serving) is treated as\n\t\t\t\t// \"retry\". The\n\t\t\t\t// health body is captured to stdout and classified by\n\t\t\t\t// `isWriteReadyHealthBody`; a not-yet-serving status surfaces as\n\t\t\t\t// `ready:false` so the gate keeps polling until the deadline.\n\t\t\t\tyield* setCurrentPluginPhase(\n\t\t\t\t\t`waiting for storage-node-${i} write-readiness on 127.0.0.1:${spec.containerApiPort}`,\n\t\t\t\t);\n\t\t\t\tyield* waitForProbe({\n\t\t\t\t\tlabel: `walrus.storage-node.${i}`,\n\t\t\t\t\ttimeoutMs: Duration.toMillis(readyTimeout),\n\t\t\t\t\tintervalMs: NODE_READY_PROBE_INTERVAL_MS,\n\t\t\t\t\tprobe: () =>\n\t\t\t\t\t\truntime\n\t\t\t\t\t\t\t.exec(handle, [\n\t\t\t\t\t\t\t\t'sh',\n\t\t\t\t\t\t\t\t'-c',\n\t\t\t\t\t\t\t\t// Stage 1: TCP bind check (busybox-portable). Stage 2:\n\t\t\t\t\t\t\t\t// fetch /v1/health and print the body to stdout so the\n\t\t\t\t\t\t\t\t// result classifier can read `nodeStatus`. `curl -fs`\n\t\t\t\t\t\t\t\t// fails (non-zero) until the REST server returns 2xx.\n\t\t\t\t\t\t\t\t`nc -z 127.0.0.1 ${spec.containerApiPort} || exit 1; ` +\n\t\t\t\t\t\t\t\t\t`curl -fs http://127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH} || exit 1`,\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.pipe(\n\t\t\t\t\t\t\t\tEffect.map((result) => {\n\t\t\t\t\t\t\t\t\t// Non-zero exit → not ready (port unbound / health\n\t\t\t\t\t\t\t\t\t// not serving yet). Exit 0 → classify the health body\n\t\t\t\t\t\t\t\t\t// for write-readiness; a not-yet-serving status keeps\n\t\t\t\t\t\t\t\t\t// the gate polling.\n\t\t\t\t\t\t\t\t\tif (result.exitCode !== 0) return exitCodeProbeResult(result);\n\t\t\t\t\t\t\t\t\tif (isWriteReadyHealthBody(result.stdout)) return true;\n\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\tready: false as const,\n\t\t\t\t\t\t\t\t\t\tdetail: { nodeStatus: 'not-serving', body: result.stdout.slice(0, 256) },\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t}).pipe(\n\t\t\t\t\tEffect.mapError((cause) => {\n\t\t\t\t\t\tif (cause instanceof ProbeTimeoutError) {\n\t\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\t\t'storage-node',\n\t\t\t\t\t\t\t\t`storage-node-${i} never became write-ready within ${Duration.toMillis(readyTimeout)}ms ` +\n\t\t\t\t\t\t\t\t\t`(container=${containerName}, probe=127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH}, ` +\n\t\t\t\t\t\t\t\t\t`publicUrl=http://${publicHostname}:${WALRUS_ROUTER_PORT}). ` +\n\t\t\t\t\t\t\t\t\t`The container was created, but walrus-node did not bind its API port or did not ` +\n\t\t\t\t\t\t\t\t\t`finish re-syncing its committee/epoch to a serving nodeStatus (a node recreated from ` +\n\t\t\t\t\t\t\t\t\t`a committed/restored snapshot image must catch up before it accepts blob writes). ` +\n\t\t\t\t\t\t\t\t\t`Check the storage-node container logs for the faucet, WAL exchange, committee ` +\n\t\t\t\t\t\t\t\t\t`recovery, or walrus-node run step that stalled.`,\n\t\t\t\t\t\t\t\t{ cause: cause.lastError ?? cause.lastNotReady ?? cause },\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\t'storage-node',\n\t\t\t\t\t\t\t`storage-node-${i} ready-probe exec failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t);\n\n\t\t\t\treturn {\n\t\t\t\t\tnodeIndex: i,\n\t\t\t\t\t// Stable node-id derived from name. The on-chain\n\t\t\t\t\t// public_key + bls12-381 keypair lives inside the\n\t\t\t\t\t// per-node keystore the deploy one-shot emitted;\n\t\t\t\t\t// this descriptor is the routing handle, not the\n\t\t\t\t\t// crypto identity. The SDK consumer reads the\n\t\t\t\t\t// public key off `packageConfig` rather than this\n\t\t\t\t\t// per-node descriptor.\n\t\t\t\t\tnodeId: `walrus-node-${i}`,\n\t\t\t\t\tpublicHostname,\n\t\t\t\t\t// Router-fronted URL on the well-known walrus\n\t\t\t\t\t// entrypoint port — Traefik routes by Host: header.\n\t\t\t\t\trpcUrl: `http://${publicHostname}:${WALRUS_ROUTER_PORT}`,\n\t\t\t\t} satisfies WalrusStorageNode;\n\t\t\t});\n\n\t\tconst nodes = yield* Effect.all(indices.map(bootOne), {\n\t\t\tconcurrency: 'unbounded',\n\t\t});\n\n\t\treturn { nodes };\n\t});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage-nodes.mjs","names":[],"sources":["../../../src/plugins/walrus/storage-nodes.ts"],"sourcesContent":["// Walrus storage-node committee lifecycle.\n//\n// Distilled-doc reference (06-walrus.md §\"Capabilities CONSUMED\" +\n// §\"Lifecycle phase 4\"): N storage-node containers on a per-stack\n// docker network. Each node carries one Traefik route via the `Routable`\n// capability; the router binds host port 9185 once globally and dispatches\n// by `Host:` header.\n//\n// Substrate-side responsibilities:\n// - `ContainerRuntime.ensureContainer` per node — content-addressed\n// image, label tuple, recreate policy.\n// - Parallel `Effect.all({concurrency: 'unbounded'})` over [0..N) —\n// concurrent boot.\n// - Per-node stop finalizer fires in a *forked parallel scope* so\n// teardown collapses to ~max(grace) instead of summing graces\n// (distilled-doc invariant 21 — \"StopFinalizerScope MUST be a\n// forked parallel scope\").\n//\n// What we DON'T own here:\n// - Plugin row narration.\n// - The faucet-strategy registration — `faucet-strategy.ts`.\n// - The deploy one-shot — `deploy.ts`.\n\nimport { Duration, Effect, Scope } from 'effect';\n\nimport type {\n\tContainerHandle,\n\tContainerRuntime,\n\tImageRef,\n} from '../../contracts/container-runtime.ts';\nimport {\n\tProbeTimeoutError,\n\texitCodeProbeResult,\n\twaitForProbe,\n} from '../../substrate/runtime/probes.ts';\nimport { setCurrentPluginPhase } from '../../substrate/runtime/current-plugin.ts';\nimport { ensureManagedContainer } from '../../substrate/runtime/managed-container.ts';\nimport { HOST_GATEWAY_EXTRA_HOSTS } from '../../substrate/runtime/host-gateway.ts';\nimport { walrusDeployMountPaths } from './deploy-paths.ts';\nimport { walrusPluginError, type WalrusPluginError } from './errors.ts';\n\n/** Default per-node grace window. Storage nodes maintain RocksDB-\n * backed state; need >10s to flush and checkpoint on `docker stop`.\n * Distilled-doc invariant 22. */\nexport const DEFAULT_NODE_STOP_GRACE_SECONDS = 20;\n\n/** Default per-node ready-probe timeout. */\nexport const DEFAULT_NODE_READY_TIMEOUT_MS = 60_000;\n\n/** Distilled-doc invariant 9: the on-chain `public_port` value\n * matches the global router entrypoint port. One canonical\n * constant — opportunity #14 in the distilled doc was to deduplicate\n * this across three files. */\nexport const WALRUS_ROUTER_PORT = 9185;\n\n/** Default container API port — each storage node binds this inside\n * the container. Same value as the router port (the storage node's\n * in-container bind matches the on-chain public_port). */\nexport const DEFAULT_CONTAINER_API_PORT = WALRUS_ROUTER_PORT;\n\n/** Per-node descriptor — what the plugin resolved value surfaces\n * for downstream consumers (the `@mysten/walrus` SDK reads this).\n *\n * Note: no `publicKey` field. The per-node BLS12-381 keypair lives\n * inside the per-node keystore the deploy one-shot emitted; the SDK\n * consumer reads the public key off `packageConfig` rather than this\n * routing-handle descriptor. */\nexport interface WalrusStorageNode {\n\treadonly nodeIndex: number;\n\treadonly nodeId: string;\n\treadonly publicHostname: string;\n\treadonly rpcUrl: string;\n}\n\n/** Storage-node startup config — the plugin's local-cluster mode\n * passes this in once per cluster boot. */\nexport interface StorageNodesSpec {\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly walrusName: string;\n\t/** One image ref PER node (`images[i]` for node `i`; length MUST equal\n\t * `nodeCount`). Each node runs under its own tag so its committed writable\n\t * layer (RocksDB slivers) promotes to a DISTINCT image on snapshot restore;\n\t * a single shared tag collapses N committed layers -> 1 on the restore\n\t * image-promote, orphaning N-1 nodes' blobs. */\n\treadonly images: ReadonlyArray<ImageRef>;\n\treadonly nodeCount: number;\n\t/** Pre-allocated /24 prefix from `subnetForStack`. Example:\n\t * `'10.42.7'` (the third octet is hashed per stack). The deploy helper\n\t * still needs deterministic generated listening IPs, but runtime routing\n\t * between managed containers goes through Docker DNS aliases rather than\n\t * assuming Docker assigned those addresses. */\n\treadonly subnetPrefix: string;\n\treadonly containerApiPort: number;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n\treadonly deployHostMountPath: string;\n\t/** On-disk per-stack root from `StackPathsService.stackRoot`. The\n\t * walrus bind-mount is computed by `walrusDeployMountPaths` as\n\t * `<stackRoot> -> <mountTarget>`; the deploy output dir MUST be a\n\t * descendant. Threaded explicitly to remove the previous\n\t * `dirname(dirname(dirname(...)))` walk-up footgun. */\n\treadonly stackRoot: string;\n\t/** Fingerprint of the deploy outputs mounted into each node. */\n\treadonly deployConfigHash: string;\n\treadonly stopGraceSeconds?: number;\n\treadonly readyTimeoutMs?: number;\n}\n\n/** Result of starting the committee — descriptors used by the\n * plugin's resolved value. */\nexport interface StorageNodesAcquired {\n\treadonly nodes: ReadonlyArray<WalrusStorageNode>;\n}\n\n/** Deterministic generated listening IP base for the upstream dry-run config\n * generator. These addresses are not used as Docker runtime addresses; peer\n * containers dial storage nodes through `computeCommitteeHostname()` aliases. */\nexport const WALRUS_NODE_IP_BASE = 10;\n\n/** Compute the public hostname for one storage node — the\n * `routerHostname(identity, 'walrus-node-' + i)` shape\n * (06-walrus.md §\"Routes registered\"). Main stack omits the stack\n * segment; non-main inserts it after the service label. */\nexport const computePublicHostname = (app: string, stack: string, nodeIndex: number): string => {\n\tconst base = `walrus-node-${nodeIndex}.${app}.localhost`;\n\treturn stack === 'main' ? base : `walrus-node-${nodeIndex}.${stack}.${app}.localhost`;\n};\n\n/** Docker-network hostname written into the on-chain Walrus committee.\n * The real Rust publisher/aggregator read these public_host values from\n * chain and dial storage nodes from inside the Walrus network. Do not use\n * `.localhost` here: many resolvers special-case that suffix to loopback\n * inside the client-service container. */\nexport const computeCommitteeHostname = (nodeIndex: number): string => `dryrun-node-${nodeIndex}`;\n\nexport const storageNodeConfigHash = (parts: {\n\treadonly deployConfigHash: string;\n\treadonly nodeIndex: number;\n\treadonly committeeHostname: string;\n\treadonly deploySourceHostPath: string;\n\treadonly deployMountTarget: string;\n\treadonly containerApiPort: number;\n\treadonly walrusNetworkName: string;\n\treadonly suiNetworkName: string;\n}): string =>\n\t[\n\t\tparts.deployConfigHash,\n\t\t`node=${parts.nodeIndex}`,\n\t\t`committee-host=${parts.committeeHostname}`,\n\t\t`mount=${parts.deploySourceHostPath}->${parts.deployMountTarget}`,\n\t\t`api=${parts.containerApiPort}`,\n\t\t`net=${parts.walrusNetworkName},${parts.suiNetworkName}`,\n\t].join('|');\n\nexport const buildWalrusNetworkName = (app: string, stack: string, walrusName: string): string =>\n\t`devstack-${app}-${stack}-walrus-${walrusName}-net`;\n\n/** Per-node TCP ready-probe interval. */\nconst NODE_READY_PROBE_INTERVAL_MS = 500;\n\n/** Walrus storage-node REST health endpoint. Local nodes keep Walrus'\n * self-signed TLS enabled because the Rust publisher/aggregator clients\n * authenticate storage nodes by their network public key and always dial\n * HTTPS. The router still exposes an HTTP localhost URL and proxies to\n * the HTTPS upstream for browser/dev convenience. */\nconst NODE_HEALTH_PATH = '/v1/health';\n\n/** Recovery / not-yet-serving markers in a `/v1/health` body. A node that\n * just recreated (fresh `docker run` from a committed/restored image — the\n * snapshot capture/restore resume) comes back up and reports a recovery\n * status while it re-syncs its committee/epoch; writing a blob to it before\n * it catches up fails the write quorum with \"Too many failures while\n * writing blob to nodes\". The boot ready-gate blocks until NONE of these\n * markers is present so the node is genuinely serving. */\nconst NODE_RECOVERY_MARKERS: ReadonlyArray<RegExp> = [\n\t/\"nodeStatus\"\\s*:\\s*\"Recovery/i,\n\t/\"nodeStatus\"\\s*:\\s*\"Standby\"/i,\n\t/\"nodeStatus\"\\s*:\\s*\"Starting\"/i,\n\t/\"isInRecovery\"\\s*:\\s*true/i,\n\t/\"inRecovery\"\\s*:\\s*true/i,\n];\n\n/** Classify a `/v1/health` 2xx body as write-ready. Biased toward NOT\n * false-negativing the boot (a too-strict classifier would time the boot\n * out on a field-name change upstream): a node whose health endpoint\n * returns 2xx is treated as serving UNLESS the body explicitly reports a\n * recovery / standby / starting status. The explicit `Active` serving\n * status is the fast-path. This gates EVERY acquire path (up / restart /\n * restore-converge / capture-resume) on write-readiness, not just\n * process-up — the snapshot survival-matrix fix. */\nconst isWriteReadyHealthBody = (body: string): boolean => {\n\tconst trimmed = body.trim();\n\tif (/\"nodeStatus\"\\s*:\\s*\"Active\"/i.test(trimmed)) return true;\n\t// Empty body from a 2xx with no payload: the listener answered but gave\n\t// no status — treat as not-yet-ready so the gate keeps polling briefly.\n\tif (trimmed.length === 0) return false;\n\treturn !NODE_RECOVERY_MARKERS.some((marker) => marker.test(trimmed));\n};\n\n/** Start the N-node committee.\n *\n * Wiring (this body is the load-bearing path):\n *\n * 1. Defense-in-depth nodeCount guard.\n * 2. Fork a parallel-strategy child scope off the caller's scope so\n * every node's `docker stop` finalizer fires concurrently on\n * outer teardown — collapses shutdown wall-time from `N×grace` to\n * `~max(grace)` (distilled-doc invariant 21).\n * 3. For each `i ∈ [0..N)`:\n * a. `ensureContainer` on the wrapper image with label tuple,\n * primary network attach (walrus net) plus committee DNS alias,\n * recreate on config-change.\n * b. In-container `/v1/health` ready probe with bounded retries\n * until the outer `readyTimeoutMs`.\n * 4. Run [3] under `Effect.all({concurrency: 'unbounded'})` — boot\n * parallelism collapses to ~max(perNode).\n *\n * WHAT THE SUBSTRATE STILL OWES:\n * - Secondary network attach (`Docker.networkConnect(suiNet)`).\n * The runtime adapter treats [0] as primary and post-attaches the\n * rest; the primary Walrus attach carries the committee DNS alias.\n * Storage nodes do not request faucet funds during boot; the\n * secondary network is retained for future Sui-side in-network calls.\n */\nexport const startStorageNodes = (\n\truntime: ContainerRuntime,\n\tspec: StorageNodesSpec,\n): Effect.Effect<StorageNodesAcquired, WalrusPluginError, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tif (spec.nodeCount < 1) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalrusPluginError(\n\t\t\t\t\t'storage-node',\n\t\t\t\t\t`storage-nodes: nodeCount must be >= 1 (got ${spec.nodeCount})`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tif (spec.images.length !== spec.nodeCount) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\twalrusPluginError(\n\t\t\t\t\t'storage-node',\n\t\t\t\t\t`storage-nodes: images length (${spec.images.length}) must equal nodeCount (${spec.nodeCount}) — one distinct per-node image tag is required for restore-safe commits`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Fork a parallel stop-scope. Per-node `ensureContainer` finalizers\n\t\t// fire here on outer teardown; closing the parallel scope races\n\t\t// the per-container `docker stop`s instead of summing them.\n\t\t//\n\t\t// Cascade semantics (verified against effect-v4 `Scope.ts`\n\t\t// `fork` docs — \"Closing the parent closes the child with the\n\t\t// same exit value\"): when ANY `bootOne` fails (e.g. a ready-\n\t\t// probe times out on node i), `Effect.all` interrupts the rest;\n\t\t// the failure propagates up through the encompassing\n\t\t// `Effect.scoped` boundary which closes `outerScope`; that\n\t\t// cascade-closes `nodeStopScope`, running EVERY already-acquired\n\t\t// per-node `docker stop` finalizer in parallel. The probe is\n\t\t// intentionally OUTSIDE `Scope.provide(nodeStopScope)` — its\n\t\t// failures do NOT need their own finalizer attached to the\n\t\t// stop scope; the just-started container's finalizer is already\n\t\t// there from the `ensureNode` acquire that preceded the probe.\n\t\tconst outerScope = yield* Effect.scope;\n\t\tconst nodeStopScope = yield* Scope.fork(outerScope, 'parallel');\n\n\t\tconst stopGraceSeconds = spec.stopGraceSeconds ?? DEFAULT_NODE_STOP_GRACE_SECONDS;\n\t\tconst readyTimeout = Duration.millis(spec.readyTimeoutMs ?? DEFAULT_NODE_READY_TIMEOUT_MS);\n\t\tconst deployMount = walrusDeployMountPaths({\n\t\t\tstackRoot: spec.stackRoot,\n\t\t\tdeployOutputDirHostPath: spec.deployHostMountPath,\n\t\t\tmountTarget: '/opt/walrus/runtime',\n\t\t});\n\n\t\tconst indices = Array.from({ length: spec.nodeCount }, (_, i) => i);\n\n\t\tconst bootOne = (i: number): Effect.Effect<WalrusStorageNode, WalrusPluginError, Scope.Scope> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst containerName = `devstack-${spec.app}-${spec.stack}-walrus-${spec.walrusName}-node-${i}`;\n\t\t\t\tconst nodeHostname = computeCommitteeHostname(i);\n\t\t\t\tconst publicHostname = computePublicHostname(spec.app, spec.stack, i);\n\n\t\t\t\tyield* setCurrentPluginPhase(`creating storage-node-${i} container ${containerName}`);\n\t\t\t\t// Acquire under the forked parallel stop scope so the\n\t\t\t\t// finalizer (docker stop) joins the per-node fan-out race.\n\t\t\t\tconst ensureNode: Effect.Effect<ContainerHandle, WalrusPluginError, Scope.Scope> =\n\t\t\t\t\tensureManagedContainer<WalrusPluginError>({\n\t\t\t\t\t\truntime,\n\t\t\t\t\t\tidentity: { app: spec.app, stack: spec.stack },\n\t\t\t\t\t\tplugin: 'walrus',\n\t\t\t\t\t\trole: `storage-node-${i}`,\n\t\t\t\t\t\tspec: {\n\t\t\t\t\t\t\tname: containerName,\n\t\t\t\t\t\t\t// Node `i`'s own image tag — distinct per node so its\n\t\t\t\t\t\t\t// committed RocksDB layer survives snapshot restore.\n\t\t\t\t\t\t\timage: spec.images[i]!,\n\t\t\t\t\t\t\trecreate: 'on-config-change',\n\t\t\t\t\t\t\tconfigHash: storageNodeConfigHash({\n\t\t\t\t\t\t\t\tdeployConfigHash: spec.deployConfigHash,\n\t\t\t\t\t\t\t\tnodeIndex: i,\n\t\t\t\t\t\t\t\tcommitteeHostname: nodeHostname,\n\t\t\t\t\t\t\t\tdeploySourceHostPath: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\tdeployMountTarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\tcontainerApiPort: spec.containerApiPort,\n\t\t\t\t\t\t\t\twalrusNetworkName: spec.walrusNetworkName,\n\t\t\t\t\t\t\t\tsuiNetworkName: spec.suiNetworkName,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tenv: {\n\t\t\t\t\t\t\t\tHOSTNAME: nodeHostname,\n\t\t\t\t\t\t\t\tDEPLOY_OUTPUT_DIR: deployMount.outputDirInContainer,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tstopGraceSeconds,\n\t\t\t\t\t\t\t// Primary network registers the committee alias that\n\t\t\t\t\t\t\t// is written into the Walrus committee record. The\n\t\t\t\t\t\t\t// secondary attach gives docker DNS for the sui\n\t\t\t\t\t\t\t// network. The adapter treats [0] as primary.\n\t\t\t\t\t\t\tnetworkAttach: [\n\t\t\t\t\t\t\t\t{ name: spec.walrusNetworkName, aliases: [nodeHostname] },\n\t\t\t\t\t\t\t\tspec.suiNetworkName,\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t// Storage nodes dial sui's host-bound RPC + faucet via\n\t\t\t\t\t\t\t// `host.docker.internal`. See\n\t\t\t\t\t\t\t// substrate/runtime/host-gateway.ts for the platform\n\t\t\t\t\t\t\t// rationale (Docker Desktop auto-wires; native Linux\n\t\t\t\t\t\t\t// Docker requires the explicit `--add-host`).\n\t\t\t\t\t\t\textraHosts: HOST_GATEWAY_EXTRA_HOSTS,\n\t\t\t\t\t\t\tmounts: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t// Mount the stack root and point the entrypoint at\n\t\t\t\t\t\t\t\t\t// the deploy child. Docker Desktop can reject\n\t\t\t\t\t\t\t\t\t// freshly-created nested bind sources immediately\n\t\t\t\t\t\t\t\t\t// after a wiped stack recreates them.\n\t\t\t\t\t\t\t\t\tsource: deployMount.sourceHostPath,\n\t\t\t\t\t\t\t\t\ttarget: deployMount.mountTarget,\n\t\t\t\t\t\t\t\t\treadonly: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmapError: (cause) =>\n\t\t\t\t\t\t\twalrusPluginError(\n\t\t\t\t\t\t\t\t'storage-node',\n\t\t\t\t\t\t\t\t`walrus storage-node-${i} (host=${nodeHostname}) ensureContainer failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t});\n\t\t\t\tconst handle: ContainerHandle = yield* Scope.provide(ensureNode, nodeStopScope);\n\n\t\t\t\t// WRITE-READY gate. A two-stage probe execed inside the\n\t\t\t\t// container against its own listener:\n\t\t\t\t//\n\t\t\t\t// 1. TCP `nc -z` — proves the in-container REST server bound\n\t\t\t\t// and is accepting connections (process-up).\n\t\t\t\t// 2. HTTP `GET /v1/health` — proves the node finished\n\t\t\t\t// re-syncing its committee/epoch and reports a SERVING\n\t\t\t\t// `nodeStatus` (write-ready). A node that just recreated\n\t\t\t\t// (the snapshot capture/restore resume = fresh `docker run`\n\t\t\t\t// from a committed/restored image) binds its port quickly\n\t\t\t\t// but stays in recovery for a while; writing a blob before\n\t\t\t\t// it is serving fails the write quorum with \"Too many\n\t\t\t\t// failures while writing blob to nodes\". Gating on\n\t\t\t\t// write-readiness HERE (in the boot ready-gate) means every\n\t\t\t\t// acquire path — up / restart / restore-converge /\n\t\t\t\t// capture-resume — inherits it, fixing the snapshot survival\n\t\t\t\t// matrix without a bespoke per-flow wait.\n\t\t\t\t//\n\t\t\t\t// `exec` only fails on daemon-level errors; a non-zero exit code\n\t\t\t\t// (port not yet bound, health not yet serving) is treated as\n\t\t\t\t// \"retry\". The\n\t\t\t\t// health body is captured to stdout and classified by\n\t\t\t\t// `isWriteReadyHealthBody`; a not-yet-serving status surfaces as\n\t\t\t\t// `ready:false` so the gate keeps polling until the deadline.\n\t\t\t\tyield* setCurrentPluginPhase(\n\t\t\t\t\t`waiting for storage-node-${i} write-readiness on 127.0.0.1:${spec.containerApiPort}`,\n\t\t\t\t);\n\t\t\t\tyield* waitForProbe({\n\t\t\t\t\tlabel: `walrus.storage-node.${i}`,\n\t\t\t\t\ttimeoutMs: Duration.toMillis(readyTimeout),\n\t\t\t\t\tintervalMs: NODE_READY_PROBE_INTERVAL_MS,\n\t\t\t\t\tprobe: () =>\n\t\t\t\t\t\truntime\n\t\t\t\t\t\t\t.exec(handle, [\n\t\t\t\t\t\t\t\t'sh',\n\t\t\t\t\t\t\t\t'-c',\n\t\t\t\t\t\t\t\t// Stage 1: TCP bind check (busybox-portable). Stage 2:\n\t\t\t\t\t\t\t\t// fetch /v1/health and print the body to stdout so the\n\t\t\t\t\t\t\t\t// result classifier can read `nodeStatus`. `curl -fks`\n\t\t\t\t\t\t\t\t// fails (non-zero) until the TLS REST server returns 2xx.\n\t\t\t\t\t\t\t\t`nc -z 127.0.0.1 ${spec.containerApiPort} || exit 1; ` +\n\t\t\t\t\t\t\t\t\t`curl -fks https://127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH} || exit 1`,\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.pipe(\n\t\t\t\t\t\t\t\tEffect.map((result) => {\n\t\t\t\t\t\t\t\t\t// Non-zero exit → not ready (port unbound / health\n\t\t\t\t\t\t\t\t\t// not serving yet). Exit 0 → classify the health body\n\t\t\t\t\t\t\t\t\t// for write-readiness; a not-yet-serving status keeps\n\t\t\t\t\t\t\t\t\t// the gate polling.\n\t\t\t\t\t\t\t\t\tif (result.exitCode !== 0) return exitCodeProbeResult(result);\n\t\t\t\t\t\t\t\t\tif (isWriteReadyHealthBody(result.stdout)) return true;\n\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\tready: false as const,\n\t\t\t\t\t\t\t\t\t\tdetail: { nodeStatus: 'not-serving', body: result.stdout.slice(0, 256) },\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t}).pipe(\n\t\t\t\t\tEffect.mapError((cause) => {\n\t\t\t\t\t\tif (cause instanceof ProbeTimeoutError) {\n\t\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\t\t'storage-node',\n\t\t\t\t\t\t\t\t`storage-node-${i} never became write-ready within ${Duration.toMillis(readyTimeout)}ms ` +\n\t\t\t\t\t\t\t\t\t`(container=${containerName}, probe=https://127.0.0.1:${spec.containerApiPort}${NODE_HEALTH_PATH}, ` +\n\t\t\t\t\t\t\t\t\t`publicUrl=http://${publicHostname}:${WALRUS_ROUTER_PORT}). ` +\n\t\t\t\t\t\t\t\t\t`The container was created, but walrus-node did not bind its API port or did not ` +\n\t\t\t\t\t\t\t\t\t`finish re-syncing its committee/epoch to a serving nodeStatus (a node recreated from ` +\n\t\t\t\t\t\t\t\t\t`a committed/restored snapshot image must catch up before it accepts blob writes). ` +\n\t\t\t\t\t\t\t\t\t`Check the storage-node container logs for the faucet, WAL exchange, committee ` +\n\t\t\t\t\t\t\t\t\t`recovery, or walrus-node run step that stalled.`,\n\t\t\t\t\t\t\t\t{ cause: cause.lastError ?? cause.lastNotReady ?? cause },\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn walrusPluginError(\n\t\t\t\t\t\t\t'storage-node',\n\t\t\t\t\t\t\t`storage-node-${i} ready-probe exec failed: ${cause.reason}: ${cause.detail}`,\n\t\t\t\t\t\t\t{ cause },\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t);\n\n\t\t\t\treturn {\n\t\t\t\t\tnodeIndex: i,\n\t\t\t\t\t// Stable node-id derived from name. The on-chain\n\t\t\t\t\t// public_key + bls12-381 keypair lives inside the\n\t\t\t\t\t// per-node keystore the deploy one-shot emitted;\n\t\t\t\t\t// this descriptor is the routing handle, not the\n\t\t\t\t\t// crypto identity. The SDK consumer reads the\n\t\t\t\t\t// public key off `packageConfig` rather than this\n\t\t\t\t\t// per-node descriptor.\n\t\t\t\t\tnodeId: `walrus-node-${i}`,\n\t\t\t\t\tpublicHostname,\n\t\t\t\t\t// Router-fronted URL on the well-known walrus\n\t\t\t\t\t// entrypoint port — Traefik routes by Host: header.\n\t\t\t\t\trpcUrl: `http://${publicHostname}:${WALRUS_ROUTER_PORT}`,\n\t\t\t\t} satisfies WalrusStorageNode;\n\t\t\t});\n\n\t\tconst nodes = yield* Effect.all(indices.map(bootOne), {\n\t\t\tconcurrency: 'unbounded',\n\t\t});\n\n\t\treturn { nodes };\n\t});\n"],"mappings":"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"}
|
|
@@ -43,6 +43,6 @@ const expectPort = (value, options) => {
|
|
|
43
43
|
});
|
|
44
44
|
};
|
|
45
45
|
//#endregion
|
|
46
|
-
export { config_validation_exports, defineConfigError, expectNonEmptyString, expectOptionalPositiveInteger, expectPositiveInteger };
|
|
46
|
+
export { config_validation_exports, defineConfigError, expectNonEmptyString, expectOptionalPositiveInteger, expectPort, expectPositiveInteger };
|
|
47
47
|
|
|
48
48
|
//# sourceMappingURL=config-validation.mjs.map
|
package/images/walrus/Dockerfile
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
# Relocates per-node configs out of the read-only deploy mount,
|
|
26
26
|
# swaps SUI for WAL, and execs `walrus-node run`.
|
|
27
27
|
#
|
|
28
|
+
# - /opt/walrus/scripts/run-walrus-client-service.sh — long-running
|
|
29
|
+
# release CLI entrypoint for `walrus aggregator` and `walrus publisher`.
|
|
30
|
+
# Relocates a deploy-generated wallet and execs the selected `walrus`
|
|
31
|
+
# subcommand.
|
|
32
|
+
#
|
|
28
33
|
# `ubuntu:24.04` because the walrus + sui binaries link against
|
|
29
34
|
# glibc 2.38+; debian:bookworm (glibc 2.36) fails at runtime.
|
|
30
35
|
|
|
@@ -68,6 +73,8 @@ RUN set -eux; \
|
|
|
68
73
|
done; \
|
|
69
74
|
rm -rf /tmp/walrus.tgz /tmp/walrus-unpack; \
|
|
70
75
|
/opt/walrus/bin/walrus --version; \
|
|
76
|
+
/opt/walrus/bin/walrus aggregator --help >/dev/null; \
|
|
77
|
+
/opt/walrus/bin/walrus publisher --help >/dev/null; \
|
|
71
78
|
/opt/walrus/bin/walrus-node --help >/dev/null; \
|
|
72
79
|
/opt/walrus/bin/walrus-deploy --help >/dev/null
|
|
73
80
|
|
|
@@ -108,15 +115,21 @@ RUN mkdir -p /opt/walrus/scripts /opt/walrus/outputs /var/walrus/storage
|
|
|
108
115
|
|
|
109
116
|
COPY deploy-walrus.sh /opt/walrus/scripts/deploy-walrus.sh
|
|
110
117
|
COPY run-walrus.sh /opt/walrus/scripts/run-walrus.sh
|
|
118
|
+
COPY run-walrus-client-service.sh /opt/walrus/scripts/run-walrus-client-service.sh
|
|
111
119
|
RUN chmod +x /opt/walrus/scripts/*.sh
|
|
112
120
|
|
|
113
121
|
ENV RUST_LOG=info,walrus=info
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
STOPSIGNAL SIGINT
|
|
124
|
+
|
|
125
|
+
# Storage-node REST port. Distilled-doc invariant 9. Client services bind
|
|
126
|
+
# their release CLI default unless overridden by env.
|
|
116
127
|
EXPOSE 9185
|
|
128
|
+
EXPOSE 31415
|
|
117
129
|
|
|
118
130
|
# Default to storage-node entrypoint. The deploy one-shot overrides
|
|
119
|
-
# via `runOneShot` argv: argv[0] == 'deploy' runs `deploy-walrus.sh
|
|
120
|
-
#
|
|
121
|
-
|
|
131
|
+
# via `runOneShot` argv: argv[0] == 'deploy' runs `deploy-walrus.sh`.
|
|
132
|
+
# `aggregator` and `publisher` run the release-provided client services;
|
|
133
|
+
# any other command falls back to the storage-node entrypoint.
|
|
134
|
+
ENTRYPOINT ["/bin/bash", "-c", "case \"${1:-}\" in deploy) shift; exec /opt/walrus/scripts/deploy-walrus.sh \"$@\" ;; aggregator|publisher) mode=\"$1\"; shift; exec /opt/walrus/scripts/run-walrus-client-service.sh \"$mode\" \"$@\" ;; *) exec /opt/walrus/scripts/run-walrus.sh ;; esac", "--"]
|
|
122
135
|
CMD []
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
# - dryrun-node-<i>.yaml — per-node walrus-node config.
|
|
32
32
|
# - dryrun-node-<i>.keystore — per-node sui keystore.
|
|
33
33
|
# - dryrun-node-<i>-sui.yaml — per-node sui client config.
|
|
34
|
+
# - client_config.yaml — funded Walrus client config.
|
|
35
|
+
# - sui_client.yaml — funded Sui client wallet for publisher/aggregator.
|
|
36
|
+
# - sui_client.keystore — funded Sui client keystore for publisher/aggregator.
|
|
34
37
|
#
|
|
35
38
|
# Stdout MUST include the deploy summary in `key: 0x<hex>` format
|
|
36
39
|
# (plugins/walrus/deploy.ts::parseDeployOutput regexes pick these out).
|
|
@@ -45,6 +48,7 @@ FAUCET_URL=""
|
|
|
45
48
|
PUBLIC_HOSTS_CSV=""
|
|
46
49
|
LISTENING_IPS_CSV=""
|
|
47
50
|
GC=false
|
|
51
|
+
CLIENT_WALLET_SUI_AMOUNT_MIST="${WALRUS_CLIENT_WALLET_SUI_AMOUNT_MIST:-10000000000}"
|
|
48
52
|
|
|
49
53
|
while [ $# -gt 0 ]; do
|
|
50
54
|
case "$1" in
|
|
@@ -254,11 +258,12 @@ WALRUS_NETWORK_ARG="${SUI_RPC_URL};${FAUCET_URL}"
|
|
|
254
258
|
|
|
255
259
|
"$WALRUS_DEPLOY_BIN" generate-dry-run-configs \
|
|
256
260
|
--working-dir "$WORKING_DIR" \
|
|
261
|
+
--admin-wallet-path "$ADMIN_WALLET_YAML" \
|
|
262
|
+
--sui-amount "$CLIENT_WALLET_SUI_AMOUNT_MIST" \
|
|
257
263
|
--listening-ips "${LISTENING_IPS[@]}"
|
|
258
264
|
|
|
259
265
|
# Patch generated node configs: storage_path → writable layer, rebind on
|
|
260
|
-
# 0.0.0.0,
|
|
261
|
-
# enable blob GC + data deletion when --gc was passed.
|
|
266
|
+
# 0.0.0.0, optionally enable blob GC + data deletion when --gc was passed.
|
|
262
267
|
for f in "$WORKING_DIR"/dryrun-node-*[0-9].yaml; do
|
|
263
268
|
sed -i "s|^storage_path: ${WORKING_DIR}/|storage_path: /var/walrus/storage/|" "$f"
|
|
264
269
|
sed -i -E "s|^(rest_api_address): [0-9.]+:|\\1: 0.0.0.0:|" "$f"
|
|
@@ -269,8 +274,6 @@ for f in "$WORKING_DIR"/dryrun-node-*[0-9].yaml; do
|
|
|
269
274
|
adaptive_downloader_config:
|
|
270
275
|
max_workers: 2
|
|
271
276
|
initial_workers: 2
|
|
272
|
-
tls:
|
|
273
|
-
disable_tls: true
|
|
274
277
|
NODEEOF
|
|
275
278
|
if [ "$GC" = "true" ]; then
|
|
276
279
|
cat >> "$f" <<-GCEOF
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Entrypoint for release-provided Walrus client services.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
MODE="${1:-}"
|
|
6
|
+
if [ -z "$MODE" ]; then
|
|
7
|
+
echo "run-walrus-client-service: service mode is required" >&2
|
|
8
|
+
exit 2
|
|
9
|
+
fi
|
|
10
|
+
shift
|
|
11
|
+
|
|
12
|
+
case "$MODE" in
|
|
13
|
+
aggregator|publisher) ;;
|
|
14
|
+
*)
|
|
15
|
+
echo "run-walrus-client-service: unsupported mode '$MODE'" >&2
|
|
16
|
+
exit 2
|
|
17
|
+
;;
|
|
18
|
+
esac
|
|
19
|
+
|
|
20
|
+
WORKING_DIR="${DEPLOY_OUTPUT_DIR:-/opt/walrus/outputs}"
|
|
21
|
+
BIND_ADDRESS="${WALRUS_CLIENT_SERVICE_BIND_ADDRESS:-0.0.0.0:31415}"
|
|
22
|
+
CLIENT_CONFIG_SOURCE="$WORKING_DIR/client_config.yaml"
|
|
23
|
+
CLIENT_WALLET_SOURCE="$WORKING_DIR/sui_client.yaml"
|
|
24
|
+
CLIENT_KEYSTORE_SOURCE="$WORKING_DIR/sui_client.keystore"
|
|
25
|
+
CLIENT_CONFIG_TARGET="/root/.config/walrus/client_config.yaml"
|
|
26
|
+
CLIENT_WALLET_TARGET="/root/.sui/sui_config/client.yaml"
|
|
27
|
+
CLIENT_KEYSTORE_TARGET="/root/.sui/sui_config/sui.keystore"
|
|
28
|
+
|
|
29
|
+
mkdir -p /root/.sui/sui_config /root/.config/walrus /var/walrus
|
|
30
|
+
|
|
31
|
+
require_file() {
|
|
32
|
+
local path="$1"
|
|
33
|
+
local label="$2"
|
|
34
|
+
if [ ! -s "$path" ]; then
|
|
35
|
+
echo "run-walrus-client-service: missing ${label} at ${path}" >&2
|
|
36
|
+
exit 3
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
escape_sed_replacement() {
|
|
41
|
+
printf '%s' "$1" | sed -e 's/[|&\\]/\\&/g'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
require_file "$CLIENT_CONFIG_SOURCE" "Walrus client config"
|
|
45
|
+
require_file "$CLIENT_WALLET_SOURCE" "Sui client wallet"
|
|
46
|
+
require_file "$CLIENT_KEYSTORE_SOURCE" "Sui client keystore"
|
|
47
|
+
|
|
48
|
+
echo "run-walrus-client-service: preparing ${MODE} client config from ${WORKING_DIR}"
|
|
49
|
+
|
|
50
|
+
cp "$CLIENT_CONFIG_SOURCE" "$CLIENT_CONFIG_TARGET"
|
|
51
|
+
cp "$CLIENT_WALLET_SOURCE" "$CLIENT_WALLET_TARGET"
|
|
52
|
+
cp "$CLIENT_KEYSTORE_SOURCE" "$CLIENT_KEYSTORE_TARGET"
|
|
53
|
+
|
|
54
|
+
sed -i \
|
|
55
|
+
"s|${CLIENT_KEYSTORE_SOURCE}|${CLIENT_KEYSTORE_TARGET}|g" \
|
|
56
|
+
"$CLIENT_WALLET_TARGET"
|
|
57
|
+
if ! grep -Fq "$CLIENT_KEYSTORE_TARGET" "$CLIENT_WALLET_TARGET"; then
|
|
58
|
+
echo "run-walrus-client-service: wallet config did not rewrite keystore path to ${CLIENT_KEYSTORE_TARGET}" >&2
|
|
59
|
+
exit 3
|
|
60
|
+
fi
|
|
61
|
+
if grep -Fq "$CLIENT_WALLET_SOURCE" "$CLIENT_CONFIG_TARGET"; then
|
|
62
|
+
sed -i \
|
|
63
|
+
"s|${CLIENT_WALLET_SOURCE}|${CLIENT_WALLET_TARGET}|g" \
|
|
64
|
+
"$CLIENT_CONFIG_TARGET"
|
|
65
|
+
if ! grep -Fq "$CLIENT_WALLET_TARGET" "$CLIENT_CONFIG_TARGET"; then
|
|
66
|
+
echo "run-walrus-client-service: Walrus config did not rewrite wallet path to ${CLIENT_WALLET_TARGET}" >&2
|
|
67
|
+
exit 3
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [ -n "${SUI_RPC_URL:-}" ]; then
|
|
72
|
+
escaped_sui_rpc_url=$(escape_sed_replacement "$SUI_RPC_URL")
|
|
73
|
+
sed -i -E "s|^([[:space:]]*rpc:[[:space:]]*).*$|\\1${escaped_sui_rpc_url}|" \
|
|
74
|
+
"$CLIENT_WALLET_TARGET"
|
|
75
|
+
if ! grep -Fq "rpc: ${SUI_RPC_URL}" "$CLIENT_WALLET_TARGET"; then
|
|
76
|
+
echo "run-walrus-client-service: wallet config did not rewrite rpc to ${SUI_RPC_URL}" >&2
|
|
77
|
+
exit 3
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
cmd=(
|
|
82
|
+
/opt/walrus/bin/walrus
|
|
83
|
+
"$MODE"
|
|
84
|
+
--bind-address "$BIND_ADDRESS"
|
|
85
|
+
--config "$CLIENT_CONFIG_TARGET"
|
|
86
|
+
--wallet "$CLIENT_WALLET_TARGET"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
case "$MODE" in
|
|
90
|
+
aggregator)
|
|
91
|
+
if [ -n "${SUI_RPC_URL:-}" ]; then
|
|
92
|
+
cmd+=(--rpc-url "$SUI_RPC_URL")
|
|
93
|
+
fi
|
|
94
|
+
;;
|
|
95
|
+
publisher)
|
|
96
|
+
sub_wallets_dir="/var/walrus/${MODE}-sub-wallets"
|
|
97
|
+
mkdir -p "$sub_wallets_dir"
|
|
98
|
+
cmd+=(--sub-wallets-dir "$sub_wallets_dir")
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
|
|
102
|
+
echo "run-walrus-client-service: starting walrus ${MODE} on ${BIND_ADDRESS}"
|
|
103
|
+
exec "${cmd[@]}" "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mysten-incubation/devstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Next-generation Sui devstack package.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sui",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"@mysten/codegen": "^0.11.0",
|
|
81
81
|
"@mysten/deepbook-v3": "^1.5.0",
|
|
82
82
|
"@mysten/sui": "^2.18.0",
|
|
83
|
+
"@mysten/walrus": "^1.2.0",
|
|
83
84
|
"@pothos/core": "^4.3.0",
|
|
84
85
|
"@pothos/plugin-relay": "^4.4.0",
|
|
85
86
|
"@pothos/plugin-simple-objects": "^4.1.0",
|
|
@@ -94,7 +95,6 @@
|
|
|
94
95
|
"devDependencies": {
|
|
95
96
|
"@effect/vitest": "4.0.0-beta.65",
|
|
96
97
|
"@mysten/seal": "^1.2.0",
|
|
97
|
-
"@mysten/walrus": "^1.2.0",
|
|
98
98
|
"@types/node": "^25.9.3",
|
|
99
99
|
"@types/react": "^19.2.17",
|
|
100
100
|
"ink-testing-library": "^4.0.0",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { acquireLive } from "./mode/live.mjs";
|
|
2
|
-
import { acquireForkKnown } from "./mode/fork-known.mjs";
|
|
3
|
-
import "effect";
|
|
4
|
-
//#region src/plugins/seal/service.ts
|
|
5
|
-
/** Dispatch on the known-mode discriminator. The publisher argument
|
|
6
|
-
* is unused by the known paths (no on-chain artifact produce); it's
|
|
7
|
-
* accepted for shape uniformity with the local-keygen entry point. */
|
|
8
|
-
const bootSealService = (_publisher, opts) => {
|
|
9
|
-
switch (opts.mode) {
|
|
10
|
-
case "live": return acquireLive(opts);
|
|
11
|
-
case "fork-known": return acquireForkKnown(opts);
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
//#endregion
|
|
15
|
-
export { bootSealService };
|
|
16
|
-
|
|
17
|
-
//# sourceMappingURL=service.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":[],"sources":["../../../src/plugins/seal/service.ts"],"sourcesContent":["// Seal plugin — known-mode dispatch (live + fork-known).\n//\n// Local-keygen mode is dispatched directly from the plugin barrel\n// (`index.ts`) via `bootLocalKeygen` because it needs a\n// `LocalKeygenDeps` bundle (ContainerRuntime, paths, identity, …)\n// composed from substrate services. This file only routes the two\n// pure-value-producing modes that need no substrate plumbing.\n\nimport { Effect, type Scope } from 'effect';\n\nimport type {\n\tArtifactPublishError,\n\tArtifactPublisher,\n} from '../../primitives/artifact-publisher.ts';\nimport type { SealAnyError } from './errors.ts';\nimport { acquireForkKnown, type ForkKnownInputs } from './mode/fork-known.ts';\nimport { acquireLive } from './mode/live.ts';\nimport type { SealKnownResolved } from './registry-publish.ts';\n\n/** Known-mode discriminator — live + fork-known only. */\nexport type SealMode =\n\t| {\n\t\t\treadonly mode: 'live';\n\t\t\treadonly name: string;\n\t\t\treadonly resolved: { readonly objectId: string; readonly keyServerUrl: string };\n\t }\n\t| ({ readonly mode: 'fork-known' } & ForkKnownInputs);\n\nexport type SealKnownBootResult = SealKnownResolved;\n\n/** Dispatch on the known-mode discriminator. The publisher argument\n * is unused by the known paths (no on-chain artifact produce); it's\n * accepted for shape uniformity with the local-keygen entry point. */\nexport const bootSealService = (\n\t_publisher: ArtifactPublisher,\n\topts: SealMode,\n): Effect.Effect<SealKnownBootResult, SealAnyError | ArtifactPublishError, Scope.Scope> => {\n\tswitch (opts.mode) {\n\t\tcase 'live':\n\t\t\treturn acquireLive(opts);\n\t\tcase 'fork-known':\n\t\t\treturn acquireForkKnown(opts);\n\t}\n};\n"],"mappings":";;;;;;;AAiCA,MAAa,mBACZ,YACA,SAC0F;CAC1F,QAAQ,KAAK,MAAb;EACC,KAAK,QACJ,OAAO,YAAY,IAAI;EACxB,KAAK,cACJ,OAAO,iBAAiB,IAAI;CAC9B;AACD"}
|