Fail the weekly run when the light client checkpoint is stale (#79)

The wallet now refreshes its light client specifications from this repository through the signed
config, so a wallet that never fell back no longer starts from the checkpoint its APK was built with.
That only helps if the checkpoint here is kept fresh.

light_client_specs.py --check-age DAYS reads the relay checkpoint's block number from light-client/,
compares it with the live head, and fails past DAYS. Code Quality runs it weekly with 60 days. The
README asks for the specifications to be regenerated before signing.
This commit is contained in:
SatoshiQaziMuhammed authored and GitHub committed 2026-11-03 08:09:44 +04:00
1 parent eb00e49a52
commit 35feb5afe0
3 files changed
+57 -4

No files matched your search

+5
View File
@@ -88,6 +88,11 @@ jobs:
- name: Targets signed and matching every served file
run: python3 scripts/signed_config.py check
# A wallet that never fell back to its light client has no saved state, so its first fallback
# starts from this checkpoint; refreshed specifications reach wallets through the signed config.
- name: Light client checkpoint not stale
run: python3 scripts/light_client_specs.py --relay-rpc https://zagros-rpc.pezkuwichain.io --check-age 60
chain-integrity:
name: Chain Config Integrity
runs-on: ubuntu-latest
+9
View File
@@ -66,6 +66,15 @@ served JSON file is re-signed in the same pull request:
python3 scripts/signed_config.py sign --key <targets seed>
```
The light client specifications in `light-client/` carry a relay chain checkpoint. Wallets fetch them through
the signed config and keep a later checkpoint than their APK's, so refresh them before signing:
```
python3 scripts/light_client_specs.py --relay-rpc <zagros rpc> --people-rpc <people rpc> --people-spec <full People spec>
```
Code Quality fails once the checkpoint is more than 60 days old.
Code Quality refuses a tree whose files do not match the signed list, and its weekly run fails a month
before a signature expires.
+43 -4
View File
@@ -12,6 +12,11 @@ signed/targets.json, never from peers. Every state root is checked against its c
hash before anything is written.
light_client_specs.py --relay-rpc URL --people-rpc URL --people-spec FILE
light_client_specs.py --relay-rpc URL --check-age DAYS
The second form fails when the relay checkpoint in light-client/ is older than DAYS. A wallet that
never fell back has no saved database, so its first fallback warp-syncs from this checkpoint; the
weekly Code Quality run keeps it from going stale between signings.
"""
import argparse
@@ -22,6 +27,8 @@ import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
# Zagros relay block time, to turn a block distance into days.
BLOCK_SECONDS = 6
OUT = REPO / "light-client"
# Secure WebSockets on 443 first: they look like any HTTPS WebSocket where plain libp2p TCP is
@@ -73,6 +80,32 @@ def genesis_state_root(url: str) -> str:
return header["stateRoot"]
def checkpoint_number(spec: dict) -> int:
"""Block number of the checkpoint header: SCALE, a 32-byte parent hash then a compact number."""
header = bytes.fromhex(spec["lightSyncState"]["finalizedBlockHeader"][2:])
first = header[32]
mode = first & 0b11
if mode == 0b00:
return first >> 2
if mode == 0b01:
return int.from_bytes(header[32:34], "little") >> 2
if mode == 0b10:
return int.from_bytes(header[32:36], "little") >> 2
return int.from_bytes(header[33 : 33 + (first >> 2) + 4], "little")
def check_age(relay_rpc: str, max_days: int) -> None:
spec = json.loads((OUT / "zagros.json").read_text())
checkpoint = checkpoint_number(spec)
head = int(rpc(relay_rpc, "chain_getHeader", [])["number"], 16)
days = (head - checkpoint) * BLOCK_SECONDS / 86400
print(f"relay checkpoint #{checkpoint}, head #{head}: {days:.1f} days old")
if days > max_days:
sys.exit(
f"the relay checkpoint is older than {max_days} days: run this script and re-sign"
)
def write(name: str, spec: dict) -> None:
path = OUT / name
path.write_text(json.dumps(spec, indent=2) + "\n")
@@ -82,11 +115,17 @@ def write(name: str, spec: dict) -> None:
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"
)
parser.add_argument("--people-rpc")
parser.add_argument("--people-spec", help="full People chain specification")
parser.add_argument("--check-age", type=int, metavar="DAYS")
args = parser.parse_args()
if args.check_age is not None:
check_age(args.relay_rpc, args.check_age)
return
if not (args.people_rpc and args.people_spec):
parser.error(
"--people-rpc and --people-spec are required to write the specifications"
)
OUT.mkdir(exist_ok=True)
relay = rpc(args.relay_rpc, "sync_state_genSyncSpec", [True])