Skip to content

Commit ba1619c

Browse files
committed
chore: A tool for launch validator nodes
1 parent e704244 commit ba1619c

3 files changed

Lines changed: 214 additions & 0 deletions

File tree

scripts/launch-help/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.env*
2+
data
3+
docker-compose.yml

scripts/launch-help/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## What
2+
This script is used to quickly configure and launch validator nodes.
3+
4+
## How to use
5+
### Prerequisites
6+
1. Docker 22+
7+
2. Python 3.10+
8+
3. uv 0.7+ (install: `curl -LsSf https://astral.sh/uv/install.sh | sh`)
9+
10+
### Show usage
11+
`uv run ces-launch.py --help`
12+
13+
### Steps
14+
1. Prepare the node-key and wallet private key (i.e., mnemonic) required for the validator nodes based on the `example.env` file, and rename it to `.env`.
15+
2. Generate `docker-compose.yml`: `uv run ces-launch.py gen`
16+
3. Insert related keys: `uv run ces-launch.py key-insert`
17+
4. Launch validator nodes: `uv run ces-launch.py run`

scripts/launch-help/ces-launch.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env python3
2+
# /// script
3+
# requires-python = ">=3.10"
4+
# dependencies = [
5+
# "dotenv",
6+
# "pyyaml",
7+
# ]
8+
# ///
9+
import argparse
10+
import os
11+
import sys
12+
import subprocess
13+
import yaml
14+
from dotenv import load_dotenv
15+
16+
17+
def gen_compose(args):
18+
# 1. Parse arguments
19+
chain = args.chain
20+
inst = args.inst
21+
data_dir = os.path.abspath(args.data_dir)
22+
p2p_port = args.p2p_port
23+
rpc_port = args.rpc_port
24+
25+
# 3. Generate docker-compose.yml
26+
services = {}
27+
for i in range(1, inst + 1):
28+
container_name = f"{chain}-n{i}"
29+
service = {
30+
"image": f"cesslab/cess-chain:{chain}",
31+
"network_mode": "host",
32+
"volumes": [f"{data_dir}/n{i}:/opt/cess/data"],
33+
"command": [
34+
"--base-path",
35+
"/opt/cess/data",
36+
"--chain",
37+
f"{chain}",
38+
"--port",
39+
str(p2p_port + i - 1),
40+
"--name",
41+
f"{chain.upper()}-N{i}",
42+
"--validator",
43+
"--rpc-port",
44+
str(rpc_port + i - 1),
45+
"--pruning",
46+
"archive",
47+
"--node-key",
48+
f"${{N{i}_NODE_KEY}}",
49+
"--no-telemetry",
50+
"--no-prometheus",
51+
"--no-hardware-benchmarks",
52+
],
53+
"logging": {
54+
"driver": "json-file",
55+
"options": {"max-size": "300m", "max-file": "10"},
56+
},
57+
"container_name": container_name,
58+
"environment": ["RUST_LOG=info", "RUST_BACKTRACE=full"],
59+
}
60+
# Add special parameters to the second instance
61+
if i == 2:
62+
service["command"] += [
63+
"--wasm-execution",
64+
"compiled",
65+
"--rpc-methods",
66+
"unsafe",
67+
"--rpc-external",
68+
"--rpc-cors",
69+
"all",
70+
]
71+
services[container_name] = service
72+
73+
compose = {"services": services}
74+
with open("docker-compose.yml", "w") as f:
75+
yaml.dump(compose, f, default_flow_style=False, sort_keys=False)
76+
77+
print("Generated docker-compose.yml")
78+
79+
80+
def get_chain_from_command(command_list):
81+
try:
82+
idx = command_list.index("--chain")
83+
return command_list[idx + 1]
84+
except (ValueError, IndexError):
85+
return None
86+
87+
88+
def key_insert(args):
89+
compose_file = "docker-compose.yml"
90+
# 1. Create container instances
91+
# fmt: off
92+
subprocess.run(
93+
["docker", "compose", "-f", compose_file, "--env-file", ".env", "create"],
94+
check=True,
95+
)
96+
# fmt: on
97+
load_dotenv()
98+
# 2. Read the compose file
99+
with open(compose_file) as f:
100+
compose = yaml.safe_load(f)
101+
for svc_name in compose["services"]:
102+
idx = svc_name.split("-n")[-1]
103+
chain = get_chain_from_command(compose["services"][svc_name]["command"])
104+
mnemonic = os.environ.get(f"N{idx}_MNEMONIC")
105+
shell_cmd = f"""
106+
docker compose -f {compose_file} run --rm {svc_name} key insert --base-path /opt/cess/data --chain {chain} --scheme Sr25519 --key-type babe --suri "{mnemonic}" && \
107+
docker compose -f {compose_file} run --rm {svc_name} key insert --base-path /opt/cess/data --chain {chain} --scheme Ed25519 --key-type gran --suri "{mnemonic}"
108+
"""
109+
subprocess.run(shell_cmd, shell=True, check=True)
110+
print("Keys inserted.")
111+
112+
113+
def run(args):
114+
# 1. Check if docker-compose.yml exists
115+
if not os.path.exists("docker-compose.yml"):
116+
print("docker-compose.yml not found.")
117+
sys.exit(1)
118+
# 2. Check docker compose config
119+
try:
120+
subprocess.run(
121+
["docker", "compose", "config"],
122+
check=True,
123+
stdout=subprocess.PIPE,
124+
stderr=subprocess.PIPE,
125+
)
126+
except subprocess.CalledProcessError:
127+
print("docker compose config failed.")
128+
sys.exit(1)
129+
# 3. Check the keystore directory
130+
with open("docker-compose.yml") as f:
131+
compose = yaml.safe_load(f)
132+
for svc_name in compose["services"]:
133+
chain = get_chain_from_command(compose["services"][svc_name]["command"])
134+
# Check if the keystore directory exists inside the container
135+
# fmt: off
136+
check_cmd = [
137+
"docker", "compose", "run", "--rm",
138+
"--entrypoint", "sh", svc_name,
139+
"-c", f"test -e /opt/cess/data/chains/cess-{chain}/keystore"
140+
]
141+
# fmt: on
142+
result = subprocess.run(check_cmd)
143+
if result.returncode != 0:
144+
print(
145+
f"[{svc_name}] keystore directory does not exist, please run the key-insert command first."
146+
)
147+
sys.exit(1)
148+
# 4. Start the network
149+
subprocess.run(["docker", "compose", "start"], check=True)
150+
print("CESS Validator started.")
151+
152+
153+
def main():
154+
parser = argparse.ArgumentParser(description="CESS Chain Validator Launch Tool")
155+
subparsers = parser.add_subparsers(dest="cmd", required=True)
156+
157+
# gen
158+
p_gen = subparsers.add_parser("gen", help="Generate docker-compose.yml")
159+
p_gen.add_argument(
160+
"--chain",
161+
choices=["testnet2", "testnet", "devnet"],
162+
default="devnet",
163+
help="CESS chain-specification, default: devnet",
164+
)
165+
p_gen.add_argument(
166+
"--inst", type=int, default=2, help="validator instance counts, default: 2"
167+
)
168+
p_gen.add_argument(
169+
"--data-dir", default="./data", help="data directory, default: ./data"
170+
)
171+
p_gen.add_argument(
172+
"--p2p-port", type=int, default=30333, help="p2p port, default: 30333"
173+
)
174+
p_gen.add_argument(
175+
"--rpc-port", type=int, default=9944, help="rpc port, default: 9944"
176+
)
177+
p_gen.set_defaults(func=gen_compose)
178+
179+
# key-insert
180+
p_key = subparsers.add_parser(
181+
"key-insert", help="Insert related keys into validator nodes"
182+
)
183+
p_key.set_defaults(func=key_insert)
184+
185+
# run
186+
p_run = subparsers.add_parser("run", help="Run CESS Chain validator nodes")
187+
p_run.set_defaults(func=run)
188+
189+
args = parser.parse_args()
190+
args.func(args)
191+
192+
193+
if __name__ == "__main__":
194+
main()

0 commit comments

Comments
 (0)