Files
pezkuwi-wallet-utils/scripts/light_client_specs.py
T
SatoshiQaziMuhammed eb00e49a52 Add light client chain specifications for Zagros (#78)
light-client/zagros.json and light-client/people-zagros.json are what the
wallet's embedded light client starts from.

The full specifications carry the raw genesis storage, about 3 MB each.
These carry only the genesis state root, which identifies the chain:
- The relay specification adds a checkpoint (lightSyncState) exported by
  the relay node through sync_state_genSyncSpec. It is 7.3 KB.
- People has no checkpoint, because the relay chain's finality covers it.
  It is 732 bytes.

Boot nodes put the /wss addresses on 443 first and keep the plain TCP ones.

scripts/light_client_specs.py writes both files. It checks every state root
by rebuilding the genesis header and comparing its hash with the chain's
genesis hash. A root with one byte changed is refused.

The checkpoint is a trust anchor: a client that starts from a false one can
follow a false chain. It reaches the wallet inside the APK or through
signed/targets.json, never from peers. Targets are re-signed (version 3,
689 files).

The secret scan lets through the four public chain-data keys these files
carry: stateRootHash, finalizedBlockHeader, babeEpochChanges and
grandpaAuthoritySet.
2026-11-03 00:00:54 +04:00

113 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Chain specifications for the wallet's light client, written to light-client/.
The full specifications carry the raw genesis storage, about 3 MB each. The light client needs
none of it: the genesis state root identifies the chain, and the relay chain starts from a
checkpoint (lightSyncState) that the relay node exports with sync_state_genSyncSpec. What is
written here is that trimmed form, with the boot nodes below.
The checkpoint is a trust anchor: a client that starts from a false one can follow a false
chain. It is taken from our own relay node and reaches the wallet only inside the APK or through
signed/targets.json, never from peers. Every state root is checked against its chain's genesis
hash before anything is written.
light_client_specs.py --relay-rpc URL --people-rpc URL --people-spec FILE
"""
import argparse
import hashlib
import json
import sys
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
OUT = REPO / "light-client"
# Secure WebSockets on 443 first: they look like any HTTPS WebSocket where plain libp2p TCP is
# filtered. The TCP addresses stay for networks that filter nothing.
ZAGROS_BOOT_NODES = [
"/dns/zagros-bootnode-0.pezkuwichain.io/tcp/443/wss/p2p/12D3KooWHKMr39ubvsigZXdVJzr7nnMp5YTzMywKuTzPDo3Xf1tZ",
"/dns/zagros-bootnode-1.pezkuwichain.io/tcp/443/wss/p2p/12D3KooWH6tzPuoFeyf8n6gNmus9Q3CdEGEVUvbduUnPTqJcLRLC",
"/dns/zagros-bootnode-0.pezkuwichain.io/tcp/30333/p2p/12D3KooWHKMr39ubvsigZXdVJzr7nnMp5YTzMywKuTzPDo3Xf1tZ",
"/dns/zagros-bootnode-1.pezkuwichain.io/tcp/30333/p2p/12D3KooWH6tzPuoFeyf8n6gNmus9Q3CdEGEVUvbduUnPTqJcLRLC",
]
PEOPLE_ZAGROS_BOOT_NODES = [
"/dns/zagros-people-bootnode.pezkuwichain.io/tcp/443/wss/p2p/12D3KooWLnDS5p5bvMzfco2BR4arKRbtpqoZwRvLKgCdzgVpuEQG",
"/ip4/217.77.6.126/tcp/30535/p2p/12D3KooWLnDS5p5bvMzfco2BR4arKRbtpqoZwRvLKgCdzgVpuEQG",
"/ip4/164.68.110.49/tcp/30535/p2p/12D3KooW9rHfLv4za95C4fPGTUzNk531zRrjaXYJY3pijMEYd3xB",
]
def rpc(url: str, method: str, params: list):
request = urllib.request.Request(
url,
json.dumps(
{"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
).encode(),
{"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=120) as response:
reply = json.load(response)
if "error" in reply:
sys.exit(f"{url} {method}: {reply['error']}")
return reply["result"]
def genesis_state_root(url: str) -> str:
"""The genesis state root, checked by rebuilding the genesis header and hashing it."""
genesis_hash = rpc(url, "chain_getBlockHash", [0])
header = rpc(url, "chain_getHeader", [genesis_hash])
if header["digest"]["logs"]:
sys.exit(f"{url}: genesis header has digest logs; this check assumes none")
encoded = (
bytes(32) # parent hash
+ b"\x00" # compact block number 0
+ bytes.fromhex(header["stateRoot"][2:])
+ bytes.fromhex(header["extrinsicsRoot"][2:])
+ b"\x00" # empty digest
)
rebuilt = "0x" + hashlib.blake2b(encoded, digest_size=32).hexdigest()
if rebuilt != genesis_hash:
sys.exit(f"{url}: rebuilt genesis hash {rebuilt} is not {genesis_hash}")
return header["stateRoot"]
def write(name: str, spec: dict) -> None:
path = OUT / name
path.write_text(json.dumps(spec, indent=2) + "\n")
print(f"{path.relative_to(REPO)}: {path.stat().st_size} bytes")
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--relay-rpc", required=True)
parser.add_argument("--people-rpc", required=True)
parser.add_argument(
"--people-spec", required=True, help="full People chain specification"
)
args = parser.parse_args()
OUT.mkdir(exist_ok=True)
relay = rpc(args.relay_rpc, "sync_state_genSyncSpec", [True])
if not relay.get("lightSyncState"):
sys.exit(f"{args.relay_rpc}: the exported specification has no checkpoint")
relay["genesis"] = {"stateRootHash": genesis_state_root(args.relay_rpc)}
relay["bootNodes"] = ZAGROS_BOOT_NODES
# Teyrchains have no checkpoint: the relay chain's finality covers them.
people = json.loads(Path(args.people_spec).read_text())
if people.get("relay_chain") != relay["id"]:
sys.exit(
f"People names relay chain {people.get('relay_chain')!r}, not {relay['id']!r}"
)
people["genesis"] = {"stateRootHash": genesis_state_root(args.people_rpc)}
people["bootNodes"] = PEOPLE_ZAGROS_BOOT_NODES
write("zagros.json", relay)
write("people-zagros.json", people)
if __name__ == "__main__":
main()