Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions deploy_tee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ for its [InitConfig](https://github.com/SeismicSystems/enclave/blob/seismic/crat
never provisions; it consumes a descriptor of an already-running node.
VHD upload to the Azure image registry is a seismic-images concern
(`make push-azure-*`), not this tool.
- **For `seismic-tee-bootstrap genesis`:** a built `summit`
(`$HOME/summit`, for the `genesis` binary).
- **For `seismic-tee-bootstrap genesis`:** the `genesis` binary (built from
summit) on PATH, plus a summit genesis template passed via
`--summit-template`.
- **For `seismic-tee-bootstrap up`** (cohort provisioning): the `pulumi`
CLI on PATH. The passphrase secrets provider needs
`PULUMI_CONFIG_PASSPHRASE` — set it, or let `up`/`down` prompt you on
Expand Down Expand Up @@ -143,8 +144,9 @@ uv run seismic-tee configure --node /tmp/dev-bootstrap-node-1.json --config ./no
uv run seismic-tee configure --node /tmp/dev-bootstrap-node-2.json --config ./node-2.toml --manifest ./network-manifest.json

# 4. Run the genesis ceremony once: builds genesis.toml from the cohort
# and fans it out to every summit.
uv run seismic-tee-bootstrap genesis --node /tmp/dev-bootstrap-node-1.json /tmp/dev-bootstrap-node-2.json
# (needs the `genesis` binary on PATH) and fans it out to every summit.
uv run seismic-tee-bootstrap genesis --summit-template ./summit-genesis-template.toml \
--node /tmp/dev-bootstrap-node-1.json /tmp/dev-bootstrap-node-2.json
```

After step 3 each node is up; after step 4 they produce blocks. RPC is at
Expand Down
50 changes: 35 additions & 15 deletions deploy_tee/genesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import argparse
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
Expand Down Expand Up @@ -59,19 +60,27 @@ def _parse_args() -> argparse.Namespace:
),
)
parser.add_argument(
"--code-path",
default="",
type=str,
help="path to code relative to $HOME",
"--summit-template",
type=Path,
required=True,
metavar="FILE",
help=(
"Summit genesis template TOML (network-params without "
"[[validators]]) — the same file fed to `manifest assemble`. The "
"`genesis` binary fills in the cohort's validators."
),
)
parser.add_argument(
"-g",
"--genesis-hash",
type=str,
default=None,
help="Eth genesis hash",
help="Eth genesis hash; overrides the template's. Pin to reth's actual hash.",
)
return parser.parse_args()
args = parser.parse_args()
if not args.summit_template.is_file():
raise SystemExit(f"--summit-template not found: {args.summit_template}")
return args


def _get_pubkeys(
Expand Down Expand Up @@ -115,13 +124,18 @@ def main():
args = _parse_args()
genesis_arg = ["-g", args.genesis_hash] if args.genesis_hash else []

tmpdir = tempfile.mkdtemp()
home = Path.home() if not args.code_path else Path.home() / args.code_path

summit_path = str(home / "summit")
summit_genesis_target = f"{summit_path}/target/debug/genesis"
summit_example_genesis = f"{summit_path}/example_genesis.toml"
# `genesis` is summit's binary; expect it on PATH (build summit and symlink
# its target/debug/genesis onto PATH, the same way summit expects `reth`).
# Fail with a clear message instead of a subprocess FileNotFoundError.
genesis_bin = shutil.which("genesis")
if genesis_bin is None:
raise SystemExit(
"`genesis` binary not found on PATH. Build summit and put its "
"`genesis` binary on PATH, e.g. "
"`ln -s <summit>/target/debug/genesis ~/.cargo/bin/genesis`."
)

tmpdir = tempfile.mkdtemp()
validators, node_clients = _get_pubkeys(args.node)

tmp_validators = f"{tmpdir}/validators.json"
Expand All @@ -132,20 +146,26 @@ def main():
# Output streams to the terminal (check=True raises on failure).
subprocess.run(
[
summit_genesis_target,
genesis_bin,
"-o",
tmpdir,
"-i",
summit_example_genesis,
str(args.summit_template),
"-v",
tmp_validators,
*genesis_arg,
],
check=True,
)

# Log the built genesis's path (not its contents) before delivery, mirroring
# how `configure` logs the merged node config it POSTs.
genesis_path = Path(f"{tmpdir}/genesis.toml")
print(f"Built genesis -> {genesis_path}")

for _, client in node_clients:
client.post_genesis_filepath(Path(f"{tmpdir}/genesis.toml"))
print(f"Sending genesis to {client.url}")
client.post_genesis_filepath(genesis_path)


if __name__ == "__main__":
Expand Down
16 changes: 13 additions & 3 deletions deploy_tee/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ def format_provisioning(status: dict) -> str:
done = status.get("bytes_done", 0)
total = status.get("bytes_total", 0)
if not total:
return "provisioning: starting (no measurement yet)"
return "encrypting disk: starting (no measurement yet)"
pct = 100.0 * done / total
line = f"{_bar(pct)} {pct:5.1f}% {_gib(done)}/{_gib(total)} GiB"
line = f"encrypting disk {_bar(pct)} {pct:5.1f}% {_gib(done)}/{_gib(total)} GiB"
eta = status.get("eta_seconds")
if eta:
line += f" eta {_duration(eta)}"
Expand Down Expand Up @@ -142,7 +142,17 @@ def emit(line: str, *, transient: bool) -> None:
state = status.get("state")

if state == "provisioning":
seen_provisioning = True
if not seen_provisioning:
seen_provisioning = True
# One-time context: the bar alone doesn't say what's happening,
# and this phase is long enough to look like a hang.
emit(
"First-boot: initializing the encrypted /persistent disk "
"— a one-time full-disk wipe (LUKS + dm-integrity) that can "
"take 1h+ on large disks. The node must finish this before "
"it can boot further.",
transient=False,
)
emit(format_provisioning(status), transient=True)
elif state == "error":
emit(
Expand Down
49 changes: 49 additions & 0 deletions deploy_tee/tests/test_summit_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Tests for deploy_tee.utils.summit_client (stdlib unittest; no test deps).

Run with:
uv run python -m unittest discover -s deploy_tee/tests -v
"""

import unittest
from unittest import mock

from deploy_tee.utils import summit_client
from deploy_tee.utils.summit_client import SummitClient


class SummitClientTests(unittest.TestCase):
def _resp(self, body: dict):
r = mock.Mock()
r.raise_for_status.return_value = None
r.json.return_value = body
return r

def _patch(self, resp):
return mock.patch.object(summit_client.requests, "post", return_value=resp)

def test_get_public_keys_posts_jsonrpc_and_parses(self):
result = {"node": "n", "consensus": "c"}
resp = self._resp({"jsonrpc": "2.0", "id": 1, "result": result})
with self._patch(resp) as post:
keys = SummitClient("https://x/summit").get_public_keys()
self.assertEqual((keys.node, keys.consensus), ("n", "c"))
# POSTs a JSON-RPC envelope to the /summit base, camelCase method.
self.assertEqual(post.call_args[0][0], "https://x/summit")
self.assertEqual(post.call_args[1]["json"]["method"], "getPublicKeys")

def test_send_genesis_posts_content_as_param(self):
resp = self._resp({"jsonrpc": "2.0", "id": 1, "result": "ok"})
with self._patch(resp) as post:
SummitClient("https://x/summit").send_genesis("foo = 1\n")
self.assertEqual(post.call_args[1]["json"]["method"], "sendGenesis")
self.assertEqual(post.call_args[1]["json"]["params"], ["foo = 1\n"])

def test_rpc_error_raises(self):
resp = self._resp({"jsonrpc": "2.0", "id": 1, "error": {"message": "boom"}})
with self._patch(resp):
with self.assertRaises(RuntimeError):
SummitClient("https://x/summit").get_public_keys()


if __name__ == "__main__":
unittest.main()
43 changes: 20 additions & 23 deletions deploy_tee/utils/summit_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,42 @@ class PublicKeys:


class SummitClient:
def __init__(self, url: str):
self.url = url
"""Client for a node's summit RPC.

def _get(self, path: str) -> str:
response = requests.get(f"{self.url}/{path}")
response.raise_for_status()
return response.text
Summit's RPC is jsonrpsee (JSON-RPC 2.0): a single POST endpoint, method
names in camelCase (see summit/rpc/src/api.rs). nginx proxies `/summit` →
localhost:3030 and strips the prefix, so we POST the envelope to the
`/summit` base — there is no per-method REST path or GET.
"""

def _get_json(self, path: str) -> Json:
response = requests.get(f"{self.url}/{path}")
response.raise_for_status()
return response.json()
def __init__(self, url: str):
self.url = url

def _post_text(self, path: str, body: str) -> str:
def _rpc(self, method: str, params: list | None = None) -> Json:
response = requests.post(
f"{self.url}/{path}",
data=body,
headers={"Content-Type": "text/plain"},
self.url,
json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []},
timeout=30,
)
response.raise_for_status()
return response.text
data = response.json()
if "error" in data:
raise RuntimeError(f"summit RPC {method} failed: {data['error']}")
return data["result"]

def health(self) -> str:
return self._get("health")
return self._rpc("health")

def get_public_keys(self) -> PublicKeys:
keys = self._get_json("get_public_keys")
keys = self._rpc("getPublicKeys")
return PublicKeys(node=keys["node"], consensus=keys["consensus"])

def send_share(self, share: str) -> str:
return self._post_text("send_share", share)

def send_genesis(self, genesis: GenesisText) -> str:
self.validate_genesis_text(genesis)
return self._post_text("send_genesis", genesis)
return self._rpc("sendGenesis", [genesis])

def post_genesis_filepath(self, path: Path):
text = self.load_genesis_file(path)
self.send_genesis(text)
self.send_genesis(self.load_genesis_file(path))

@staticmethod
def load_genesis_file(path: Path) -> GenesisText:
Expand Down
Loading