From 73a6d533686b99e1207b0228d2ab26828623e675 Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:59:10 +0000 Subject: [PATCH 01/10] feat: gpt-oss-120B at ~40 tok/s over WAN (pipelined + 3-stage ring + in-region coordinator) Pipelined async-draft coordinator (specpipe.coordinate_pipe) takes gpt-oss-120B from a latency-bound ~18 to ~40 tok/s (peak ~42), greedy/exact, over WAN on 3 scattered RTX 4090s + a coordinator. Two structural wins beyond pipelining: a 3-stage 12-layer ring (4 WAN hops not 5, fits a 24GB card) and placing the layer-less coordinator in-region (cut the ring 174->102ms). Also lands a fixed-topology tree-verify path (fastverify.tree_decode, validated bit-exact) kept as groundwork. launch_oss.py = the swarm launcher. Verifiable receipt: docs/receipts/gpt-oss-120b-wan-20260619.json (4 distinct GPU UUIDs / IPs / US states, WAN edge RTTs, output token hash, sync==pipelined token match). This is the build target for the permissionless Phases 3-6. --- README.md | 32 ++- docs/PROOF.md | 5 +- docs/ROADMAP.md | 2 +- docs/receipts/gpt-oss-120b-wan-20260619.json | 263 +++++++++++++++++++ phase0/fastverify.py | 73 +++++ phase0/launch_oss.py | 183 +++++++++++++ phase0/specpipe.py | 225 +++++++++++++++- research/draft_server.py | 9 +- research/treeverify_probe.py | 43 +++ 9 files changed, 815 insertions(+), 20 deletions(-) create mode 100644 docs/receipts/gpt-oss-120b-wan-20260619.json create mode 100644 phase0/launch_oss.py create mode 100644 research/treeverify_probe.py diff --git a/README.md b/README.md index bc8b7a1..d1d9ffa 100644 --- a/README.md +++ b/README.md @@ -115,14 +115,30 @@ Shard is c0mpute infrastructure, held to its three guarantees: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). It is the number-one open problem and is treated as one. -## Earlier milestone: gpt-oss-120B at ~18–25 tok/s over WAN - -The first Phase-2 result: **120B across 4× RTX 4090 in different US states, ~18–25 -tok/s, exact** — same playbook (speculative decode + a CUDA-graph fast verify), on a -smaller model. GLM-5.2 (above) is the current flagship: 6× the parameters, faster, -on a longer scattered ring. Full design record: -[docs/research/wan-speculative-decoding.md](docs/research/wan-speculative-decoding.md) -and [docs/research/glm-5.2-on-consumer-blackwell.md](docs/research/glm-5.2-on-consumer-blackwell.md). +## gpt-oss-120B at ~40 tok/s over WAN — the permissionless build target + +120B (MXFP4, 36 layers) across **3 scattered RTX 4090s in different US states** + a +coordinator, **~40 tok/s (peak ~42), greedy, exact**. This is the rig the permissionless +work (Phase 3+) is built on — plain 24GB consumer cards, the hardware a real volunteer runs. +This run's verifiable receipt (distinct GPU UUIDs / IPs / states, WAN edge RTTs, output +hash, sync-vs-pipelined token match): [`docs/receipts/gpt-oss-120b-wan-20260619.json`](docs/receipts/gpt-oss-120b-wan-20260619.json). + +The climb from a latency-bound ~18 tok/s, each step measured: + +| Step | tok/s | What changed | +|------|-------|--------------| +| pipelined spec-decode (4-stage) | 25.8 | async-draft overlap + many verify chunks in flight + RTT-optimal ring order | +| + **3-stage (12-layer) ring** | 28.8 | fatter stages → 4 WAN hops instead of 5 (12 layers fits a 24GB card) | +| + **coordinator placed in-region** | **~40 (peak ~42)** | the coordinator holds no model layers, so it can live anywhere; moving it off the cross-country leg cut the ring 174→102 ms | + +The last step is the one nobody looks for: the cheapest node in the system — the +layer-less coordinator — was sitting a continent away from the swarm, paying two long +round-trips on every token. Putting it next to the stages, on the same scattered nodes, +was a ~40% latency cut for free. Full record: +[docs/research/wan-speculative-decoding.md](docs/research/wan-speculative-decoding.md). + +GLM-5.2 (above) remains the **frontier-size** flagship — 6× the parameters at 744B; +gpt-oss-120B is the faster, consumer-card build target the network is bootstrapped on. ## Repository layout diff --git a/docs/PROOF.md b/docs/PROOF.md index 84b1c16..5ee7efd 100644 --- a/docs/PROOF.md +++ b/docs/PROOF.md @@ -79,8 +79,9 @@ skeptic checks the engine file against). `reference.token_ids` lets anyone re-ru - **GLM-5.2 744B NVFP4 at ~30 tok/s over WAN** — 7 GPUs in 6 US states, pipelined spec-decode + CUDA-graphed draft: [`receipts/glm52-nvfp4-wan-20260618.json`](receipts/glm52-nvfp4-wan-20260618.json). -- **gpt-oss-120B at ~18–25 tok/s over WAN** — the earlier Phase-2 result - (see [README](../README.md) and [research log](research/wan-speculative-decoding.md)). +- **gpt-oss-120B at ~40 tok/s over WAN** — 3 stages (12 layers each) + an in-region coordinator + across 4 US states, pipelined spec-decode; the permissionless build target: + [`receipts/gpt-oss-120b-wan-20260619.json`](receipts/gpt-oss-120b-wan-20260619.json). ## Scope / honesty diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 18fb322..765f43f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -43,7 +43,7 @@ The payoff. Add the draft-verify loop. The permissionless layer is built and proven **first on gpt-oss-120B across four RTX 4090s**, then generalized up. That rig is what a real volunteer's hardware actually looks like (24GB consumer cards, not 96GB Blackwell), it's the most practical swarm a stranger can join today, and it's the cheapest to iterate on Vast. The GLM-5.2 744B run (~30 tok/s, 7 GPUs) already proved the performance ceiling and that the engine scales up; the 120B/4090 setup is the development target for everything below. Once the permissionless stack works there, it carries up to the 744B class unchanged. -**Foundation already proven (Phases 0-2).** Owned authenticated + encrypted transport, fail-fast edge supervision, pipelined speculative decoding, and the split itself: gpt-oss-120B at **9 layers/node across 4× RTX 4090** (~16GB each, no single 24GB card holds it) over WAN via `phase0/pipeline.py` (~18-25 tok/s with spec-decode; ~3.5 tok/s plain across Washington ↔ Quebec, ~95 ms), and GLM-5.2 744B across 7 scattered GPUs at ~30 tok/s. What's left is turning that *hand-deployed* swarm into one that strangers can join, get paid by, can't cheat, and can't leak. That is Phases 3-6. +**Foundation already proven (Phases 0-2).** Owned authenticated + encrypted transport, fail-fast edge supervision, pipelined speculative decoding, and the split itself. gpt-oss-120B (MXFP4, 36 layers) now runs at **~40 tok/s (peak ~42), greedy/exact, over WAN on 3 scattered RTX 4090s** + a coordinator (`phase0/specpipe.py` pipelined coordinator, `phase0/launch_oss.py`) — up from a latency-bound ~18 via: async-draft pipelining, a **3-stage 12-layer ring** (4 WAN hops not 5), and **placing the layer-less coordinator in-region** (cut the ring 174→102 ms). GLM-5.2 744B runs across 7 scattered GPUs at ~30 tok/s (the frontier-size proof). What's left is turning this *hand-deployed* swarm into one that strangers can join, get paid by, can't cheat, and can't leak. That is Phases 3-6. ## Phase 3 — live & earning in c0mpute (trusted swarm) (target: ~1 week) diff --git a/docs/receipts/gpt-oss-120b-wan-20260619.json b/docs/receipts/gpt-oss-120b-wan-20260619.json new file mode 100644 index 0000000..9be0a71 --- /dev/null +++ b/docs/receipts/gpt-oss-120b-wan-20260619.json @@ -0,0 +1,263 @@ +{ + "run_id": "gpt-oss-120b-wan-20260619", + "utc": "2026-06-19T09:57:01.128915Z", + "shard_commit": "40ca820bdb2c4e772e48635d9386d67db150ed31", + "engine_file": "phase0/specpipe.py", + "engine_sha256": "59bf2a8f3189bde28148c201eac32774f207ca8c149159e599a5e227316d3ea4", + "model": "gpt-oss-120b", + "quant": "mxfp4", + "topology": "3 stages of 12 layers each + an in-region coordinator (draft only); 4-hop ring", + "prompt": "Explain decentralized computing in two sentences.", + "output_text": "analysisNeed to answer concisely: two sentences. Provide definition and key aspects.assistantfinalDecentralized computing distributes processing, storage, and decision\u2011making across a network of independent nodes rather than relying on a single central server or data center. This architecture enhances fault tolerance, scalability, and privacy by allowing each node to operate autonomously while still collaborating through consensus or peer\u2011to\u2011peer protocols.", + "output_token_ids": [ + 200005, + 35644, + 200008, + 23483, + 316, + 6052, + 4468, + 276, + 1151, + 25, + 1920, + 40536, + 13, + 51441, + 16083, + 326, + 2140, + 15712, + 13, + 200007, + 200006, + 173781, + 200005, + 17196, + 200008, + 6206, + 48026, + 2110, + 34349, + 163118, + 12323, + 11, + 7928, + 11, + 326, + 8660, + 50005, + 37891, + 5251, + 261, + 5402, + 328, + 13313, + 15749, + 7542, + 1572, + 64139, + 402, + 261, + 4590, + 11082, + 6017, + 503, + 1238, + 6170, + 13, + 1328, + 24022, + 70548, + 20795, + 46581, + 11, + 152600, + 11, + 326, + 16480, + 656, + 16246, + 2454, + 4596, + 316, + 19866, + 37793, + 10701, + 2049, + 2928, + 102752, + 1819, + 46012, + 503, + 24770, + 50005, + 935, + 50005, + 32092, + 49170, + 13 + ], + "output_sha256": "f646e0dbe2b3c695b9f495d508fbc0a8d96ba938e90eb845fda69f65c1873f70", + "tok_s_warm": 39.8, + "decode": "greedy (exact)", + "nodes": [ + { + "role": "coordinator", + "layer_range": null, + "public_ip": "136.59.129.136", + "geo": "Utah, US", + "gpu_uuid": "GPU-1eb664cd-c6f9-387c-682e-d7225edc76fb", + "gpu_name": "NVIDIA GeForce RTX 4090", + "note": "holds no 120B layers; runs the gpt-oss-20b vLLM draft + drives generation" + }, + { + "role": "stage0 (served-head)", + "layer_range": [ + 0, + 11 + ], + "public_ip": "66.114.157.224", + "geo": "California, US", + "gpu_uuid": "GPU-0abcaebf-4660-6386-eebe-3dabdbb39a73", + "gpu_name": "NVIDIA GeForce RTX 4090" + }, + { + "role": "stage1", + "layer_range": [ + 12, + 23 + ], + "public_ip": "173.239.95.142", + "geo": "Nevada, US", + "gpu_uuid": "GPU-5d73df3c-5537-1808-c8a7-90bb2cab0b53", + "gpu_name": "NVIDIA GeForce RTX 4090" + }, + { + "role": "tail", + "layer_range": [ + 24, + 35 + ], + "public_ip": "76.121.3.151", + "geo": "Washington, US", + "gpu_uuid": "GPU-3939cfdc-a999-dbfb-1c9b-512b7a97c693", + "gpu_name": "NVIDIA GeForce RTX 4090" + } + ], + "edges": [ + { + "from": "UT", + "to": "CA", + "rtt_ms": 16.0 + }, + { + "from": "CA", + "to": "NV", + "rtt_ms": 11.0 + }, + { + "from": "NV", + "to": "WA", + "rtt_ms": 40.0 + }, + { + "from": "WA", + "to": "UT", + "rtt_ms": 38.0 + } + ], + "reference": { + "source": "within-engine: synchronous vs pipelined coordinator, same engine, both greedy", + "tokens_match": true, + "token_ids": [ + 200005, + 35644, + 200008, + 23483, + 316, + 6052, + 4468, + 276, + 1151, + 25, + 1920, + 40536, + 13, + 51441, + 16083, + 326, + 2140, + 15712, + 13, + 200007, + 200006, + 173781, + 200005, + 17196, + 200008, + 6206, + 48026, + 2110, + 34349, + 163118, + 12323, + 11, + 7928, + 11, + 326, + 8660, + 50005, + 37891, + 5251, + 261, + 5402, + 328, + 13313, + 15749, + 7542, + 1572, + 64139, + 402, + 261, + 4590, + 11082, + 6017, + 503, + 1238, + 6170, + 13, + 1328, + 24022, + 70548, + 20795, + 46581, + 11, + 152600, + 11, + 326, + 16480, + 656, + 16246, + 2454, + 4596, + 316, + 19866, + 37793, + 10701, + 2049, + 2928, + 102752, + 1819, + 46012, + 503, + 24770, + 50005, + 935, + 50005, + 32092, + 49170, + 13 + ] + } +} \ No newline at end of file diff --git a/phase0/fastverify.py b/phase0/fastverify.py index cda706c..3fdb1f5 100644 --- a/phase0/fastverify.py +++ b/phase0/fastverify.py @@ -16,6 +16,7 @@ """ import torch from pipeline import _causal_mask +from tree import tree_mask class StaticKV: @@ -101,3 +102,75 @@ def decode(self, h, start): # fixed q_len, graph self._set(h, start) # warmup wrote the cache; restore round inputs self.graph.replay() return self.out + + # ---- TREE verify: a fixed-topology tree (M nodes) graphed like the linear path ---- + # build_tree(w,d) gives a FIXED structure (par/dep constant; only token values change), + # so the M-node forward is graphable. tree KV is stored at contiguous scratch slots + # [start, start+M); the tree mask routes each node to its ancestors' slots + the prefix. + def _tbuild(self, M, par, dep): + self.tM = M; self.tpar = list(par) + self.th = torch.zeros(1, M, self.hidden, dtype=torch.bfloat16, device=self.dev) + self.tpos = torch.zeros(1, M, dtype=torch.long, device=self.dev) + self.tcp = torch.zeros(M, dtype=torch.long, device=self.dev) + self.tmf = torch.zeros(1, 1, M, self.maxlen, dtype=torch.bfloat16, device=self.dev) + self.tmw = torch.zeros(1, 1, M, self.maxlen, dtype=torch.bfloat16, device=self.dev) + anc = torch.zeros(M, M, dtype=torch.bool, device=self.dev) # anc[i,tj] = tj ancestor of i (incl self) + for i in range(M): + j = i + while j != -1: + anc[i, j] = True; j = par[j] + self.tanc = anc + self.tdepv = torch.tensor(dep, device=self.dev) + + def _tset(self, h, start, par, dep): + """build the round's tree mask VECTORIZED (the topology is fixed; only `start` + shifts the prefix boundary) -- no per-element GPU writes.""" + M = self.tM + z = torch.zeros((), dtype=torch.bfloat16, device=self.dev) + mnb = torch.full((), torch.finfo(torch.bfloat16).min, dtype=torch.bfloat16, device=self.dev) + self.th.copy_(h) + posM = start + self.tdepv # [M] query abs positions + self.tpos.copy_(posM.unsqueeze(0)) + self.tcp.copy_(torch.arange(start, start + M, device=self.dev)) # contiguous scratch slots + cols = torch.arange(self.maxlen, device=self.dev) + allow_f = torch.zeros(M, self.maxlen, dtype=torch.bool, device=self.dev) + allow_f[:, :start] = True # full: attend all committed prefix + allow_f[:, start:start + M] = self.tanc # tree: attend ancestors (scratch slots) + self.tmf[0, 0] = torch.where(allow_f, z, mnb) + if self.win: + allow_w = torch.zeros(M, self.maxlen, dtype=torch.bool, device=self.dev) + allow_w[:, :start] = (posM[:, None] - cols[None, :start]) < self.win + allow_w[:, start:start + M] = self.tanc & ((posM[:, None] - posM[None, :]) < self.win) + self.tmw[0, 0] = torch.where(allow_w, z, mnb) + else: + self.tmw.copy_(self.tmf) + + def _tbody(self): + self.cache.cp = self.tcp + return self._layers(self.th, self.tpos, self.rotary(self.th, self.tpos), self.tmf, self.tmw) + + def tree_decode(self, h, start, par, dep): + if getattr(self, "tM", None) != h.shape[1] or getattr(self, "tpar", None) != list(par): + self._tbuild(h.shape[1], par, dep); self.tgraph = None + self._tset(h, start, par, dep) + if getattr(self, "tgraph", None) is None: + s = torch.cuda.Stream(); s.wait_stream(torch.cuda.current_stream()) + with torch.cuda.stream(s): + for _ in range(3): self._tbody() + torch.cuda.current_stream().wait_stream(s) + self.tgraph = torch.cuda.CUDAGraph() + with torch.cuda.graph(self.tgraph): + self.tout = self._tbody() + self._tset(h, start, par, dep) # warmup dirtied the cache; restore inputs + self.tgraph.replay() + return self.tout + + def tree_gather(self, start, keep): + """compact the accepted path's KV (scratch slots start+keep[i]) to contiguous + committed positions start+i, every layer. keep[0]=0 (root). next round's prefix + becomes [0, start+len(keep)).""" + src = torch.tensor([start + k for k in keep], device=self.dev) + dst = torch.arange(start, start + len(keep), device=self.dev) + for lk, lv in zip(self.cache.k, self.cache.v): + lk.index_copy_(2, dst, lk.index_select(2, src)) + lv.index_copy_(2, dst, lv.index_select(2, src)) diff --git a/phase0/launch_oss.py b/phase0/launch_oss.py new file mode 100644 index 0000000..870b5af --- /dev/null +++ b/phase0/launch_oss.py @@ -0,0 +1,183 @@ +"""Launcher for the gpt-oss-120B scattered-4090 swarm (specpipe fast-verify). + +Brings up the swarm and runs the SYNC baseline then the PIPELINED coordinator on the +SAME warm stages, so the two tok/s numbers are a clean A/B on one topology. + + python launch_oss.py --stages id0,id1,id2,id3 --coord COORDID \ + --prompt "Explain decentralized computing in two sentences." \ + --max-new 160 --sweep 3,4 --depth 4 + +Flow: eps -> launch tail-first (stage3=tail, stage0=served-head; all --fast --direct-return) + -> draft_server (vLLM gpt-oss-20b) on coord -> warm barrier -> coord SYNC sweep, PIPE sweep. +Teardown is manual (vastai stop/destroy). Stage order is taken as given (fixed across both runs). +""" +import sys, json, time, subprocess, argparse, concurrent.futures as cf + +KEY = "/root/.ssh/vast_c0mpute" +PSK = "d6821353a558b647c56be52f3efd9796ab3ee4f8ca77978007ed0049f8f1406a" +PORT = 29600 +DRAFT_PORT = 8200 +M120 = "/root/models/gpt-oss-120b" +M20 = "/root/models/gpt-oss-20b" +SSHO = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", + "-o", "ConnectTimeout=20", "-o", "BatchMode=yes", "-o", "ServerAliveInterval=20"] + + +def instances(): + r = subprocess.run(["vastai", "show", "instances-v1", "--raw"], capture_output=True, text=True) + o = r.stdout + try: + d = json.loads(o) # v1 raw is a JSON object/array + except Exception: + k = o.find("[") # tolerate a leading non-JSON line + d = json.loads(o[k:]) if k >= 0 else [] + lst = d if isinstance(d, list) else d.get("instances", []) + return {i["id"]: i for i in lst} + + +def ep(inst): + """public ip + mapped host port for container :29600 (cross-box reachable).""" + p = (inst.get("ports") or {}).get(f"{PORT}/tcp") + return inst["public_ipaddr"], (p[0]["HostPort"] if p else None) + + +def ssh22(inst): + p22 = (inst.get("ports") or {}).get("22/tcp") + host = inst["public_ipaddr"] if p22 else inst["ssh_host"] + port = p22[0]["HostPort"] if p22 else inst["ssh_port"] + return host, port + + +def rssh(inst, cmd, timeout=120): + h, p = ssh22(inst) + return subprocess.run(["ssh", "-i", KEY, "-p", str(p)] + SSHO + [f"root@{h}", cmd], + capture_output=True, text=True, timeout=timeout) + + +def fire(inst, cmd, timeout=25): + """launch a detached (setsid) process; tolerate the ssh not returning -- vLLM/torch + can hold the channel open via child fds, but the setsid survives and readiness is + polled separately. so a TimeoutExpired here is not a failure.""" + try: + rssh(inst, cmd, timeout) + except subprocess.TimeoutExpired: + pass + + +def launch_stage(inst, stage, nstages, nxt_ep, served_head): + is_tail = stage == nstages - 1 + nextarg = f" --next {nxt_ep}" if nxt_ep else "" + head = " --served-head" if served_head else "" + # kill prior GPU procs (NEVER pkill -f specpipe -> would kill this ssh's match); free the port + cmd = (f"nvidia-smi --query-compute-apps=pid --format=csv,noheader | xargs -r kill -9 2>/dev/null; " + f"fuser -k {PORT}/tcp 2>/dev/null; sleep 2; rm -f /root/stage.log; cd /root && " + f"SHARD_PSK={PSK} setsid bash -c 'python3 specpipe.py --stage {stage} --nstages {nstages} " + f"--model {M120} --listen-port {PORT}{nextarg}{head} --fast --direct-return " + f"--timeout 300 > /root/stage.log 2>&1' /dev/null 2>&1 &") + fire(inst, cmd) + + +def warm_stage(inst, label): + for _ in range(80): + r = rssh(inst, "grep -ciE 'listening' /root/stage.log 2>/dev/null; " + "grep -ciE 'Traceback|Error|exit status' /root/stage.log 2>/dev/null", 30) + nums = [x for x in r.stdout.split() if x.isdigit()] + if nums and nums[0] != "0": + return label, True + if len(nums) > 1 and nums[1] != "0": + tail = rssh(inst, "tail -5 /root/stage.log", 20).stdout + return f"{label} ERR: {tail[-400:]}", False + time.sleep(10) + return f"{label} TIMEOUT", False + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--stages", required=True, help="comma ids in chain order (head..tail)") + ap.add_argument("--coord", type=int, required=True) + ap.add_argument("--prompt", default="Explain decentralized computing in two sentences.") + ap.add_argument("--max-new", type=int, default=160) + ap.add_argument("--ks", default="4") + ap.add_argument("--depths", default="2,4,8") + ap.add_argument("--tree", default="", help="run FAST graphed tree spec 'w,d' instead of the linear compare") + ap.add_argument("--dump", action="store_true", help="write the warm run (ids+hash+sync-match) to /root/run.json for the receipt") + ap.add_argument("--skip-draft", action="store_true", help="reuse a draft already running on coord") + a = ap.parse_args() + + sids = [int(x) for x in a.stages.split(",")] + nstages = len(sids) + insts = instances() + coord = insts[a.coord] + stages = [insts[i] for i in sids] + try: # RTT-optimal stage order: minimize coord->s0->..->sN->coord + import itertools, launch_swarm + alln = [coord] + stages + print("[mesh] measuring all-pairs RTT to order the ring...", flush=True) + M = launch_swarm.mesh_rtt(alln) + best = border = None + for perm in itertools.permutations(range(1, len(alln))): + c = M[0][perm[0]] + sum(M[perm[i]][perm[i + 1]] for i in range(len(perm) - 1)) + M[perm[-1]][0] + if best is None or c < best: + best, border = c, perm + if best is not None and best < 9000: # all hops probed (9999 = a failed probe) + stages = [alln[i] for i in border] + print(f"[mesh] optimal loop {best:.0f}ms; order {[s.get('geolocation') for s in stages]}", flush=True) + else: + print("[mesh] probe inconclusive; keeping given order", flush=True) + except Exception as e: + print(f"[mesh] skipped ({type(e).__name__}: {e}); keeping given order", flush=True) + eps = [ep(s) for s in stages] + print(f"[topology] coord={a.coord} ({coord.get('geolocation')})", flush=True) + for k, s in enumerate(stages): + ip, hp = eps[k] + print(f" stage{k} {s['id']} ({s.get('geolocation')}) {ip}:{hp}" + f"{' [served-head]' if k == 0 else ''}{' [tail]' if k == nstages-1 else ''}", flush=True) + + if a.skip_draft: + print("[draft] --skip-draft: reusing the draft already running on coord", flush=True) + else: + print("[draft] starting vLLM gpt-oss-20b on coord (loads in parallel with stages)...", flush=True) + fire(coord, f"nvidia-smi --query-compute-apps=pid --format=csv,noheader | xargs -r kill -9 2>/dev/null; " + f"fuser -k {DRAFT_PORT}/tcp 2>/dev/null; sleep 2; rm -f /root/draft.log; cd /root && " + f"SHARD_PSK={PSK} CUDA_VISIBLE_DEVICES=0 setsid bash -c " + f"'/root/vllmenv/bin/python draft_server.py --model {M20} --port {DRAFT_PORT} " + f"> /root/draft.log 2>&1' /dev/null 2>&1 &") + + print("[launch] stages tail-first; wait each to listen before launching its predecessor " + "(120B partial load ~1-2min/node, so a predecessor never connects to a dead successor)...", flush=True) + for k in range(nstages - 1, -1, -1): + nxt = f"{eps[k+1][0]}:{eps[k+1][1]}" if k < nstages - 1 else None + launch_stage(stages[k], k, nstages, nxt, served_head=(k == 0)) + label, ok = warm_stage(stages[k], f"stage{k} {stages[k]['id']}") + print(f" {'OK ' if ok else 'FAIL '}{label}", flush=True) + if not ok: + print("[abort] stage failed to warm", flush=True); return + + # wait for the draft server + for _ in range(60): + r = rssh(coord, "grep -ciE 'ready, listening' /root/draft.log 2>/dev/null", 20) + if r.stdout.strip().split() and r.stdout.strip().split()[0] != "0": + print("[draft] ready", flush=True); break + time.sleep(10) + else: + print("[draft] not ready:", rssh(coord, "tail -6 /root/draft.log", 20).stdout[-500:], flush=True); return + + head_ep = f"{eps[0][0]}:{eps[0][1]}" + tail_ep = f"{eps[nstages-1][0]}:{eps[nstages-1][1]}" + base = (f"cd /root && SHARD_PSK={PSK} python3 specpipe.py --coordinator --nstages {nstages} " + f"--model {M20} --draft-server 127.0.0.1:{DRAFT_PORT} --next {head_ep} " + f"--direct-return --tail {tail_ep} --prompt \"{a.prompt}\" --max-new {a.max_new} --timeout 300") + + if a.tree: + print(f"\n[run] === FAST TREE spec (w,d={a.tree}), cold+warm ===", flush=True) + r = rssh(coord, base + f" --tree-fast {a.tree} 2>&1 | grep -viE 'INFO|WARNING|warn'", 1800) + else: + dump = " --dump /root/run.json" if a.dump else "" + print(f"\n[run] === COMPARE: SYNC vs PIPE (ks={a.ks}, depths={a.depths}), cold+warm, ONE process ===", flush=True) + r = rssh(coord, base + f" --compare --ks {a.ks} --depths {a.depths}{dump} 2>&1 | grep -viE 'INFO|WARNING|warn'", 1800) + print(r.stdout[-2500:], flush=True) + print("\n[done] stages still warm; teardown: vastai destroy instance ", flush=True) + + +if __name__ == "__main__": + main() diff --git a/phase0/specpipe.py b/phase0/specpipe.py index 0a8e76c..a2002b3 100644 --- a/phase0/specpipe.py +++ b/phase0/specpipe.py @@ -178,17 +178,25 @@ def serve_spec_fast(parts, stage, nstages, listen_port, nxt, timeout, dev, direc if not direct: recv_msg(nxt_sock) if not direct: send_msg(conn, "ok") continue + g = msg.get("gather") + if g: # lazy: compact prev tree's accepted path KV + fv.tree_gather(g[0], g[1]) if "token_ids" in msg: # served head: embed ids here x = parts["embed"](torch.tensor([msg["token_ids"]], device=dev)) else: x = msg["h"].to(dev) - h = fv.prefill(x, msg["start"]) if first else fv.decode(x, msg["start"]) - first = False + if "par" in msg: # TREE verify (fixed-topology graph) + h = fv.tree_decode(x, msg["start"], msg["par"], msg["dep"]) + else: # LINEAR verify (prefill, then graphed decode) + h = fv.prefill(x, msg["start"]) if first else fv.decode(x, msg["start"]); first = False if is_tail: h = parts["norm"](h) send_msg(conn, parts["lm_head"](h).argmax(-1)[0].tolist()) else: - send_msg(nxt_sock, {"op": "verify", "h": h.cpu(), "start": msg["start"]}) + fwd = {"op": "verify", "h": h.cpu(), "start": msg["start"]} + if "par" in msg: fwd["par"] = msg["par"]; fwd["dep"] = msg["dep"] + if g: fwd["gather"] = g + send_msg(nxt_sock, fwd) if not direct: send_msg(conn, recv_msg(nxt_sock)) verifies += 1 @@ -234,9 +242,14 @@ def serve_tail_fast(parts, listen_port, timeout, dev): msg = recv_msg(pred_conn) if msg["op"] == "reset": fv.reset(); first = True; send_msg(ret_conn, "ok"); continue + g = msg.get("gather") + if g: + fv.tree_gather(g[0], g[1]) x = msg["h"].to(dev) - h = fv.prefill(x, msg["start"]) if first else fv.decode(x, msg["start"]) - first = False + if "par" in msg: # TREE verify + h = fv.tree_decode(x, msg["start"], msg["par"], msg["dep"]) + else: + h = fv.prefill(x, msg["start"]) if first else fv.decode(x, msg["start"]); first = False h = parts["norm"](h) send_msg(ret_conn, parts["lm_head"](h).argmax(-1)[0].tolist()); verifies += 1 except EDGE_ERRORS as e: @@ -350,6 +363,7 @@ def embed(tokens): # python list -> this node's block o "mean_K": (sum(k_hist) / len(k_hist)) if k_hist else K, "k_lo": min(k_hist) if k_hist else K, "k_hi": max(k_hist) if k_hist else K, "draft_ms": t_draft / max(rounds, 1) * 1000, "verify_ms": t_verify / max(rounds, 1) * 1000, + "output_ids": out, } @@ -413,6 +427,87 @@ def coordinate(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, "mean_K": (sum(k_hist) / len(k_hist)) if k_hist else K, "k_lo": min(k_hist) if k_hist else K, "k_hi": max(k_hist) if k_hist else K, "draft_ms": t_draft / max(rounds, 1) * 1000, "verify_ms": t_verify / max(rounds, 1) * 1000, + "output_ids": out, + } + + +def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, depth, ret_sock=None): + """PIPELINED coordinator: keep `depth` verify chunks in flight over the ring, so + throughput approaches the ring's per-chunk THROUGHPUT, not its full latency (the + GLM-pipe lever, on the gpt-oss fast-verify path). Same K+1-token chunk as the + synchronous coordinate() ([tail_tok]+K drafts) so the fixed-shape CUDA graph holds; + the StaticKV writes at each chunk's `start`, so after a divergence the fresh chunk + (sent next) overwrites the stale chunks' KV and the coordinator just discards the + stale RESULTS. Greedy => output identical to the synchronous path. Needs the swarm + in --direct-return mode (fire-forward stages, tail returns straight here).""" + pipe_sock.settimeout(timeout) + rx = ret_sock if ret_sock is not None else pipe_sock + eos = tok.eos_token_id + enc = tok.apply_chat_template([{"role": "user", "content": prompt}], + add_generation_prompt=True, return_tensors="pt", return_dict=True) + prompt_ids = enc["input_ids"][0].tolist() + out = [] + t_draft = t_recv = 0.0 + try: + send_msg(pipe_sock, {"op": "reset"}); recv_msg(rx) + send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids, "start": 0}) # prefill (eager) + cur = recv_msg(rx)[-1] + pos = len(prompt_ids) + out = [cur] + inflight = [] # FIFO of (start_pos, drafts) sent but not yet read + discard = 0 # stale post-divergence results still to drain + send_pos = pos # absolute pos where the next chunk writes + dprefix = prompt_ids + [cur] # draft-server query prefix; dprefix[-1] == next tail_tok + valid = accepted = wasted = 0 + t0 = time.time() + done = False + # ASYNC DRAFT: keep exactly one draft request outstanding so the draft server + # computes the next chunk WHILE the current verify chunks cross the WAN. Each fill + # collects the ready draft, sends the verify chunk, then issues the next draft request + # (which then runs concurrently with the verify read below). draft latency is hidden. + send_msg(draft_sock, {"ids": dprefix, "k": K}) # prime: one outstanding request + while not done: + while len(inflight) < depth and not done: # FILL the pipeline + td = time.time(); ds = recv_msg(draft_sock); t_draft += time.time() - td # ready (overlapped) + send_msg(pipe_sock, {"op": "verify", "token_ids": [dprefix[-1]] + ds, "start": send_pos}) + inflight.append((send_pos, ds)); dprefix = dprefix + ds; send_pos += K + send_msg(draft_sock, {"ids": dprefix, "k": K}) # issue next -> runs during the read below + tr = time.time(); r = recv_msg(rx); t_recv += time.time() - tr # READ one result + sp, ds = inflight.pop(0) + if discard > 0: # stale (post-divergence) -> skip + discard -= 1; wasted += 1; continue + n = 0 + for j in range(K): + if ds[j] == r[j]: n += 1 + else: break + valid += 1; accepted += n + if n == K: + out.extend(ds); pos += K; cur = ds[-1] + committed = ds + else: # divergence -> correct + flush in-flight + committed = ds[:n] + [r[n]] + out.extend(committed); cur = r[n]; pos += n + 1 + discard = len(inflight) # every chunk still in flight is stale + recv_msg(draft_sock) # outstanding draft is stale -> drop it + dprefix = prompt_ids + out; send_pos = pos # re-draft from the corrected prefix + send_msg(draft_sock, {"ids": dprefix, "k": K}) # re-prime from the corrected prefix + if len(out) >= max_new or cur == eos or eos in committed: + done = True + recv_msg(draft_sock) # drain the outstanding draft request + while inflight: # drain unread results -> sockets clean for next gen + recv_msg(rx); inflight.pop(0) + except EDGE_ERRORS as e: + raise TransportError(f"pipeline edge failed at token {len(out)} ({type(e).__name__}: {e})") from e + dt = time.time() - t0 + if eos in out: + out = out[:out.index(eos)] + return { + "text": tok.decode(out, skip_special_tokens=True), "n_tokens": len(out), "rounds": valid, + "mean_accept": accepted / max(valid, 1), + "toks_per_traversal": (accepted + valid) / max(valid, 1), + "tok_s": len(out) / max(dt, 1e-9), "wasted": wasted, "depth": depth, "K": K, + "draft_ms": t_draft / max(valid, 1) * 1000, "recv_ms": t_recv / max(valid, 1) * 1000, + "output_ids": out, } @@ -474,6 +569,59 @@ def coordinate_tree(draft_sock, pipe_sock, tok, prompt, tree_cfg, max_new, timeo } +def coordinate_tree_fast(draft_sock, pipe_sock, tok, prompt, tree_cfg, max_new, timeout, ret_sock=None): + """SYNC tree spec on the FAST (graphed) verify. Draft a fixed-topology tree, verify all + its nodes in ONE traversal (FastVerify.tree_decode), accept the best root-to-leaf path, + and compact the accepted KV (static tree_gather). gather payload is (start_prev, kept) + for the static cache (vs the eager gather_cache's absolute-index list). exact greedy.""" + pipe_sock.settimeout(timeout) + rx = ret_sock if ret_sock is not None else pipe_sock + eos = tok.eos_token_id + enc = tok.apply_chat_template([{"role": "user", "content": prompt}], + add_generation_prompt=True, return_tensors="pt", return_dict=True) + prompt_ids = enc["input_ids"][0].tolist() + out = [] + t_draft = t_verify = 0.0 + try: + send_msg(pipe_sock, {"op": "reset"}); recv_msg(rx) + send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids, "start": 0}) # linear prefill + cur = recv_msg(rx)[-1] + pos = len(prompt_ids); out = [cur] + rounds, accepted_total, tnodes = 0, 0, 0 + gather_prev = None + t0 = time.time() + while len(out) < max_new and cur != eos: + td = time.time() + send_msg(draft_sock, {"ids": prompt_ids + out, "tree": tree_cfg}); tr = recv_msg(draft_sock) + t_draft += time.time() - td + tk, par, dep = tr["tok"], tr["par"], tr["dep"] + children = [[] for _ in tk] + for i, p in enumerate(par): + if p != -1: children[p].append(i) + tv = time.time() + send_msg(pipe_sock, {"op": "verify", "token_ids": tk, "par": par, "dep": dep, + "start": pos, "gather": gather_prev}) + targ = recv_msg(rx) # one argmax per tree node + t_verify += time.time() - tv + committed, kept = accept_tree(tk, par, {i: c for i, c in enumerate(children)}, targ) + out.extend(committed); cur = committed[-1] + gather_prev = (pos, kept) # static-cache compaction for next round + pos += len(kept) + rounds += 1; accepted_total += len(kept) - 1; tnodes += len(tk) + if eos in committed: + break + except EDGE_ERRORS as e: + raise TransportError(f"tree edge failed at token {len(out)} ({type(e).__name__}: {e})") from e + dt = time.time() - t0 + if eos in out: + out = out[:out.index(eos)] + return {"text": tok.decode(out, skip_special_tokens=True), "n_tokens": len(out), "rounds": rounds, + "mean_accept": accepted_total / max(rounds, 1), + "toks_per_traversal": len(out) / max(rounds, 1), "tree_nodes": tnodes / max(rounds, 1), + "tok_s": len(out) / max(dt, 1e-9), + "draft_ms": t_draft / max(rounds, 1) * 1000, "verify_ms": t_verify / max(rounds, 1) * 1000} + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--stage", type=int, default=0) @@ -494,6 +642,13 @@ def main(): ap.add_argument("--adaptive", action="store_true", help="tune K live from the running acceptance rate") ap.add_argument("--fast", action="store_true", help="serve node: static-cache CUDA-graph verify (~5x, fixed-K linear)") ap.add_argument("--sweep", default="", help="comma K list to measure on one load, 0=adaptive (e.g. 2,3,4,0)") + ap.add_argument("--pipe", action="store_true", help="coordinator: PIPELINED spec-decode (depth chunks in flight; needs --direct-return)") + ap.add_argument("--depth", type=int, default=4, help="pipelined coordinator: verify chunks in flight") + ap.add_argument("--compare", action="store_true", help="coordinator: SYNC then PIPE (cold+warm) in ONE process for a clean A/B") + ap.add_argument("--depths", default="2,4,8", help="--compare: pipe depths to sweep (one process)") + ap.add_argument("--ks", default="4", help="--compare: K values to sweep (one process; graph recaptures per K)") + ap.add_argument("--tree-fast", default="", help="coordinator: FAST graphed tree spec 'w,d' (cold+warm)") + ap.add_argument("--dump", default="", help="--pipe: write {prompt, output_ids, tok_s} JSON here (for the receipt)") ap.add_argument("--prompt", default="Explain decentralized computing in two sentences.") ap.add_argument("--max-new", type=int, default=128) ap.add_argument("--timeout", type=float, default=120.0) @@ -523,6 +678,66 @@ def main(): f"draft {r['draft_ms']:.0f}ms + verify {r['verify_ms']:.0f}ms/round", flush=True) print(f"\n[coord] === OUTPUT ===\n{r['text']}\n", flush=True) return + if args.tree_fast: # FAST graphed tree spec (cold + warm), sweep 'w,d;w,d' + for spec in args.tree_fast.split(";"): + w, d = (int(x) for x in spec.split(",")) + cfg = {"width": w, "depth": d} + for i in range(2): + r = coordinate_tree_fast(draft_sock, pipe_sock, tok, args.prompt, cfg, args.max_new, + args.timeout, ret_sock=ret_sock) + print(f"[TREE-FAST w={w},d={d} {'warm' if i else 'cold'}] {r['tok_s']:.2f} tok/s | " + f"{r['toks_per_traversal']:.2f} tok/trav | accept {r['mean_accept']:.2f} | " + f"{r['tree_nodes']:.0f} nodes | draft {r['draft_ms']:.0f}ms verify {r['verify_ms']:.0f}ms", flush=True) + print(f"\n[coord] === OUTPUT ===\n{r['text'][:400]}\n", flush=True) + return + if args.compare: # SYNC then PIPE in ONE process (clean warm A/B) + depths = [int(x) for x in args.depths.split(",")] + sync_warm = pipe_warm = None + for K in [int(x) for x in args.ks.split(",")]: + for i in range(2): # sync: cold (captures K+1 graph), then warm + r = coordinate(draft_sock, pipe_sock, tok, args.prompt, K, args.max_new, args.timeout, ret_sock=ret_sock) + if i: sync_warm = r + print(f"[SYNC K={K} {'warm' if i else 'cold'}] {r['tok_s']:.2f} tok/s | " + f"{r['toks_per_traversal']:.2f} tok/trav | accept {r['mean_accept']:.2f} | " + f"draft {r['draft_ms']:.0f}ms verify {r['verify_ms']:.0f}ms", flush=True) + for d in depths: + for i in range(2): # pipe: cold, then warm, at each depth + r = coordinate_pipe(draft_sock, pipe_sock, tok, args.prompt, K, args.max_new, + args.timeout, d, ret_sock=ret_sock) + if i: pipe_warm = r + print(f"[PIPE K={K} depth={d} {'warm' if i else 'cold'}] {r['tok_s']:.2f} tok/s | " + f"{r['toks_per_traversal']:.2f} tok/trav | accept {r['mean_accept']:.2f} | " + f"+{r['wasted']} stale | draft {r['draft_ms']:.0f}ms recv {r['recv_ms']:.0f}ms", flush=True) + if args.dump and sync_warm and pipe_warm: # receipt: pipe ids + the sync-vs-pipe lossless check + import json, hashlib + sids, pids = sync_warm["output_ids"], pipe_warm["output_ids"] + json.dump({"prompt": args.prompt, "model": args.model, + "tok_s_warm": round(pipe_warm["tok_s"], 2), "n_tokens": pipe_warm["n_tokens"], + "output_ids": pids, "output_text": pipe_warm["text"], + "output_sha256": hashlib.sha256(json.dumps(pids).encode()).hexdigest(), + "tokens_match_sync": (sids == pids)}, open(args.dump, "w")) + print(f"[coord] dumped receipt run -> {args.dump} | tokens_match_sync={sids == pids}", flush=True) + print(f"\n[coord] === sample output ===\n{r['text'][:400]}\n", flush=True) + return + if args.pipe: # PIPELINED coordinator (depth chunks in flight) + ks = [int(x) for x in args.sweep.split(",")] if args.sweep else [args.K] + for kv in ks: + r = coordinate_pipe(draft_sock, pipe_sock, tok, args.prompt, kv, args.max_new, + args.timeout, args.depth, ret_sock=ret_sock) + print(f"[PIPE K={kv} depth={args.depth}] {r['tok_s']:.2f} tok/s | {r['toks_per_traversal']:.2f} tok/traversal | " + f"accept {r['mean_accept']:.2f} | +{r['wasted']} stale | " + f"draft {r['draft_ms']:.0f}ms recv {r['recv_ms']:.0f}ms/round", flush=True) + if args.dump: + import json, hashlib + ids = r["output_ids"] + rec = {"prompt": args.prompt, "model": args.model, "K": ks[-1], "depth": args.depth, + "tok_s_warm": round(r["tok_s"], 2), "n_tokens": r["n_tokens"], "output_ids": ids, + "output_text": r["text"], + "output_sha256": hashlib.sha256(json.dumps(ids).encode()).hexdigest()} + json.dump(rec, open(args.dump, "w")) + print(f"[coord] dumped run -> {args.dump} (sha256 {rec['output_sha256'][:16]}..)", flush=True) + print(f"\n[coord] === OUTPUT ===\n{r['text']}\n", flush=True) + return ks = [int(x) for x in args.sweep.split(",")] if args.sweep else [args.K] for kv in ks: adaptive = (kv == 0) or (not args.sweep and args.adaptive) diff --git a/research/draft_server.py b/research/draft_server.py index 8087c24..a609c48 100644 --- a/research/draft_server.py +++ b/research/draft_server.py @@ -44,10 +44,11 @@ def build_tree(ids, width, depth): lp = o[0].outputs[0].logprobs[0] # {tok_id: Logprob} at position 1 kids = [t for t, _ in sorted(lp.items(), key=lambda kv: kv[1].logprob, reverse=True)[:width]] tok, par, dep = [ids[-1]], [-1], [0] # node 0 = root = cur - for ct in kids: - spc = SamplingParams(temperature=0, max_tokens=max(depth - 1, 0), ignore_eos=True) - cont = llm.generate([TokensPrompt(prompt_token_ids=ids + [ct])], spc, use_tqdm=False) - chain = [ct] + list(cont[0].outputs[0].token_ids) + spc = SamplingParams(temperature=0, max_tokens=max(depth - 1, 0), ignore_eos=True) + conts = llm.generate([TokensPrompt(prompt_token_ids=ids + [ct]) for ct in kids], + spc, use_tqdm=False) # BATCHED: all branches in one vLLM call + for bi, ct in enumerate(kids): + chain = [ct] + list(conts[bi].outputs[0].token_ids) parent = 0 for d, t in enumerate(chain): tok.append(t); par.append(parent); dep.append(d + 1); parent = len(tok) - 1 diff --git a/research/treeverify_probe.py b/research/treeverify_probe.py new file mode 100644 index 0000000..987ed75 --- /dev/null +++ b/research/treeverify_probe.py @@ -0,0 +1,43 @@ +"""De-risk probe (one node): does the graphed fixed-topology tree forward +(FastVerify.tree_decode) reproduce the eager static tree forward bit-for-bit? + +This isolates the CUDA-graph mechanics for the tree (shapes, the tree mask buffer, +the scratch-slot KV layout, capture/replay) on a real gpt-oss-120b stage, the same +way fastverify_graph.py de-risked the linear path -- so we don't debug graph capture +over the WAN. Tree LOGIC (ancestor mask + accept + gather) is validated end-to-end +on the swarm afterwards. +""" +import torch +from pipeline import load_stage +from fastverify import FastVerify + +MODEL = "/root/models/gpt-oss-120b" +dev = "cuda" +parts = load_stage(MODEL, 0, 4, device=dev) # head stage: embed + layers 0-8 +embed = parts["embed"] +torch.manual_seed(0) + +L = 24 +prompt = torch.randint(0, 150000, (1, L), device=dev) + +# fixed w=2,d=4 tree (build_tree shape): root + 2 chains of length 4 -> 9 nodes +par = [-1, 0, 1, 2, 3, 0, 5, 6, 7] +dep = [0, 1, 2, 3, 4, 1, 2, 3, 4] +M = len(par) +ttok = torch.randint(0, 150000, (1, M), device=dev) + +with torch.no_grad(): + fv = FastVerify(parts, dev=dev) + # eager static tree forward (no graph) + fv.reset(); fv.prefill(embed(prompt), 0) + fv._tbuild(M, par, dep); fv._tset(embed(ttok), L, par, dep) + he = fv._tbody().clone() + # graphed tree forward (capture + replay) on the same prefix + tree + fv.reset(); fv.prefill(embed(prompt), 0) + fv.tgraph = None + hg = fv.tree_decode(embed(ttok), L, par, dep).clone() + +d = (he.float() - hg.float()).abs() +print(f"tree forward graphed-vs-eager-static | shape {tuple(hg.shape)} | " + f"max|diff|={d.max().item():.6f} mean|diff|={d.mean().item():.7f}") +print("PROBE_OK" if d.max().item() < 1e-2 else "PROBE_DIVERGE") From 36e658cf86eac234df285e1b4e5f6488682e5ab0 Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:51:17 +0000 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20permissionless=20transport=20?= =?UTF-8?q?=E2=80=94=20120B=20over=20libp2p,=20no=20shared=20secret=20(JOI?= =?UTF-8?q?N/step=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the shared-SHARD_PSK TCP wire with a libp2p sidecar: a Go go-libp2p daemon that runs next to the Python engine as a transparent TCP-over-libp2p tunnel. Each node holds its own ed25519 key (PeerId) — no shared secret anywhere. The engine swaps transport by one import (wire -> shard.transport, the PSK-free message codec); its logic is untouched. Proven: gpt-oss-120B split across 4 scattered boxes (UT/CA/NV/WA) served over libp2p produces bit-identical greedy tokens to the trusted-wire receipt — same 4 physical GPUs, sha f646e0db...3f70. Receipt: docs/receipts/gpt-oss-120b-libp2p-20260619.json. - sidecar/: go-libp2p tunnel + per-node identity (tunnel + self-test modes) - shard/transport.py: PSK-free send_msg/recv_msg, drop-in for wire - phase0/specpipe.py: --dump on the sync coordinator path - docs/INTEGRATION.md: full done-right architecture (5-verb build); STATE.md: status - docs/NETWORK.md: the network narrative (c0mpute = network, Shard = engine) Next (step 2): NAT traversal (DCUtR + relay; today used mapped public ports), identity<->cwt_ account binding, and re-enabling the pipelined perf path (~40 tok/s) over the sidecar. --- STATE.md | 42 ++ docs/INTEGRATION.md | 227 +++++++++ docs/NETWORK.md | 115 +++++ .../gpt-oss-120b-libp2p-20260619.json | 158 ++++++ phase0/specpipe.py | 8 + shard/transport.py | 133 ++++-- sidecar/README.md | 58 +++ sidecar/go.mod | 88 ++++ sidecar/go.sum | 449 ++++++++++++++++++ sidecar/main.go | 300 ++++++++++++ 10 files changed, 1543 insertions(+), 35 deletions(-) create mode 100644 STATE.md create mode 100644 docs/INTEGRATION.md create mode 100644 docs/NETWORK.md create mode 100644 docs/receipts/gpt-oss-120b-libp2p-20260619.json create mode 100644 sidecar/README.md create mode 100644 sidecar/go.mod create mode 100644 sidecar/go.sum create mode 100644 sidecar/main.go diff --git a/STATE.md b/STATE.md new file mode 100644 index 0000000..8b6e204 --- /dev/null +++ b/STATE.md @@ -0,0 +1,42 @@ +# Shard — build status + +One glance, full picture. The whole network is **5 verbs**. SERVE is done; we're building the other four. + +## The map +1. **JOIN** — a stranger's GPU gets in *(identity, NAT transport, pull its slice of weights)* +2. **FORM** — the network picks nearby nodes and wires them into a swarm *(scheduler, assignment, heal)* +3. **SERVE** — the swarm answers the request, fast — ✅ **DONE** (~40 tok/s gpt-oss-120B, ~30 GLM-5.2 744B over WAN) +4. **PROVE** — each node proves it actually ran its layer *(signed receipts, layer-block spot-check)* +5. **PAY** — each node gets paid for its bit *(per-node, c0mpute rails)* + +Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these five, done right. + +## Build steps (→ verb · status) +| # | Step | Verb | Status | +|---|------|------|--------| +| 0 | Engine (pipeline + spec-decode + pipelining) | SERVE | ✅ done | +| 1 | libp2p sidecar + per-node identity + data-plane (retire `SHARD_PSK`) | JOIN | ✅ **done** | +| 2 | NAT traversal + bind identity ↔ c0mpute account | JOIN | ◀ next | +| 3 | Manifest + content-addressed weight fetch | JOIN | todo | +| 4 | Scheduler + assignment protocol | FORM | todo | +| 5 | Job routing + signed receipts + per-node pay | PROVE/PAY | todo | +| 6 | Reputation upgrade + layer-block spot-check | PROVE | todo | +| 7 | Heal + mid-request fault tolerance | FORM | todo *(research)* | +| 8 | P2P propagation takes over from mirror | JOIN | todo *(additive)* | + +## Now +**Step 1 (JOIN transport) DONE.** ✅ The real gpt-oss-120B, split across 4 scattered boxes (UT·CA·NV·WA) over **libp2p with per-node keys and no `SHARD_PSK`**, produced **bit-identical** greedy tokens to the committed `wire.py` receipt (sha `f646e0db…3f70`, 87 tokens). Proven incrementally: 1.1 key-auth round-trip → 1.2 engine↔sidecar tensors → 1.3a transparent TCP-over-libp2p tunnel → 1.3b PSK-free message codec → 1.3d-i cross-box libp2p over real WAN → 1.3d-ii the full 120B ring. Sidecar = `sidecar/main.go`; engine wire = `shard/transport.py`; the engine ran unmodified except `import wire → import shard.transport as wire`. + +**Loose ends before committing step 1:** +- Prune the superseded 1.2 unix-socket bridge (sidecar `-engine` mode + `Edge`/`ActivationCodec`) — the tunnel replaced it. +- Re-enable the **perf path** over the sidecar (direct-return + pipelining → ~40 tok/s; the relay-back sync proof ran at 22.9 tok/s). The 2-connection tail is the only fiddly bit. Correctness-independent. +- Land a libp2p receipt (mirrors the wire.py one) as the banked proof. + +**Next (step 2):** NAT traversal (home GPUs behind NAT — DCUtR + relay; today used Vast's mapped public ports) + bind each node's libp2p key ↔ its c0mpute `cwt_` account. + +## Decisions locked +- **Boundary law:** dependencies point one way — `c0mpute → shard`, never reverse. Shard is a pure engine. +- **Transport:** libp2p via a **Go** (`go-libp2p`) sidecar; Python engine talks to it over a local Unix socket. +- **Identity folds into the libp2p step** (libp2p gives keypair identity for free — a separate identity layer would be throwaway). +- **Verification:** graded reputation + a layer-block challenge (canary-style); economic-now (eject + withhold pay) → crypto-later. +- **`$ZERO` staking** = yield only, no slashing — orthogonal to verification, left out of it. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000..598a674 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,227 @@ +# Shard × c0mpute — the done-right architecture + +*The build spec for taking a hand-deployed swarm to a permissionless one, with no rip-out +debt. This is the internal engineering contract; [NETWORK.md](NETWORK.md) is the public +narrative. (Supersedes the transport/scheduler/integration sections of the older +[ARCHITECTURE.md](ARCHITECTURE.md), which predates the proven stack — SGLang→transformers, +aioquic→libp2p, shared PSK→per-node keys.) Build target: **gpt-oss-120B on 4× RTX 4090**, +the most practical swarm a stranger can join.* + +--- + +## 0. The law + +> **Dependencies point one way: `c0mpute → shard`, never the reverse.** + +Shard is a pure engine: given a model and a set of peers, it swarms them to serve tokens +fast, with content-verified shards and per-node verifiable receipts. Shard knows **nothing** +about `$ZERO`, `privy_id`, USDC, payments, reputation, or the orchestrator. If shard ever +imports a c0mpute concept, the boundary leaked — and that's the rip-out debt we refuse. + +This isn't tidiness. It's the proof of permissionlessness: an engine that doesn't depend on +the network's permission system can run under *any* network. **Shard is BitTorrent the +protocol; c0mpute is the swarm that speaks it.** + +Every "which repo?" is answered by this rule. Runs on a contributor's GPU → shard. Network +brain (identity, money, reputation, tracker, catalog) → c0mpute. The wire between them → a +protocol contract, defined here, named on both sides. + +--- + +## 1. The two halves + +**shard (the engine — what a contributor installs):** +- The inference engine (pipeline split + speculative decoding + pipelining — proven: ~40 + tok/s gpt-oss-120B, ~30 tok/s GLM-5.2 744B over WAN). +- The **node-agent**: loads its assigned layer block, joins the ring, runs the block, + forwards activations, emits signed per-batch receipts. +- The **transport sidecar** (libp2p) and the **layer-block challenge** primitive. + +**c0mpute (the network brain):** +- The **tracker** (orchestrator): node pool, registration, job routing. +- The **scheduler**: forms swarms from the pool (the assignment protocol). +- The **latency graph**, the **swarm registry**, the **reputation** system, the **economy** + (per-node payment), the **MODEL_CATALOG**. + +**The contract:** the protocol messages in §7–§8. Implemented on both sides; owned by neither. + +--- + +## 2. Identity (kills the shared PSK) + +Today every node shares one `SHARD_PSK`. That's replaced by **per-node keys**: + +- Each node generates a **libp2p keypair → PeerId** (stable network identity). This lives in + shard — it's the engine's identity, nothing c0mpute-specific. +- **Binding to a c0mpute account:** at registration the node signs a challenge proving + control of *both* its PeerId and its `cwt_`/`privy_id`. c0mpute records the binding. The + node-agent only ever exposes "here is my PeerId + a signature"; c0mpute does the binding. + Law preserved. + +Result: authentication is per-node and cryptographic; `SHARD_PSK` retires. + +--- + +## 3. Transport — libp2p via sidecar + +A mature **Go/Rust libp2p daemon runs as a sidecar** on each node; the Python engine talks +to it over a local Unix socket / gRPC (local hop ≈ 0.1 ms, nothing against a WAN ring). + +What we use libp2p for: +- **Noise/TLS encryption + QUIC** on every link. +- **NAT traversal** — AutoNAT detects reachability, **DCUtR hole-punching**, **circuit-relay-v2** + fallback for hard NATs. *This is the whole point — home GPUs behind NAT are the thesis.* +- **Kademlia DHT** for peer discovery + content routing. +- **Direct streams** for the activation hot-path between adjacent ring stages. Gossipsub is + for control/discovery only — **never on the hot path.** + +The sidecar replaces `phase0/wire.py`'s role (PSK-authenticated TCP). The sidecar owns +connection + NAT; the engine streams activation bytes through it. + +--- + +## 4. Model propagation — content-addressed + +A node only needs *its* block, so it only fetches a fraction of the model. + +- **Manifest** (signed JSON): `{model_id, layer_count, tokenizer, arch, shards:[{shard_id, + sha256, size}], publisher_pubkey, signature}`. +- A node fetches its shards from **any provider** via libp2p content routing and **verifies + each chunk against the manifest hash** on arrival. A malicious peer physically cannot feed + you corrupted weights. +- **Catalog:** c0mpute's `MODEL_CATALOG` holds a pointer to the manifest (CID/URL + + publisher pubkey). Adding a model = publish a manifest + add a catalog entry. + +**Propagation seam:** the source is pluggable. Now → a seed **mirror is just the first +provider**. Later → peers announce the shards they hold and **P2P takes over** — additive, +zero rework, because the fetch was content-verified from day one. + +--- + +## 5. The swarm engine (shard — proven) + +A model is a stack of layers split into contiguous blocks, one block per GPU; activations +stream through in a ring. A **coordinator** holds no layers, runs a small draft, and drives +**speculative decoding** (draft proposes K tokens → distributed model verifies all in one +ring traversal → greedy commit), with **pipelining** (many traversals in flight). The +node-agent's job per swarm: load its block, link its ring neighbors via the sidecar, run the +block, forward activations, **sign a receipt per batch**. + +--- + +## 6. Receipts + verification (the canary, upgraded for swarms) + +c0mpute's current anti-cheat is a whole-model canary: send a math+nonce prompt, check the +answer. **It cannot probe a stage-node** — a node holding layers 12–23 never sees a prompt, +it transforms an activation tensor. Swarms need two new things, and they split cleanly across +the law: + +**(a) Signed receipts (shard emits, c0mpute consumes).** Each stage node signs +`{swarm_id, job_id, batch_id, layer_range, in_hash, out_hash}` with its node key. The +coordinator collects receipts and submits them on `job:complete`. Payment integrity falls +out for free: c0mpute pays each node per its signed receipts; the coordinator **can't +fabricate** a node's receipt (needs its key), and a node **can't be paid** without producing +one. This kills coordinator-takes-all and stops the coordinator stealing pay. + +**(b) Layer-block challenge (shard provides the primitive, c0mpute owns the policy).** A +verifier feeds a stage-node a **known activation** whose correct `out_hash` was computed by +re-running that block on a trusted/redundant node. Mismatch → strike. Shard exposes only +"run this block on this input → output hash"; *when* to probe, *how* to score, *when* to +eject is c0mpute policy. This is the stage-node analogue of the existing canary. + +**Reputation upgrade (c0mpute).** Today reputation is binary (`banned` / not). Swarms need a +**graded score** the scheduler consumes — to prefer reliable nodes, to **pin leaky boundary +layers (embedding/final) to the most-trusted nodes** (privacy), and to gate who may +coordinate. The existing recent-window ban logic stays; we add the gradient on top. + +**Verification seam:** economic-now (strike → reputation hit → eject + withhold pay) → +crypto-later. The receipt's `in_hash/out_hash` slot is exactly where a cheap proof drops in. + +--- + +## 7. The scheduler / tracker (c0mpute — the centralized seam) + +Holds the node pool, a **sparse, decaying latency graph** (full N² doesn't scale; stale +jitter lies), the swarm registry, reputation, and the catalog. To serve a model: + +1. Pick a **low-latency cluster** with enough VRAM for the model + a coordinator. +2. Fit **contiguous blocks to each node's VRAM, fat nodes first** (fewer hops). +3. Order the ring to minimize the loop; **place the coordinator in-region** (this lever was + worth ~50% — 174→102 ms). +4. Assign. + +**`swarm:assign`** → each chosen node: +``` +{ swarm_id, manifest_ref, layer_start, layer_end, role: "stage"|"coordinator", + peers: [{ peer_id, addr, layer_range }], coordinator_peer } +``` +Node fetches its shards (if uncached), loads, links neighbors, signals ready. All ready → +swarm live. + +**Heal, don't reshuffle.** A dropped node → pull a replacement from the pool, reload its +block, re-link the ring. New joins land in the **pool**, never reshuffle a running swarm +(pure churn). Global re-optimization runs on a slow cadence or on break, never per-join. + +**Control-plane seam:** centralized first. It holds **no weights and no user data**, so +decentralizing it later (gossip, elected schedulers) is a clean swap, not a rewrite. + +--- + +## 8. Job flow + payment (per-node) + +``` +client → c0mpute tracker → job:new → coordinator +coordinator runs coordinate_pipe over the ring + → job:token (stream) → client + → job:complete { response, tokensGenerated, receipts[] } → tracker +tracker verifies receipts → attributes tokens per node → pays via worker_earnings +``` + +Each node earns for the tokens its block helped produce (coordinator earns for draft + +drive). Same c0mpute rails, no coordinator-takes-all. + +--- + +## 9. The seams, stated honestly + +| Seam | Now (correct, not debt) | Later (clean swap) | +|---|---|---| +| Control plane | centralized scheduler (holds no weights/data) | gossip / elected | +| Verification | economic: strike + eject + withhold pay | cryptographic proof in the hash slot | +| Propagation | seed mirror = first provider | P2P peers take over (additive) | + +None of these is an "in-between" that gets ripped out — each is the right interface with a +capability switched on later. + +--- + +## 10. Build sequence — done right, exercised early + +No throwaway code, but a real swarm runs *soon* so the design gets pressure-tested. + +1. **Identity + handshake** — per-node keys ⟷ `privy_id` binding; retire `SHARD_PSK`. +2. **libp2p sidecar + activation data-plane** — replace `wire.py`; 2-node swarm over libp2p, + LAN → NAT. *First NAT-traversed link.* +3. **Manifest + content-addressed fetch** — mirror as first provider; node pulls + verifies + its block. +4. **Scheduler (central) + assignment** — auto bring-up a swarm from a pool. *First fully + automatic swarm.* +5. **Job routing + signed receipts + per-node payment** — **live & earning on c0mpute.** +6. **Reputation upgrade + layer-block spot-check** — trust hardening. +7. **Heal + mid-request fault tolerance.** +8. **P2P propagation** takes over from the mirror (additive). + +Each step leaves a running, correct system. + +--- + +## 11. What's genuine research (not pretending otherwise) + +- **Layer-block spot-check at scale**, and eventually a cheap cryptographic proof to replace + re-compute-and-compare. +- **Mid-request KV fault tolerance** — a node vanishing mid-token means recompute, not + seamless migration. +- **Decentralized scheduling** — the control-plane seam, when we take it. +- **Privacy** — boundary-layer pinning + trusted-only routing; earns its word phase by phase. + +Everything before those is engineering we've already proven variants of. diff --git a/docs/NETWORK.md b/docs/NETWORK.md new file mode 100644 index 0000000..c72b4c7 --- /dev/null +++ b/docs/NETWORK.md @@ -0,0 +1,115 @@ +# The c0mpute network: swarm inference on scattered consumer GPUs + +*How the c0mpute network — the permissionless GPU network behind [c0mpute.ai](https://c0mpute.ai) — +is being reshaped so its nodes can **swarm**: pool into clusters that serve models far too big for +any single card. The engine that makes a swarm fast over the open internet is **Shard**. c0mpute is +the network; Shard is the engine.* + +--- + +## The idea + +Frontier AI runs in datacenters because frontier models don't fit on one GPU. c0mpute is building +the other path: a permissionless network where anyone plugs in a GPU, and those GPUs **swarm** — +pool together to serve a model far too big for any single card. No datacenter. No gatekeeper. +Uncensored, private, and the people who supply the compute get paid. + +c0mpute already runs single-GPU inference across volunteer GPUs. This is the next layer — **swarm +inference**: many scattered consumer GPUs collectively serving a 120B or 744B model over the open +internet, fast enough to actually use. + +And it's real, not a whitepaper. gpt-oss-120B at **~40 tok/s**, GLM-5.2 744B at **~30 tok/s**, on +consumer RTX 4090s and Blackwell cards scattered across US states — every run emits a verifiable +receipt (distinct GPU IDs, real WAN latencies, output hash). Here's how the network works. + +## A swarm + +A model is a stack of layers. Split the stack into contiguous blocks, put one block on each GPU, +and stream activations through them in order. No node ever holds the whole model. A swarm is: + +- **Stage nodes** — each holds a contiguous block of layers (its *shard*). +- **A coordinator** — holds *no* model layers; it runs a small draft model and drives generation. + +Activations flow `coordinator → stage 0 → stage 1 → … → tail → back to coordinator` — one loop per +step. + +## The one law: latency dominates + +Over the open internet the bottleneck isn't compute, it's the round-trip — every token traverses the +whole ring. So the network's #1 job isn't "find GPUs that fit," it's **assemble low-latency local +clusters**. We learned this the expensive way: moving the (layer-less) coordinator from across the +country *into* the swarm's region cut the ring **174 ms → 102 ms** and lifted throughput **~50%** — +for free. The entire topology is built around this single fact. + +## Heterogeneous GPUs, interchangeable shards + +Nodes are all different — 16 GB, 24 GB, 80 GB. So blocks are **sized to each node's VRAM**, not +uniform. And a consequence people miss: **prefer fat nodes.** One 80 GB GPU replaces three 24 GB +GPUs *and* removes two WAN hops — fewer hops = faster. Big GPUs don't just add capacity, they shorten +the ring. + +A shard is just weights + kernels, so **the same block runs on any capable GPU** — which gives +redundancy, failover, and reassignment. Two honest caveats: the KV-cache for an in-flight request is +node-local (failing a node over mid-request means recompute, not seamless migration), and across GPU +architectures quantized outputs can differ at genuine near-ties (both valid — expected, not a bug). + +## How a swarm self-assembles + +The network keeps a **sparse, decaying latency graph** of who's close to whom (a full N² matrix +doesn't scale to thousands of nodes, and jitter makes stale data lie). To serve a model, the +scheduler: + +1. Picks a **low-latency cluster** with enough total VRAM for the model + a coordinator. +2. Assigns contiguous blocks fit to each node's VRAM (fat nodes first → fewer hops). +3. Orders the ring to minimize the loop, and **places the coordinator in-region**. +4. Designates a coordinator/draft node (the draft must share the target's tokenizer family). + +New nodes do **not** reshuffle running swarms — that's pure churn (reloading models, dropping +in-flight requests). They land in a **pool**, and are used to form new swarms or to **heal** one +(replace a dropped node). Global re-optimization runs on a slow cadence or when something breaks — +never on every join. + +## How models reach the nodes — trustlessly + +A node only needs *its* block, so each node holds and serves a fraction of the model. The +decentralized way to distribute weights is node-to-node propagation (BitTorrent-style) — but the +load-bearing piece isn't the transport, it's **content-addressing**. Every model ships a signed +**manifest**: the hash of every shard. A node fetches chunks from *any* peer and verifies each +against the manifest. In a permissionless network that's everything — **a malicious peer physically +cannot feed you corrupted weights.** Once chunks are hashed, peer-to-peer propagation is almost free +and gets faster as the network grows. We start hybrid (a seed mirror + manifest verification) and let +peer-propagation take over. + +## How it stays fast + +Splitting a model across the internet is easy; making it *usable* is the part Shard owns. The trick +is **speculative decoding**: a small draft proposes several tokens, the distributed model verifies +them all in one ring traversal, and greedy acceptance commits the verified prefix — so one +round-trip commits many tokens. Layer on **pipelining** (many traversals in flight at once) and the +WAN stops being the floor. That's how 36 layers across 4 states reach ~40 tok/s, greedy and +deterministic. ([receipts](receipts/) · [how a skeptic checks one](PROOF.md)) + +## Trust, pay, privacy — and what's still hard + +c0mpute already has the economy: contribute compute, get paid per token. Swarms plug into the same +rails — each node earns for the tokens its block helped produce. The honest frontier: + +- **Trustless verification.** How do you know a node actually ran its layer instead of forwarding + plausible garbage? Redundant spot-checks + staking with slashing. This is the real research; we + won't pretend it's solved. +- **Privacy.** A node decrypts to run its layer, so it sees the activations it processes. Mitigation: + pin the leaky boundary layers (embedding, final layers) to staked/trusted nodes; route sensitive + requests to trusted-only. "Private" earns its word phase by phase, never on day one. +- **Fault tolerance.** Recovering a request when a node vanishes mid-token (KV migration) is harder + than failover-for-new-requests. + +## The path + +Centralized control plane first — the c0mpute orchestrator forms the clusters, allocates the blocks, +routes the requests. It holds no weights and no user data, so decentralizing it later (gossip, +elected schedulers) is a clean swap, not a rewrite. Ship the network that works today, remove the +trust assumptions one at a time, in the open, measured, never overclaimed. + +**The foundation in one line:** a latency-aware clustering engine that assembles fat-node, in-region +rings from content-verified shards, and serves frontier models across GPUs nobody owns together. +Permissionless inference — anyone joins, anyone earns. diff --git a/docs/receipts/gpt-oss-120b-libp2p-20260619.json b/docs/receipts/gpt-oss-120b-libp2p-20260619.json new file mode 100644 index 0000000..c9996d8 --- /dev/null +++ b/docs/receipts/gpt-oss-120b-libp2p-20260619.json @@ -0,0 +1,158 @@ +{ + "run_id": "gpt-oss-120b-libp2p-20260619", + "utc": "2026-06-19T16:49:34.863094Z", + "shard_commit_parent": "73a6d533686b99e1207b0228d2ab26828623e675", + "what": "the same 120B swarm as the wire.py receipt, now served over libp2p with per-node keys and NO SHARD_PSK", + "transport": "libp2p (go-libp2p sidecar v0.33.2) \u2014 transparent TCP-over-libp2p tunnel; per-node ed25519 keys; Noise-encrypted; no shared secret", + "decode": "greedy (exact); relay-back sync coordinator", + "model": "gpt-oss-120b", + "quant": "mxfp4", + "topology": "3 stages of 12 layers + a layer-less coordinator (draft); relay-back ring over libp2p", + "prompt": "Explain decentralized computing in two sentences.", + "output_text": "analysisNeed to answer concisely: two sentences. Provide definition and key aspects.assistantfinalDecentralized computing distributes processing, storage, and decision\u2011making across a network of independent nodes rather than relying on a single central server or data center. This architecture enhances fault tolerance, scalability, and privacy by allowing each node to operate autonomously while still collaborating through consensus or peer\u2011to\u2011peer protocols.", + "output_token_ids": [ + 200005, + 35644, + 200008, + 23483, + 316, + 6052, + 4468, + 276, + 1151, + 25, + 1920, + 40536, + 13, + 51441, + 16083, + 326, + 2140, + 15712, + 13, + 200007, + 200006, + 173781, + 200005, + 17196, + 200008, + 6206, + 48026, + 2110, + 34349, + 163118, + 12323, + 11, + 7928, + 11, + 326, + 8660, + 50005, + 37891, + 5251, + 261, + 5402, + 328, + 13313, + 15749, + 7542, + 1572, + 64139, + 402, + 261, + 4590, + 11082, + 6017, + 503, + 1238, + 6170, + 13, + 1328, + 24022, + 70548, + 20795, + 46581, + 11, + 152600, + 11, + 326, + 16480, + 656, + 16246, + 2454, + 4596, + 316, + 19866, + 37793, + 10701, + 2049, + 2928, + 102752, + 1819, + 46012, + 503, + 24770, + 50005, + 935, + 50005, + 32092, + 49170, + 13 + ], + "n_tokens": 87, + "tok_s": 22.86, + "output_sha256": "f646e0dbe2b3c695b9f495d508fbc0a8d96ba938e90eb845fda69f65c1873f70", + "verification": { + "wire_receipt": "gpt-oss-120b-wan-20260619.json", + "wire_output_sha256": "f646e0dbe2b3c695b9f495d508fbc0a8d96ba938e90eb845fda69f65c1873f70", + "libp2p_output_sha256": "f646e0dbe2b3c695b9f495d508fbc0a8d96ba938e90eb845fda69f65c1873f70", + "bit_identical": true + }, + "nodes": [ + { + "role": "coordinator", + "layer_range": null, + "public_ip": "136.59.129.136", + "geo": "Utah, US", + "libp2p_addr": "/ip4/136.59.129.136/tcp/34842/p2p/12D3KooWJheA8cz8qjptUnJ55TbURybiNiakfZ7Dxqn9F7N4kn21", + "gpu_uuid": "GPU-1eb664cd-c6f9-387c-682e-d7225edc76fb", + "gpu_name": "NVIDIA GeForce RTX 4090" + }, + { + "role": "stage0 (served-head)", + "layer_range": [ + 0, + 11 + ], + "public_ip": "66.114.157.224", + "geo": "California, US", + "libp2p_addr": "/ip4/66.114.157.224/tcp/41071/p2p/12D3KooWPHL1ubijRiugd8GpuPktqeWkumNBvkhNsYa451DXEr2d", + "gpu_uuid": "GPU-0abcaebf-4660-6386-eebe-3dabdbb39a73", + "gpu_name": "NVIDIA GeForce RTX 4090" + }, + { + "role": "stage1", + "layer_range": [ + 12, + 23 + ], + "public_ip": "173.239.95.142", + "geo": "Nevada, US", + "libp2p_addr": "/ip4/173.239.95.142/tcp/41366/p2p/12D3KooWDD1j3CjxaFmBZGzy7mX2sWQWbfDvZB9J7JLQbwWLQwYq", + "gpu_uuid": "GPU-5d73df3c-5537-1808-c8a7-90bb2cab0b53", + "gpu_name": "NVIDIA GeForce RTX 4090" + }, + { + "role": "tail", + "layer_range": [ + 24, + 35 + ], + "public_ip": "76.121.3.151", + "geo": "Washington, US", + "libp2p_addr": "/ip4/76.121.3.151/tcp/26423/p2p/12D3KooWQ5Mt1PgMMBArWxyrGC5aP2g54Vm4NBX87ZZcyhf4T5NR", + "gpu_uuid": "GPU-3939cfdc-a999-dbfb-1c9b-512b7a97c693", + "gpu_name": "NVIDIA GeForce RTX 4090" + } + ] +} \ No newline at end of file diff --git a/phase0/specpipe.py b/phase0/specpipe.py index a2002b3..17c8cdf 100644 --- a/phase0/specpipe.py +++ b/phase0/specpipe.py @@ -750,6 +750,14 @@ def main(): print(f"\n[coord] === OUTPUT ===\n{r['text']}\n", flush=True) print(f"[coord] {r['n_tokens']} tok | {r['tok_s']:.2f} tok/s | {r['toks_per_traversal']:.2f} tok/traversal | " f"accept {r['mean_accept']:.2f} | draft {r['draft_ms']:.0f}ms + verify {r['verify_ms']:.0f}ms/round", flush=True) + if args.dump: # sync output ids + hash (for the receipt / a transport A/B) + import json, hashlib + ids = r["output_ids"] + json.dump({"prompt": args.prompt, "model": args.model, "K": ks[-1], "mode": "sync", + "tok_s": round(r["tok_s"], 2), "n_tokens": r["n_tokens"], "output_ids": ids, + "output_text": r["text"], + "output_sha256": hashlib.sha256(json.dumps(ids).encode()).hexdigest()}, open(args.dump, "w")) + print(f"[coord] dumped run -> {args.dump} (sha256 {hashlib.sha256(json.dumps(ids).encode()).hexdigest()[:16]}..)", flush=True) return parts = load_stage(args.model, args.stage, args.nstages, device=args.device) diff --git a/shard/transport.py b/shard/transport.py index cfbbe10..faab7f9 100644 --- a/shard/transport.py +++ b/shard/transport.py @@ -1,51 +1,114 @@ -"""inter-stage transport — the part we own, the wedge. +"""inter-stage transport — the part shard owns, the wedge. -moves the activation tensor across the public internet between two adjacent -pipeline stages, reliably, over nat. quic + hole-punching + relay fallback + -a quantized activation codec + backpressure/reconnect. fully instrumented: -every edge logs its own health, no opaque "broken pipe". +The networking lives in the libp2p sidecar (a Go daemon — see ../sidecar): identity, +encryption, NAT traversal, and the peer connections are its job. The engine keeps its +plain socket code and talks to the sidecar over localhost; the sidecar carries each +connection to/from the right ring neighbour over libp2p. So this module is just the +*message* layer the engine speaks over that socket — `send_msg`/`recv_msg`, a drop-in +for phase0/wire.py minus the ChaCha seal (libp2p already encrypts + authenticates the +link). The engine swaps transport by one import: `import wire` → `import shard.transport +as wire`. -start: aioquic in python. lift the hot path to rust (quinn) when bandwidth demands. +Per the boundary law (docs/INTEGRATION.md) this is pure engine plumbing: peers and +bytes, nothing about c0mpute. + +frame wire: 8-byte big-endian length + JSON header + raw tensor blobs (matches the +phase0/wire.py framing so call sites are unchanged). """ +import json +import socket +import struct + +import numpy as np +import torch + + +def _read_exact(sock: socket.socket, n: int) -> bytes: + buf = bytearray() + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + raise ConnectionError("sidecar closed the connection") + buf += chunk + return bytes(buf) + + +# ── message codec (engine protocol): JSON header + raw tensor blobs ── +# Ported from phase0/wire.py's _pack/_unpack — the *message* format the engine speaks +# (control fields + tensors). No ChaCha seal: encryption + identity are the sidecar's job. +_DTYPES = {str(d): d for d in (torch.float32, torch.float16, torch.bfloat16, + torch.int64, torch.int32, torch.uint8, torch.bool)} + + +def _pack(obj) -> bytes: + blobs = [] + + def encode(o): + if torch.is_tensor(o): + t = o.detach().cpu().contiguous() + blobs.append(t.reshape(-1).view(torch.uint8).numpy().tobytes()) + return {"__t__": len(blobs) - 1, "dtype": str(t.dtype), "shape": list(t.shape)} + if isinstance(o, dict): + return {k: encode(v) for k, v in o.items()} + if isinstance(o, (list, tuple)): + return [encode(v) for v in o] + if o is None or isinstance(o, (bool, int, float, str)): + return o + raise TypeError(f"transport cannot encode {type(o).__name__}") + head = json.dumps(encode(obj)).encode() + out = bytearray(struct.pack("!I", len(head)) + head) + for b in blobs: + out += struct.pack("!Q", len(b)) + b + return bytes(out) -class ActivationCodec: - """serialize + (fp8/int8) quantize + optionally compress a hidden-state tensor. - activations tolerate quantization far better than weights, and bandwidth is the - cost center on a home uplink, so this is where we spend effort. - """ +def _unpack(buf: bytes): + (hlen,) = struct.unpack_from("!I", buf, 0) + head = json.loads(buf[4:4 + hlen]) + blobs, off = [], 4 + hlen + while off < len(buf): + (blen,) = struct.unpack_from("!Q", buf, off); off += 8 + blobs.append(buf[off:off + blen]); off += blen - def encode(self, hidden_states) -> bytes: - raise NotImplementedError # phase 1 + def decode(node): + if isinstance(node, dict): + if "__t__" in node: + dt = _DTYPES.get(node["dtype"]) + if dt is None: + raise ValueError(f"unknown tensor dtype {node['dtype']!r}") + shape, blob = node["shape"], blobs[node["__t__"]] + if not blob: + return torch.empty(shape, dtype=dt) + raw = torch.from_numpy(np.frombuffer(blob, dtype=np.uint8).copy()) + return raw.view(dt).reshape(shape) + return {k: decode(v) for k, v in node.items()} + if isinstance(node, list): + return [decode(v) for v in node] + return node - def decode(self, payload: bytes): - raise NotImplementedError # phase 1 + return decode(head) -class Edge: - """one supervised pipeline edge: a quic connection from this stage to the next. +def send_msg(sock: socket.socket, obj) -> int: + """pack + send one message over a (sidecar-tunneled) socket; returns bytes on the wire. + drop-in for phase0/wire.send_msg minus the SHARD_PSK seal (libp2p encrypts the link).""" + frame = _pack(obj) + sock.sendall(struct.pack("!Q", len(frame)) + frame) + return 8 + len(frame) - handles hole-punch via the rendezvous, relay fallback, send/recv of encoded - activations, timeouts, backpressure, reconnect. surfaces health() so a stalled - edge is detected fast instead of wedging the pipeline. - """ - def __init__(self, peer_id: str, rendezvous: str, codec: "ActivationCodec"): - self.peer_id = peer_id - self.rendezvous = rendezvous - self.codec = codec +def recv_msg(sock: socket.socket): + """receive + unpack one message. drop-in for phase0/wire.recv_msg.""" + (n,) = struct.unpack("!Q", _read_exact(sock, 8)) + return _unpack(_read_exact(sock, n)) - async def connect(self) -> None: - """hole-punch to peer via rendezvous; fall back to relay on symmetric nat.""" - raise NotImplementedError # phase 0 (direct) / phase 1 (nat) - async def send(self, hidden_states) -> None: - raise NotImplementedError # phase 0 +# No-ops so `import shard.transport as wire` is a perfect drop-in: the libp2p sidecar +# provides encryption + identity, so there is no pre-shared key to load. +def key_from_env(var: str = "SHARD_PSK") -> None: + return None - async def recv(self): - raise NotImplementedError # phase 0 - def health(self) -> dict: - """rtt, in-flight, last-ok timestamp. the thing the black-box binary never gave us.""" - raise NotImplementedError +def use_key(material) -> None: + return None diff --git a/sidecar/README.md b/sidecar/README.md new file mode 100644 index 0000000..bd0bf6c --- /dev/null +++ b/sidecar/README.md @@ -0,0 +1,58 @@ +# sidecar — shard's libp2p transport daemon + +A Go (`go-libp2p`) daemon that runs next to the Python engine on every node. It owns the +node's keypair identity (an ed25519 key → libp2p PeerId) and moves activation frames +between adjacent pipeline stages over authenticated, encrypted libp2p streams. Per the +boundary law ([../docs/INTEGRATION.md](../docs/INTEGRATION.md)) it knows only **peers and +bytes** — nothing about `$ZERO`, accounts, payments, or the orchestrator. + +It's a single static binary (no CGO), so we build once and `scp` it to any linux/amd64 +node — a node needs no Go toolchain to run it. + +## Build + +```sh +cd sidecar +GOTOOLCHAIN=local GOPROXY=https://goproxy.io,direct go build -o /tmp/sidecar . +``` + +Two load-bearing build pins (in the spirit of `phase0/setup_box.sh`'s `kernels` pin): + +- **`go-libp2p v0.33.2`** — builds on Go 1.22 (our toolchain); newer releases want Go 1.23/1.25+. + It still ships DCUtR + circuit-relay-v2, so step 2 (NAT traversal) isn't blocked by the pin. +- **`GOPROXY=goproxy.io`** — `proxy.golang.org` returns `403` on the `klauspost/compress` zip + (and Go only falls back to `direct` on `404`, not `403`), which stalls the whole resolve. + `goproxy.io` serves it fine. + +## Run as a tunnel (how the engine uses it) + +The sidecar is a transparent TCP↔libp2p tunnel. Pin the libp2p port to the node's +public-mapped port; the engine then dials/listens on localhost and the sidecar carries +each connection to/from the right ring neighbour: + +```sh +# a stage: accept inbound streams -> the local engine, and carry the engine's +# next-hop connection to the downstream peer over libp2p +sidecar -key /root/node.key -listen /ip4/0.0.0.0/tcp/29600 \ + -inbound 127.0.0.1:29610 \ + -forward 127.0.0.1:29611=/ip4//tcp//p2p/ +``` + +The engine runs unchanged except one import: `import wire` → `import shard.transport as +wire` (see `../shard/transport.py`). Proven: gpt-oss-120B across 4 scattered boxes over +libp2p, no `SHARD_PSK`, bit-identical to the trusted-wire receipt +(`../docs/receipts/gpt-oss-120b-libp2p-20260619.json`). + +## Self-test — identity + connect-by-key + round-trip (connectivity check) + +```sh +# terminal A (listener): prints its PeerId + dialable multiaddr, writes the addr to a file +/tmp/sidecar -key /tmp/a.key -addrfile /tmp/addrA + +# terminal B (dialer): connects by key, round-trips a 2 MiB activation-sized frame +/tmp/sidecar -key /tmp/b.key -peer "$(cat /tmp/addrA)" -size 2097152 +# -> ROUND-TRIP OK: 2097152 bytes echoed by ... +``` + +`ROUND-TRIP OK` with two distinct PeerIds = the transport is up, authenticated by key, and +intact. No `SHARD_PSK` anywhere. diff --git a/sidecar/go.mod b/sidecar/go.mod new file mode 100644 index 0000000..acbae2b --- /dev/null +++ b/sidecar/go.mod @@ -0,0 +1,88 @@ +module shard/sidecar + +go 1.22.2 + +require ( + github.com/libp2p/go-libp2p v0.33.2 + github.com/multiformats/go-multiaddr v0.12.3 +) + +require ( + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.2 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.1.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-nat v0.2.0 // indirect + github.com/libp2p/go-netroute v0.2.1 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v4 v4.0.1 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/miekg/dns v1.1.58 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.5.0 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/onsi/ginkgo/v2 v2.15.0 // indirect + github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.47.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/quic-go v0.42.0 // indirect + github.com/quic-go/webtransport-go v0.6.0 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.uber.org/dig v1.17.1 // indirect + go.uber.org/fx v1.20.1 // indirect + go.uber.org/mock v0.4.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.18.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + lukechampine.com/blake3 v1.2.1 // indirect +) diff --git a/sidecar/go.sum b/sidecar/go.sum new file mode 100644 index 0000000..15c8c47 --- /dev/null +++ b/sidecar/go.sum @@ -0,0 +1,449 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= +github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= +github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= +github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= +github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-libp2p v0.33.2 h1:vCdwnFxoGOXMKmaGHlDSnL4bM3fQeW8pgIa9DECnb40= +github.com/libp2p/go-libp2p v0.33.2/go.mod h1:zTeppLuCvUIkT118pFVzA8xzP/p2dJYOMApCkFh0Yww= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= +github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= +github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= +github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= +github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= +github.com/multiformats/go-multiaddr v0.12.3 h1:hVBXvPRcKG0w80VinQ23P5t7czWgg65BmIvQKjDydU8= +github.com/multiformats/go-multiaddr v0.12.3/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= +github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= +github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= +github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= +github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k= +github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= +github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= +github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY= +github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/sidecar/main.go b/sidecar/main.go new file mode 100644 index 0000000..2bf0897 --- /dev/null +++ b/sidecar/main.go @@ -0,0 +1,300 @@ +// Command sidecar is shard's libp2p transport daemon. +// +// One sidecar runs alongside the Python engine on every node. It owns the node's +// cryptographic identity (an ed25519 keypair -> libp2p PeerId) and carries the engine's +// inter-stage connections to/from its ring neighbours over authenticated, encrypted +// libp2p streams. It runs as a transparent TCP<->libp2p tunnel: the engine keeps its +// plain socket code and just talks to localhost; the sidecar does the network. This is +// what replaces the shared-SHARD_PSK TCP wire (phase0/wire.py). +// +// Per the boundary law (docs/INTEGRATION.md): pure engine plumbing. It knows nothing +// about $ZERO, accounts, payments, or the orchestrator — only peers and bytes. +// +// Modes: +// -inbound HOST:PORT tunnel: dial the local engine for each inbound stream +// -forward LOCAL=PEER_MULTIADDR tunnel: listen LOCAL, carry each conn to PEER (repeatable) +// -peer PEER_MULTIADDR self-test: round-trip one frame to a listener (connectivity check) +// (none) self-test listener: echo one frame back +package main + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/binary" + "flag" + "fmt" + "io" + "log" + "net" + "os" + "strings" + "time" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" +) + +// activationProto is the stream protocol carrying inter-stage traffic (and the self-test). +const activationProto = "/shard/activation/1.0.0" + +// stringList is a repeatable string flag (used for -forward). +type stringList []string + +func (s *stringList) String() string { return strings.Join(*s, ",") } +func (s *stringList) Set(v string) error { *s = append(*s, v); return nil } + +// loadOrCreateKey returns a stable node identity, persisting it to keyPath so a node +// keeps the same PeerId across restarts. This per-node key is what replaces the shared +// SHARD_PSK: a node proves who it is by holding this key, not by knowing a secret. +func loadOrCreateKey(keyPath string) (crypto.PrivKey, error) { + if keyPath != "" { + if b, err := os.ReadFile(keyPath); err == nil { + return crypto.UnmarshalPrivateKey(b) + } + } + priv, _, err := crypto.GenerateEd25519Key(rand.Reader) + if err != nil { + return nil, err + } + if keyPath != "" { + b, err := crypto.MarshalPrivateKey(priv) + if err != nil { + return nil, err + } + if err := os.WriteFile(keyPath, b, 0o600); err != nil { + return nil, err + } + } + return priv, nil +} + +// writeFrame / readFrame: a 4-byte big-endian length prefix + payload (the self-test wire). +func writeFrame(w io.Writer, b []byte) error { + var hdr [4]byte + binary.BigEndian.PutUint32(hdr[:], uint32(len(b))) + if _, err := w.Write(hdr[:]); err != nil { + return err + } + _, err := w.Write(b) + return err +} + +func readFrame(r io.Reader) ([]byte, error) { + var hdr [4]byte + if _, err := io.ReadFull(r, hdr[:]); err != nil { + return nil, err + } + b := make([]byte, binary.BigEndian.Uint32(hdr[:])) + _, err := io.ReadFull(r, b) + return b, err +} + +func newHost(priv crypto.PrivKey, listen string) (host.Host, error) { + // libp2p defaults give Noise/TLS encryption + a stream muxer; every link is + // authenticated to the peer's key. TCP for now; QUIC + NAT traversal land in step 2. + return libp2p.New( + libp2p.Identity(priv), + libp2p.ListenAddrStrings(listen), + ) +} + +// fullAddrs returns this host's dialable /p2p multiaddrs (addr + /p2p/). +func fullAddrs(h host.Host) []string { + p2p := multiaddr.StringCast("/p2p/" + h.ID().String()) + out := make([]string, 0, len(h.Addrs())) + for _, a := range h.Addrs() { + out = append(out, a.Encapsulate(p2p).String()) + } + return out +} + +func main() { + keyPath := flag.String("key", "", "path to persist the node key (keeps PeerId stable)") + peerAddr := flag.String("peer", "", "self-test: dial this /p2p multiaddr and round-trip a frame") + addrFile := flag.String("addrfile", "", "write this host's dial multiaddr here (for scripting)") + listenAddr := flag.String("listen", "/ip4/0.0.0.0/tcp/0", "libp2p listen multiaddr; pin the port for cross-box reach, e.g. /ip4/0.0.0.0/tcp/29600") + inbound := flag.String("inbound", "", "tunnel: dial this local engine addr (host:port) for each inbound libp2p stream") + var forwards stringList + flag.Var(&forwards, "forward", "tunnel: localAddr=peerMultiaddr — listen localAddr, carry each conn to the peer (repeatable)") + size := flag.Int("size", 1<<20, "self-test frame size in bytes (default 1 MiB)") + flag.Parse() + + priv, err := loadOrCreateKey(*keyPath) + if err != nil { + log.Fatalf("key: %v", err) + } + h, err := newHost(priv, *listenAddr) + if err != nil { + log.Fatalf("host: %v", err) + } + defer h.Close() + log.Printf("peer id: %s", h.ID()) + addrs := fullAddrs(h) + for _, a := range addrs { + fmt.Printf("ADDR %s\n", a) + } + if *addrFile != "" { + pick := addrs[0] + for _, a := range addrs { + if strings.Contains(a, "127.0.0.1") { + pick = a + break + } + } + if err := os.WriteFile(*addrFile, []byte(pick), 0o644); err != nil { + log.Fatalf("addrfile: %v", err) + } + } + + // Tunnel mode: a transparent TCP<->libp2p bridge. The engine keeps its own socket + // code and just talks to localhost; the sidecar carries each connection to/from the + // right ring neighbour over libp2p. This is what replaces wire.py's TCP. + if *inbound != "" || len(forwards) > 0 { + if *inbound != "" { + runInbound(h, *inbound) + } + for _, f := range forwards { + pp := strings.SplitN(f, "=", 2) + if len(pp) != 2 { + log.Fatalf("bad -forward %q (want localAddr=peerMultiaddr)", f) + } + go runForward(h, pp[0], pp[1]) + } + log.Printf("tunnel up (inbound=%q forwards=%v)", *inbound, []string(forwards)) + select {} + } + + // Self-test dialer: connect by multiaddr, round-trip a frame, verify it byte-for-byte. + // libp2p's Noise handshake guarantees the peer holds the key in the multiaddr. + if *peerAddr != "" { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + maddr, err := multiaddr.NewMultiaddr(*peerAddr) + if err != nil { + log.Fatalf("bad -peer: %v", err) + } + info, err := peer.AddrInfoFromP2pAddr(maddr) + if err != nil { + log.Fatalf("bad -peer: %v", err) + } + if err := h.Connect(ctx, *info); err != nil { + log.Fatalf("connect: %v", err) + } + s, err := h.NewStream(ctx, info.ID, activationProto) + if err != nil { + log.Fatalf("stream: %v", err) + } + defer s.Close() + log.Printf("connected to %s (key-authenticated)", s.Conn().RemotePeer()) + + blob := make([]byte, *size) + if _, err := rand.Read(blob); err != nil { + log.Fatalf("rand: %v", err) + } + start := time.Now() + if err := writeFrame(s, blob); err != nil { + log.Fatalf("send: %v", err) + } + got, err := readFrame(s) + if err != nil { + log.Fatalf("recv: %v", err) + } + rtt := time.Since(start) + if !bytes.Equal(got, blob) { + log.Fatalf("ROUND-TRIP MISMATCH: sent %d bytes, got %d", len(blob), len(got)) + } + fmt.Printf("ROUND-TRIP OK: %d bytes echoed by %s in %v\n", len(blob), s.Conn().RemotePeer(), rtt) + return + } + + // Self-test listener: echo any frame back to the sender (connectivity check). + h.SetStreamHandler(activationProto, func(s network.Stream) { + defer s.Close() + b, err := readFrame(s) + if err != nil { + log.Printf("recv: %v", err) + return + } + log.Printf("recv %d bytes from %s", len(b), s.Conn().RemotePeer()) + if err := writeFrame(s, b); err != nil { + log.Printf("send: %v", err) + } + }) + log.Printf("listening; start a second sidecar with -peer ") + select {} // serve until killed +} + +// openStream dials a peer by multiaddr and opens an activation stream. libp2p's +// Noise handshake guarantees the peer holds the key named in the multiaddr. +func openStream(h host.Host, peerAddr string) (network.Stream, error) { + maddr, err := multiaddr.NewMultiaddr(peerAddr) + if err != nil { + return nil, err + } + info, err := peer.AddrInfoFromP2pAddr(maddr) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := h.Connect(ctx, *info); err != nil { + return nil, err + } + return h.NewStream(ctx, info.ID, activationProto) +} + +// pipe copies bytes bidirectionally between two streams until either side closes. +func pipe(a, b io.ReadWriteCloser) { + done := make(chan struct{}, 2) + cp := func(dst io.Writer, src io.Reader) { io.Copy(dst, src); done <- struct{}{} } + go cp(a, b) + go cp(b, a) + <-done + a.Close() + b.Close() +} + +// runForward listens on a local TCP addr; each accepted connection is carried to the +// peer over a fresh libp2p stream — so the engine dials localhost and reaches the peer. +func runForward(h host.Host, listenAddr, peerMaddr string) { + ln, err := net.Listen("tcp", listenAddr) + if err != nil { + log.Fatalf("forward listen %s: %v", listenAddr, err) + } + log.Printf("forward %s -> %s", listenAddr, peerMaddr) + for { + c, err := ln.Accept() + if err != nil { + log.Printf("forward accept: %v", err) + return + } + go func() { + s, err := openStream(h, peerMaddr) + if err != nil { + log.Printf("forward dial: %v", err) + c.Close() + return + } + pipe(c, s) + }() + } +} + +// runInbound pipes each inbound libp2p stream to a fresh connection to the local +// engine — so the engine accepts on localhost, fed by its ring neighbours. +func runInbound(h host.Host, engineAddr string) { + h.SetStreamHandler(activationProto, func(s network.Stream) { + c, err := net.Dial("tcp", engineAddr) + if err != nil { + log.Printf("inbound -> engine %s: %v", engineAddr, err) + s.Reset() + return + } + pipe(s, c) + }) +} From 84b6f9b8b3e385a801d7722e30b6224c443013fb Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:36:51 +0000 Subject: [PATCH 03/10] =?UTF-8?q?perf:=20direct-return=20+=20pipelining=20?= =?UTF-8?q?over=20libp2p=20=E2=80=94=2044.8=20tok/s,=20parity-or-better?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-enable the pipelined perf path over the sidecar. The earlier failure was a latent race in serve_tail_fast: it identified the coordinator's direct-return channel by arrival order (first-readable), but over the sidecar the fire-forwarded reset can reach the predecessor connection before hello_return reaches the return connection, so the tail mis-read a reset as the hello and tore both down. Now identify the return channel by content (hello_return), tolerant of either order — transport-agnostic, no regression on TCP. Result on the 4-box 120B ring over libp2p (plain TCP, no QUIC): PIPE K=4 depth=2 warm 44.79 tok/s, bit-identical greedy output (tokens_match_sync=True, same sha as the wire receipt). vs trusted-wire 39.8 — parity-or-better this window. Receipt updated to the perf path. --- STATE.md | 7 +++--- .../gpt-oss-120b-libp2p-20260619.json | 8 +++++-- phase0/specpipe.py | 24 ++++++++++++++----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/STATE.md b/STATE.md index 8b6e204..c8fa028 100644 --- a/STATE.md +++ b/STATE.md @@ -27,10 +27,9 @@ Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these fi ## Now **Step 1 (JOIN transport) DONE.** ✅ The real gpt-oss-120B, split across 4 scattered boxes (UT·CA·NV·WA) over **libp2p with per-node keys and no `SHARD_PSK`**, produced **bit-identical** greedy tokens to the committed `wire.py` receipt (sha `f646e0db…3f70`, 87 tokens). Proven incrementally: 1.1 key-auth round-trip → 1.2 engine↔sidecar tensors → 1.3a transparent TCP-over-libp2p tunnel → 1.3b PSK-free message codec → 1.3d-i cross-box libp2p over real WAN → 1.3d-ii the full 120B ring. Sidecar = `sidecar/main.go`; engine wire = `shard/transport.py`; the engine ran unmodified except `import wire → import shard.transport as wire`. -**Loose ends before committing step 1:** -- Prune the superseded 1.2 unix-socket bridge (sidecar `-engine` mode + `Edge`/`ActivationCodec`) — the tunnel replaced it. -- Re-enable the **perf path** over the sidecar (direct-return + pipelining → ~40 tok/s; the relay-back sync proof ran at 22.9 tok/s). The 2-connection tail is the only fiddly bit. Correctness-independent. -- Land a libp2p receipt (mirrors the wire.py one) as the banked proof. +**Perf path re-enabled (direct-return + pipelining over libp2p):** **44.79 tok/s warm @ depth 2**, bit-identical (sha `f646e0db…3f70`, `tokens_match_sync=True`) — i.e. **parity-or-better vs the trusted-wire 39.8** (this window's return leg was 45 ms). Sweep: PIPE d2 warm 44.8 / d4 warm 39.3 / SYNC warm 33.5. The fix was a latent race in `serve_tail_fast` — it now identifies the return channel by content (`hello_return`), not arrival order. So libp2p adds no real tax; QUIC stays a step-2 lever, not needed for parity. + +Done & committed: prune of the dead 1.2 bridge, the libp2p receipt, the tail fix. **Next (step 2):** NAT traversal (home GPUs behind NAT — DCUtR + relay; today used Vast's mapped public ports) + bind each node's libp2p key ↔ its c0mpute `cwt_` account. diff --git a/docs/receipts/gpt-oss-120b-libp2p-20260619.json b/docs/receipts/gpt-oss-120b-libp2p-20260619.json index c9996d8..309151f 100644 --- a/docs/receipts/gpt-oss-120b-libp2p-20260619.json +++ b/docs/receipts/gpt-oss-120b-libp2p-20260619.json @@ -4,10 +4,14 @@ "shard_commit_parent": "73a6d533686b99e1207b0228d2ab26828623e675", "what": "the same 120B swarm as the wire.py receipt, now served over libp2p with per-node keys and NO SHARD_PSK", "transport": "libp2p (go-libp2p sidecar v0.33.2) \u2014 transparent TCP-over-libp2p tunnel; per-node ed25519 keys; Noise-encrypted; no shared secret", - "decode": "greedy (exact); relay-back sync coordinator", + "decode": "greedy (exact); direct-return + pipelined coordinator (K=4, depth=2); sync==pipe (tokens_match)", "model": "gpt-oss-120b", "quant": "mxfp4", - "topology": "3 stages of 12 layers + a layer-less coordinator (draft); relay-back ring over libp2p", + "topology": "3 stages of 12 layers + a layer-less coordinator (draft); direct-return ring over libp2p", + "tok_s_warm": 44.79, + "tok_s_sweep": {"pipe_d2_warm": 44.79, "pipe_d4_warm": 39.34, "sync_warm": 33.46}, + "vs_wire_receipt_tok_s": 39.8, + "note_speed": "parity-or-better vs the trusted-wire receipt; libp2p adds no real tax (this window's return leg ~45ms vs the wire receipt's ~55ms). plain TCP-over-libp2p, no QUIC.", "prompt": "Explain decentralized computing in two sentences.", "output_text": "analysisNeed to answer concisely: two sentences. Provide definition and key aspects.assistantfinalDecentralized computing distributes processing, storage, and decision\u2011making across a network of independent nodes rather than relying on a single central server or data center. This architecture enhances fault tolerance, scalability, and privacy by allowing each node to operate autonomously while still collaborating through consensus or peer\u2011to\u2011peer protocols.", "output_token_ids": [ diff --git a/phase0/specpipe.py b/phase0/specpipe.py index 17c8cdf..75fe8c4 100644 --- a/phase0/specpipe.py +++ b/phase0/specpipe.py @@ -226,20 +226,32 @@ def serve_tail_fast(parts, listen_port, timeout, dev): ready, _, _ = select.select([c1, c2], [], [], timeout) if not ready: c1.close(); c2.close(); continue - ret_conn = ready[0] + # identify the return channel by CONTENT, not arrival order: the coordinator + # sends {"op":"hello_return"} on it. Over some transports (e.g. the libp2p + # sidecar) the predecessor's first forward message can race ahead of the hello, + # so the first-readable conn may be the predecessor — handle either order. try: - hello = recv_msg(ret_conn) + first_conn = ready[0] + m0 = recv_msg(first_conn) + if isinstance(m0, dict) and m0.get("op") == "hello_return": + ret_conn = first_conn; pred_conn = c2 if first_conn is c1 else c1 + pending = None + else: # first_conn is the predecessor; m0 is a real msg + pred_conn = first_conn; ret_conn = c2 if first_conn is c1 else c1 + hello = recv_msg(ret_conn) + if not (isinstance(hello, dict) and hello.get("op") == "hello_return"): + c1.close(); c2.close(); continue + pending = m0 except EDGE_ERRORS: c1.close(); c2.close(); continue - if not (isinstance(hello, dict) and hello.get("op") == "hello_return"): - c1.close(); c2.close(); continue - pred_conn = c2 if ret_conn is c1 else c1; pred_conn.settimeout(timeout) + pred_conn.settimeout(timeout) print("[tail] predecessor + coordinator-return connected", flush=True) fv.reset(); first = True; verifies = 0 with torch.no_grad(): while True: try: - msg = recv_msg(pred_conn) + msg = pending if pending is not None else recv_msg(pred_conn) + pending = None if msg["op"] == "reset": fv.reset(); first = True; send_msg(ret_conn, "ok"); continue g = msg.get("gather") From e81fbb7aa987df2d2ca745ac51f77f4845949415 Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:43:57 +0000 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20sidecar=20NAT=20stack=20=E2=80=94?= =?UTF-8?q?=20relay=20join=20proven,=20direct-line=20(DCUtR)=20wired=20(st?= =?UTF-8?q?ep=202.1/2.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the NAT-traversal stack to the sidecar so home GPUs behind a router can join: QUIC, DCUtR hole-punching, circuit-relay-v2 (service + client), AutoNAT, -announce (advertise the real public addr behind a container's port mapping), explicit client.Reserve (deterministic + observable, vs autorelay's black-box finder), and a connection monitor that logs RELAY vs DIRECT. Proven on real boxes (UT relay, CA public, a genuinely NAT-blocked node): the NAT'd node reserves a relay slot and data crosses the relay both ways — i.e. a bedroom GPU can join. The direct hole-punched line (DCUtR) is wired but can't be demonstrated in a 3-node Docker test: DCUtR waits for the node's confirmed public address, which needs several observer peers / AutoNAT, and a Docker box is likely symmetric-NAT. Not a code bug — it activates on the real multi-node network with a real (cone) home NAT. Next: 2.3 identity <-> cwt_ binding (unblocked); validate the direct line on the real network or by standing up AutoNAT/relay helper nodes. --- STATE.md | 7 ++- sidecar/main.go | 139 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 138 insertions(+), 8 deletions(-) diff --git a/STATE.md b/STATE.md index c8fa028..17cd7fa 100644 --- a/STATE.md +++ b/STATE.md @@ -16,7 +16,7 @@ Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these fi |---|------|------|--------| | 0 | Engine (pipeline + spec-decode + pipelining) | SERVE | ✅ done | | 1 | libp2p sidecar + per-node identity + data-plane (retire `SHARD_PSK`) | JOIN | ✅ **done** | -| 2 | NAT traversal + bind identity ↔ c0mpute account | JOIN | ◀ next | +| 2 | NAT traversal + bind identity ↔ c0mpute account | JOIN | ◀ **building** | | 3 | Manifest + content-addressed weight fetch | JOIN | todo | | 4 | Scheduler + assignment protocol | FORM | todo | | 5 | Job routing + signed receipts + per-node pay | PROVE/PAY | todo | @@ -31,7 +31,10 @@ Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these fi Done & committed: prune of the dead 1.2 bridge, the libp2p receipt, the tail fix. -**Next (step 2):** NAT traversal (home GPUs behind NAT — DCUtR + relay; today used Vast's mapped public ports) + bind each node's libp2p key ↔ its c0mpute `cwt_` account. +**Step 2 (building) — NAT + identity binding.** +- ✅ **2.1** sidecar NAT stack: QUIC + DCUtR + circuit-relay-v2 (service + client) + AutoNAT + `-announce` + explicit `client.Reserve` + a conn monitor (RELAY/DIRECT). `sidecar/main.go`. +- ◑ **2.2** relay join PROVEN, direct line needs real topology. Confirmed on real boxes: this dev box is genuinely NAT-blocked (direct IPv4 dial fails); it **reserves a relay slot** and **data crosses the relay both ways** to the NAT'd node. DCUtR (the direct hole-punch) stalls in-test: `waiting until we have at least one public address` — the NAT'd node can't confirm its observed/public address with only one peer (libp2p needs several observers/AutoNAT), and a Docker box is likely symmetric-NAT (un-punchable). **Not a code bug** — DCUtR needs the real multi-node network (peers + AutoNAT) and a real home (cone) NAT to activate. Validate the direct line on the actual network, not a 3-box Docker test. +- ☐ **2.3** identity ↔ `cwt_` binding: node signs a challenge proving control of (PeerId, cwt_); c0mpute records it (c0mpute-repo change — shard signs, c0mpute records). Unblocked. ## Decisions locked - **Boundary law:** dependencies point one way — `c0mpute → shard`, never reverse. Shard is a pure engine. diff --git a/sidecar/main.go b/sidecar/main.go index 2bf0897..ec9c335 100644 --- a/sidecar/main.go +++ b/sidecar/main.go @@ -36,6 +36,7 @@ import ( "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + relayclient "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client" "github.com/multiformats/go-multiaddr" ) @@ -94,13 +95,66 @@ func readFrame(r io.Reader) ([]byte, error) { return b, err } -func newHost(priv crypto.PrivKey, listen string) (host.Host, error) { +type natOpts struct { + quic bool + relayService bool + announce string + staticRelays []peer.AddrInfo +} + +// tcpToQuic derives a QUIC listen addr from a TCP one: /ip4/x/tcp/P -> /ip4/x/udp/P/quic-v1. +func tcpToQuic(maddr string) string { + i := strings.Index(maddr, "/tcp/") + if i < 0 { + return "" + } + port := maddr[i+len("/tcp/"):] + if j := strings.Index(port, "/"); j >= 0 { + port = port[:j] + } + return maddr[:i] + "/udp/" + port + "/quic-v1" +} + +func newHost(priv crypto.PrivKey, listen string, n natOpts) (host.Host, error) { // libp2p defaults give Noise/TLS encryption + a stream muxer; every link is - // authenticated to the peer's key. TCP for now; QUIC + NAT traversal land in step 2. - return libp2p.New( + // authenticated to the peer's key. The NAT stack (DCUtR hole-punching + circuit + // relay) lets home GPUs behind NAT join; QUIC (udp) hole-punches more reliably. + listens := []string{listen} + if n.quic { + if q := tcpToQuic(listen); q != "" { + listens = append(listens, q) + } + } + opts := []libp2p.Option{ libp2p.Identity(priv), - libp2p.ListenAddrStrings(listen), - ) + libp2p.ListenAddrStrings(listens...), + libp2p.EnableHolePunching(), // DCUtR: punch a direct hole between two NAT'd peers + } + if n.announce != "" { + // libp2p only sees container-internal addrs behind Vast's port mapping; advertise + // the real public addr so reservations/circuit addrs others get are actually dialable. + ann, err := multiaddr.NewMultiaddr(n.announce) + if err != nil { + return nil, err + } + opts = append(opts, libp2p.AddrsFactory(func(addrs []multiaddr.Multiaddr) []multiaddr.Multiaddr { + return append([]multiaddr.Multiaddr{ann}, addrs...) + })) + } + if n.relayService { + // be a public relay (circuit-relay-v2). Force public reachability so the hop + // service activates immediately — otherwise libp2p waits for AutoNAT to confirm + // reachability, which never happens without AutoNAT-peer infrastructure. + opts = append(opts, libp2p.EnableRelayService(), libp2p.ForceReachabilityPublic()) + } + // NAT'd nodes reserve on relays explicitly (in main) rather than via autorelay's + // background finder — deterministic, observable, and the scheduler hands out the + // circuit address anyway. Force private reachability so DCUtR knows we're the NAT'd + // side and initiates the hole-punch to the public peer. + if len(n.staticRelays) > 0 { + opts = append(opts, libp2p.ForceReachabilityPrivate()) + } + return libp2p.New(opts...) } // fullAddrs returns this host's dialable /p2p multiaddrs (addr + /p2p/). @@ -122,13 +176,32 @@ func main() { var forwards stringList flag.Var(&forwards, "forward", "tunnel: localAddr=peerMultiaddr — listen localAddr, carry each conn to the peer (repeatable)") size := flag.Int("size", 1<<20, "self-test frame size in bytes (default 1 MiB)") + relaySvc := flag.Bool("relay", false, "run as a circuit-relay-v2 server (public rendezvous for NAT'd nodes)") + relaysCSV := flag.String("relays", "", "comma-separated relay /p2p multiaddrs to use when behind NAT") + useQuic := flag.Bool("quic", false, "also listen on QUIC (udp) — better hole-punching + lossy links") + announce := flag.String("announce", "", "advertise this public multiaddr ahead of auto-detected ones (e.g. /ip4/PUBIP/tcp/PORT)") flag.Parse() priv, err := loadOrCreateKey(*keyPath) if err != nil { log.Fatalf("key: %v", err) } - h, err := newHost(priv, *listenAddr) + var staticRelays []peer.AddrInfo + for _, s := range strings.Split(*relaysCSV, ",") { + if s = strings.TrimSpace(s); s == "" { + continue + } + ma, err := multiaddr.NewMultiaddr(s) + if err != nil { + log.Fatalf("bad -relays entry %q: %v", s, err) + } + ai, err := peer.AddrInfoFromP2pAddr(ma) + if err != nil { + log.Fatalf("bad -relays entry %q: %v", s, err) + } + staticRelays = append(staticRelays, *ai) + } + h, err := newHost(priv, *listenAddr, natOpts{quic: *useQuic, relayService: *relaySvc, announce: *announce, staticRelays: staticRelays}) if err != nil { log.Fatalf("host: %v", err) } @@ -151,6 +224,28 @@ func main() { } } + go monitorConns(h) // log RELAY vs DIRECT connections so we can watch DCUtR upgrade + + // NAT'd node: explicitly reserve a slot on each relay and keep the connection + // protected, so the relay can forward inbound connections to us. Clear errors, + // no autorelay guesswork. + for _, relay := range staticRelays { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + if err := h.Connect(ctx, relay); err != nil { + log.Printf("relay connect %s: %v", relay.ID, err) + cancel() + continue + } + res, err := relayclient.Reserve(ctx, h, relay) + cancel() + if err != nil { + log.Printf("relay reserve %s: %v", relay.ID, err) + continue + } + h.ConnManager().Protect(relay.ID, "relay") + log.Printf("RESERVED relay slot on %s (expires %s)", relay.ID, res.Expiration) + } + // Tunnel mode: a transparent TCP<->libp2p bridge. The engine keeps its own socket // code and just talks to localhost; the sidecar carries each connection to/from the // right ring neighbour over libp2p. This is what replaces wire.py's TCP. @@ -248,6 +343,27 @@ func openStream(h host.Host, peerAddr string) (network.Stream, error) { return h.NewStream(ctx, info.ID, activationProto) } +// monitorConns logs each new connection and whether it's via a relay or DIRECT — so we +// can watch DCUtR upgrade a relay rendezvous into a direct hole-punched link. +func monitorConns(h host.Host) { + seen := map[string]bool{} + for { + time.Sleep(5 * time.Second) + for _, c := range h.Network().Conns() { + a := c.RemoteMultiaddr().String() + kind := "DIRECT" + if strings.Contains(a, "p2p-circuit") { + kind = "RELAY" + } + key := c.RemotePeer().String() + "|" + a + if !seen[key] { + seen[key] = true + log.Printf("CONN %s via %s [%s]", c.RemotePeer(), a, kind) + } + } + } +} + // pipe copies bytes bidirectionally between two streams until either side closes. func pipe(a, b io.ReadWriteCloser) { done := make(chan struct{}, 2) @@ -262,6 +378,17 @@ func pipe(a, b io.ReadWriteCloser) { // runForward listens on a local TCP addr; each accepted connection is carried to the // peer over a fresh libp2p stream — so the engine dials localhost and reaches the peer. func runForward(h host.Host, listenAddr, peerMaddr string) { + // pre-establish the connection so DCUtR can upgrade relay->direct BEFORE data flows + // (otherwise the engine's first stream lands on the slow relay connection). + if ma, err := multiaddr.NewMultiaddr(peerMaddr); err == nil { + if ai, err := peer.AddrInfoFromP2pAddr(ma); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + if err := h.Connect(ctx, *ai); err != nil { + log.Printf("forward pre-connect %s: %v", ai.ID, err) + } + cancel() + } + } ln, err := net.Listen("tcp", listenAddr) if err != nil { log.Fatalf("forward listen %s: %v", listenAddr, err) From 3ea8a18756e08edadc97dd9e826ed910d9ca1cd5 Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:37:10 +0000 Subject: [PATCH 05/10] feat: upgrade to go-libp2p v0.48 + prove DCUtR hole-punch in a two-NAT lab (step 2.2) Bump go-libp2p v0.33.2 -> v0.48.0 (Go 1.25.11). The old version isn't the issue, but latest is the right place for production (hardened DCUtR/AutoNAT). Code compiled clean against v0.48 (no API breaks). Proved the direct line end-to-end in a controlled lab: two Linux network namespaces, each behind its own NAT (full-cone, UPnP-style), a relay in the middle. AutoNAT confirmed each node's public address, DCUtR upgraded the relay rendezvous to a DIRECT QUIC connection between the two NAT'd nodes, and 100 KB transferred byte-identical over it (circuit-relay caps ~2 KB, so 100 KB proves it went direct, not relayed). Gotchas worth recording: host ufw silently drops the lab's UDP (bypass for the bridge); rp_filter drops NAT'd returns (disable); and libp2p refuses to dial TEST-NET ranges (203.0.113.x) as "blocked observed address" -> use a routable range (11.0.0.x). So: every node joins; full-cone home routers get a direct line via DCUtR, restricted/ symmetric fall back to the proven relay. Same model IPFS runs at scale. --- STATE.md | 4 +- sidecar/go.mod | 115 ++++++------ sidecar/go.sum | 483 +++++++++++++----------------------------------- sidecar/main.go | 19 +- 4 files changed, 202 insertions(+), 419 deletions(-) diff --git a/STATE.md b/STATE.md index 17cd7fa..49061bb 100644 --- a/STATE.md +++ b/STATE.md @@ -32,8 +32,8 @@ Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these fi Done & committed: prune of the dead 1.2 bridge, the libp2p receipt, the tail fix. **Step 2 (building) — NAT + identity binding.** -- ✅ **2.1** sidecar NAT stack: QUIC + DCUtR + circuit-relay-v2 (service + client) + AutoNAT + `-announce` + explicit `client.Reserve` + a conn monitor (RELAY/DIRECT). `sidecar/main.go`. -- ◑ **2.2** relay join PROVEN, direct line needs real topology. Confirmed on real boxes: this dev box is genuinely NAT-blocked (direct IPv4 dial fails); it **reserves a relay slot** and **data crosses the relay both ways** to the NAT'd node. DCUtR (the direct hole-punch) stalls in-test: `waiting until we have at least one public address` — the NAT'd node can't confirm its observed/public address with only one peer (libp2p needs several observers/AutoNAT), and a Docker box is likely symmetric-NAT (un-punchable). **Not a code bug** — DCUtR needs the real multi-node network (peers + AutoNAT) and a real home (cone) NAT to activate. Validate the direct line on the actual network, not a 3-box Docker test. +- ✅ **2.1** sidecar NAT stack: QUIC + DCUtR + circuit-relay-v2 (service + client) + AutoNAT + `-announce` + explicit `client.Reserve` + conn monitor (RELAY/DIRECT). On **go-libp2p v0.48** / Go 1.25.11. `sidecar/main.go`. +- ✅ **2.2** relay join AND direct hole-punch both PROVEN. Relay: a genuinely NAT-blocked node reserves a relay slot + data crosses both ways (real boxes + lab). Direct line: built a controlled two-NAT lab with Linux netns (`/tmp/netlab.sh` + `/tmp/holepunch.py`) — **two nodes each behind their own NAT formed a DIRECT QUIC line via DCUtR and moved 100 KB byte-identical** (relay caps ~2 KB, so 100 KB proves it went direct). Required: go-libp2p v0.48, full-cone (UPnP-style) NAT, and a *routable* IP range — TEST-NET (203.0.113.x) is silently `blocked observed address` by libp2p; use real public ranges (11.0.0.x). The earlier "datacenter Docker NAT un-punchable" finding stands (that box is harsher than a home router) — but a full-cone home router punches through; restricted/symmetric fall back to the (proven) relay. - ☐ **2.3** identity ↔ `cwt_` binding: node signs a challenge proving control of (PeerId, cwt_); c0mpute records it (c0mpute-repo change — shard signs, c0mpute records). Unblocked. ## Decisions locked diff --git a/sidecar/go.mod b/sidecar/go.mod index acbae2b..28369e7 100644 --- a/sidecar/go.mod +++ b/sidecar/go.mod @@ -1,88 +1,95 @@ module shard/sidecar -go 1.22.2 +go 1.25.7 require ( - github.com/libp2p/go-libp2p v0.33.2 - github.com/multiformats/go-multiaddr v0.12.3 + github.com/libp2p/go-libp2p v0.48.0 + github.com/multiformats/go-multiaddr v0.16.0 ) require ( + filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect + filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/containerd/cgroups v1.1.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/elastic/gosigar v0.14.2 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect - github.com/francoispqt/gojay v1.2.13 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/huin/goupnp v1.3.0 // indirect - github.com/ipfs/go-cid v0.4.1 // indirect - github.com/ipfs/go-log/v2 v2.5.1 // indirect + github.com/ipfs/go-cid v0.5.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.17.6 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/koron/go-ssdp v0.0.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/libp2p/go-flow-metrics v0.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-nat v0.2.0 // indirect - github.com/libp2p/go-netroute v0.2.1 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-yamux/v4 v4.0.1 // indirect + github.com/libp2p/go-yamux/v5 v5.0.1 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.58 // indirect + github.com/miekg/dns v1.1.66 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect + github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multicodec v0.9.1 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.5.0 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/onsi/ginkgo/v2 v2.15.0 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.18.0 // indirect - github.com/prometheus/client_model v0.6.0 // indirect - github.com/prometheus/common v0.47.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/quic-go v0.42.0 // indirect - github.com/quic-go/webtransport-go v0.6.0 // indirect - github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.1.2 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.18 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect + github.com/pion/stun/v3 v3.1.1 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/webtransport-go v0.10.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - go.uber.org/dig v1.17.1 // indirect - go.uber.org/fx v1.20.1 // indirect - go.uber.org/mock v0.4.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect - golang.org/x/mod v0.15.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.18.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect - lukechampine.com/blake3 v1.2.1 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.41.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/sidecar/go.sum b/sidecar/go.sum index 15c8c47..e7804cc 100644 --- a/sidecar/go.sum +++ b/sidecar/go.sum @@ -1,164 +1,76 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= -github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= -github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= -github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= -github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= -github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= +github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= -github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= -github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= -github.com/libp2p/go-libp2p v0.33.2 h1:vCdwnFxoGOXMKmaGHlDSnL4bM3fQeW8pgIa9DECnb40= -github.com/libp2p/go-libp2p v0.33.2/go.mod h1:zTeppLuCvUIkT118pFVzA8xzP/p2dJYOMApCkFh0Yww= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= +github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= +github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= -github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= -github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= -github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= -github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= -github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -169,8 +81,6 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -179,271 +89,140 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4= -github.com/multiformats/go-multiaddr v0.12.3 h1:hVBXvPRcKG0w80VinQ23P5t7czWgg65BmIvQKjDydU8= -github.com/multiformats/go-multiaddr v0.12.3/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= -github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= -github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= +github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= +github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= -github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= +github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= -github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= -github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= -github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= -github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k= -github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= -github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= -github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY= -github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= -github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= -github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= -go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= -go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= -golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= -lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/sidecar/main.go b/sidecar/main.go index ec9c335..44059b0 100644 --- a/sidecar/main.go +++ b/sidecar/main.go @@ -142,18 +142,15 @@ func newHost(priv crypto.PrivKey, listen string, n natOpts) (host.Host, error) { })) } if n.relayService { - // be a public relay (circuit-relay-v2). Force public reachability so the hop - // service activates immediately — otherwise libp2p waits for AutoNAT to confirm - // reachability, which never happens without AutoNAT-peer infrastructure. - opts = append(opts, libp2p.EnableRelayService(), libp2p.ForceReachabilityPublic()) - } - // NAT'd nodes reserve on relays explicitly (in main) rather than via autorelay's - // background finder — deterministic, observable, and the scheduler hands out the - // circuit address anyway. Force private reachability so DCUtR knows we're the NAT'd - // side and initiates the hole-punch to the public peer. - if len(n.staticRelays) > 0 { - opts = append(opts, libp2p.ForceReachabilityPrivate()) + // be a public relay (circuit-relay-v2) + an AutoNAT server (so NAT'd peers can + // learn their reachability + observed address from us). Force public reachability + // so the hop service activates immediately. + opts = append(opts, libp2p.EnableRelayService(), libp2p.ForceReachabilityPublic(), libp2p.EnableNATService()) } + // NAT'd nodes reserve on relays explicitly (in main) and let AutoNAT + the + // observed-address manager (from several observer peers) determine reachability — so + // DCUtR can hole-punch when the NAT is cone-type. Forcing private here is wrong: it + // leaves holepunch with no public address to offer ("waiting for a public address"). return libp2p.New(opts...) } From 35af386cc7562ea8173be680655ddc93473ca707 Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:58:23 +0000 Subject: [PATCH 06/10] feat: identity-binding proof in the sidecar (shard side of step 2.3) A node proves it controls its libp2p PeerId by signing a challenge with its node key: sidecar -prove -> PEERID + base64 SIG sidecar -verify peerid,nonce,b64sig -> OK/FAIL (reference for the c0mpute verifier) The node-agent sends {PeerId, sig} to c0mpute alongside its cwt_ token; c0mpute verifies and records PeerId <-> account (that half lives in the c0mpute repo, per the boundary law). Tested: correct proof verifies, tampered nonce + impersonated PeerId both fail; and cross-language (Go signs, c0mpute's TS verifier confirms). Step 2 (JOIN) complete. --- STATE.md | 8 +++++--- sidecar/main.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/STATE.md b/STATE.md index 49061bb..0868976 100644 --- a/STATE.md +++ b/STATE.md @@ -16,7 +16,7 @@ Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these fi |---|------|------|--------| | 0 | Engine (pipeline + spec-decode + pipelining) | SERVE | ✅ done | | 1 | libp2p sidecar + per-node identity + data-plane (retire `SHARD_PSK`) | JOIN | ✅ **done** | -| 2 | NAT traversal + bind identity ↔ c0mpute account | JOIN | ◀ **building** | +| 2 | NAT traversal + bind identity ↔ c0mpute account | JOIN | ✅ **done** | | 3 | Manifest + content-addressed weight fetch | JOIN | todo | | 4 | Scheduler + assignment protocol | FORM | todo | | 5 | Job routing + signed receipts + per-node pay | PROVE/PAY | todo | @@ -31,10 +31,12 @@ Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these fi Done & committed: prune of the dead 1.2 bridge, the libp2p receipt, the tail fix. -**Step 2 (building) — NAT + identity binding.** +**Step 2 (JOIN — NAT + identity) DONE.** - ✅ **2.1** sidecar NAT stack: QUIC + DCUtR + circuit-relay-v2 (service + client) + AutoNAT + `-announce` + explicit `client.Reserve` + conn monitor (RELAY/DIRECT). On **go-libp2p v0.48** / Go 1.25.11. `sidecar/main.go`. - ✅ **2.2** relay join AND direct hole-punch both PROVEN. Relay: a genuinely NAT-blocked node reserves a relay slot + data crosses both ways (real boxes + lab). Direct line: built a controlled two-NAT lab with Linux netns (`/tmp/netlab.sh` + `/tmp/holepunch.py`) — **two nodes each behind their own NAT formed a DIRECT QUIC line via DCUtR and moved 100 KB byte-identical** (relay caps ~2 KB, so 100 KB proves it went direct). Required: go-libp2p v0.48, full-cone (UPnP-style) NAT, and a *routable* IP range — TEST-NET (203.0.113.x) is silently `blocked observed address` by libp2p; use real public ranges (11.0.0.x). The earlier "datacenter Docker NAT un-punchable" finding stands (that box is harsher than a home router) — but a full-cone home router punches through; restricted/symmetric fall back to the (proven) relay. -- ☐ **2.3** identity ↔ `cwt_` binding: node signs a challenge proving control of (PeerId, cwt_); c0mpute records it (c0mpute-repo change — shard signs, c0mpute records). Unblocked. +- ✅ **2.3** identity ↔ `cwt_` binding — proven end-to-end, cross-language. **shard signs:** `sidecar -prove ` → {PeerId, sig} with the node key (`-verify` is the reference check). **c0mpute verifies + records:** `c0mpute/lib/identity.ts verifyBindingProof` (Node-native ed25519 + inline base58/PeerId decode, zero new deps), `lib/db.ts` `worker_identity` table + `bindPeerId`/`getPeerIdOwner`, `app/api/node-bind/route.ts` (cwt_-auth + HMAC challenge nonce). Tested: Go signs → TS verifies (correct=true, tampered/impersonated=false), bind→lookup round-trips. c0mpute-side committed in the c0mpute repo (not shard). + +**Step 2 done → JOIN is complete** (engine on the wire @ ~45 tok/s, NAT-traversable, per-node paid identity). Next verb: **FORM** (step 4 scheduler/assignment) — or step 3 (content-addressed weight fetch). ## Decisions locked - **Boundary law:** dependencies point one way — `c0mpute → shard`, never reverse. Shard is a pure engine. diff --git a/sidecar/main.go b/sidecar/main.go index 44059b0..533b0fd 100644 --- a/sidecar/main.go +++ b/sidecar/main.go @@ -21,6 +21,7 @@ import ( "bytes" "context" "crypto/rand" + "encoding/base64" "encoding/binary" "flag" "fmt" @@ -177,12 +178,56 @@ func main() { relaysCSV := flag.String("relays", "", "comma-separated relay /p2p multiaddrs to use when behind NAT") useQuic := flag.Bool("quic", false, "also listen on QUIC (udp) — better hole-punching + lossy links") announce := flag.String("announce", "", "advertise this public multiaddr ahead of auto-detected ones (e.g. /ip4/PUBIP/tcp/PORT)") + prove := flag.String("prove", "", "identity binding: sign this challenge with the node key; print PEERID + SIG") + verify := flag.String("verify", "", "identity binding: verify a proof 'peerid,nonce,b64sig' -> OK/FAIL (reference for c0mpute)") flag.Parse() + // Identity-binding verify: prove a PeerId controls its key, from (peerid, nonce, sig) + // alone — no node key needed. This is the check c0mpute runs (ported to TS) before it + // records PeerId <-> account. ed25519 PeerIds embed the public key, so the verifier + // needs nothing but the proof. + if *verify != "" { + p := strings.SplitN(*verify, ",", 3) + if len(p) != 3 { + log.Fatalf("verify wants 'peerid,nonce,b64sig'") + } + pid, err := peer.Decode(p[0]) + if err != nil { + log.Fatalf("peerid: %v", err) + } + pub, err := pid.ExtractPublicKey() + if err != nil { + log.Fatalf("extract pubkey from peerid: %v", err) + } + sig, err := base64.StdEncoding.DecodeString(p[2]) + if err != nil { + log.Fatalf("sig: %v", err) + } + ok, _ := pub.Verify([]byte(p[1]), sig) + fmt.Printf("VERIFY %v\n", ok) + return + } + priv, err := loadOrCreateKey(*keyPath) if err != nil { log.Fatalf("key: %v", err) } + + // Identity-binding proof: sign a challenge nonce with the node key. The node-agent + // sends {PEERID, SIG} to c0mpute alongside its cwt_ token; c0mpute verifies the sig + // against the PeerId and records PeerId <-> account. Pure crypto — knows nothing of c0mpute. + if *prove != "" { + pid, err := peer.IDFromPublicKey(priv.GetPublic()) + if err != nil { + log.Fatalf("peerid: %v", err) + } + sig, err := priv.Sign([]byte(*prove)) + if err != nil { + log.Fatalf("sign: %v", err) + } + fmt.Printf("PEERID %s\nSIG %s\n", pid, base64.StdEncoding.EncodeToString(sig)) + return + } var staticRelays []peer.AddrInfo for _, s := range strings.Split(*relaysCSV, ",") { if s = strings.TrimSpace(s); s == "" { From e2469732e8b3b25e71138f7247eb1ad7e704c90a Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:00:43 +0000 Subject: [PATCH 07/10] =?UTF-8?q?docs:=20STATE=20=E2=80=94=20step=203=20(c?= =?UTF-8?q?ontent-addressed=20weights)=20is=20the=20remaining=20JOIN=20pie?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/STATE.md b/STATE.md index 0868976..db81160 100644 --- a/STATE.md +++ b/STATE.md @@ -36,7 +36,7 @@ Done & committed: prune of the dead 1.2 bridge, the libp2p receipt, the tail fix - ✅ **2.2** relay join AND direct hole-punch both PROVEN. Relay: a genuinely NAT-blocked node reserves a relay slot + data crosses both ways (real boxes + lab). Direct line: built a controlled two-NAT lab with Linux netns (`/tmp/netlab.sh` + `/tmp/holepunch.py`) — **two nodes each behind their own NAT formed a DIRECT QUIC line via DCUtR and moved 100 KB byte-identical** (relay caps ~2 KB, so 100 KB proves it went direct). Required: go-libp2p v0.48, full-cone (UPnP-style) NAT, and a *routable* IP range — TEST-NET (203.0.113.x) is silently `blocked observed address` by libp2p; use real public ranges (11.0.0.x). The earlier "datacenter Docker NAT un-punchable" finding stands (that box is harsher than a home router) — but a full-cone home router punches through; restricted/symmetric fall back to the (proven) relay. - ✅ **2.3** identity ↔ `cwt_` binding — proven end-to-end, cross-language. **shard signs:** `sidecar -prove ` → {PeerId, sig} with the node key (`-verify` is the reference check). **c0mpute verifies + records:** `c0mpute/lib/identity.ts verifyBindingProof` (Node-native ed25519 + inline base58/PeerId decode, zero new deps), `lib/db.ts` `worker_identity` table + `bindPeerId`/`getPeerIdOwner`, `app/api/node-bind/route.ts` (cwt_-auth + HMAC challenge nonce). Tested: Go signs → TS verifies (correct=true, tampered/impersonated=false), bind→lookup round-trips. c0mpute-side committed in the c0mpute repo (not shard). -**Step 2 done → JOIN is complete** (engine on the wire @ ~45 tok/s, NAT-traversable, per-node paid identity). Next verb: **FORM** (step 4 scheduler/assignment) — or step 3 (content-addressed weight fetch). +**Step 2 done.** JOIN's hard parts are in: engine on the wire @ ~45 tok/s, NAT-traversable, per-node paid identity. Remaining JOIN piece: **step 3** (content-addressed weight fetch — how a node pulls its layer block, trustlessly). Then **FORM** (step 4 scheduler/assignment). ## Decisions locked - **Boundary law:** dependencies point one way — `c0mpute → shard`, never reverse. Shard is a pure engine. From 5078211a00423419e4160a567d79912f23045d04 Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:04:43 +0000 Subject: [PATCH 08/10] long context support for the gpt-oss-120B swarm Context was silently capped at 2048. The fast verify static KV cache was a fixed 2048 with no bounds check and corrupted past it, and the vLLM draft was started with max_model_len 2048. Both are configurable now (--max-ctx on the stages, --max-len on the draft) and an overflow raises a clean ContextOverflow instead of a CUDA out of bounds, so the node logs it and stays up. Long prompts also need memory efficient prefill, which eager attention can't give us: gpt-oss attention sinks block sdpa and the flash sink kernel is Hopper only, so eager was the only option and it OOMs around 8k. Moved the stages onto flex_attention (Ada native, handles sinks and the sliding window, validated at cosine 0.9996 vs eager) and added chunked prefill so per chunk activations stay bounded while the KV cache grows. A 95,690 token prompt now runs through an N=4 split on four 4090s with no OOM, prefill around 207 tok/s, and the model answers it correctly. Decode at long context is still slow on the simple path; graphed flex decode is the next step. Separately, settled the spec decode question: output is deterministic at a fixed K and matches the trusted wire receipt. The differences across different K are floating point non associativity in the batched verify, not a quality regression. Numbers and repro details are in docs/receipts. --- STATE.md | 39 +++++++++++- ...pt-oss-120b-100k-feasibility-20260620.json | 41 +++++++++++++ .../gpt-oss-120b-95k-context-20260620.json | 33 ++++++++++ .../gpt-oss-120b-context-20260620.json | 60 +++++++++++++++++++ phase0/fastverify.py | 12 ++++ phase0/launch_oss.py | 9 +-- phase0/pipeline.py | 17 +++++- phase0/specpipe.py | 54 ++++++++++++----- 8 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 docs/receipts/gpt-oss-120b-100k-feasibility-20260620.json create mode 100644 docs/receipts/gpt-oss-120b-95k-context-20260620.json create mode 100644 docs/receipts/gpt-oss-120b-context-20260620.json diff --git a/STATE.md b/STATE.md index db81160..d033fa9 100644 --- a/STATE.md +++ b/STATE.md @@ -5,12 +5,49 @@ One glance, full picture. The whole network is **5 verbs**. SERVE is done; we're ## The map 1. **JOIN** — a stranger's GPU gets in *(identity, NAT transport, pull its slice of weights)* 2. **FORM** — the network picks nearby nodes and wires them into a swarm *(scheduler, assignment, heal)* -3. **SERVE** — the swarm answers the request, fast — ✅ **DONE** (~40 tok/s gpt-oss-120B, ~30 GLM-5.2 744B over WAN) +3. **SERVE** — the swarm answers the request, fast — ✅ **DONE** at the demo regime (~40 tok/s gpt-oss-120B, ~30 GLM-5.2 744B over WAN). ⚠️ **long-context hardened, not yet production** — see "Serving hardening" below. 4. **PROVE** — each node proves it actually ran its layer *(signed receipts, layer-block spot-check)* 5. **PAY** — each node gets paid for its bit *(per-node, c0mpute rails)* Every line in [docs/INTEGRATION.md](docs/INTEGRATION.md) is just one of these five, done right. +## 100k context (2026-06-20) — PROVEN for prompts; fast generation is the open piece +The "long context is a dealbreaker" worry, resolved on the real rig +([receipt](docs/receipts/gpt-oss-120b-95k-context-20260620.json), [feasibility](docs/receipts/gpt-oss-120b-100k-feasibility-20260620.json)): +- **A distributed gpt-oss-120B swarm (N=4 scattered 4090s) prefilled a 95,690-token prompt and + correctly comprehended it** — no OOM, prefill **207 tok/s** (~8 min). The memory wall is gone. +- **How:** the gpt-oss attention sink blocks sdpa and the flash-sink kernel is Hopper-only, so eager + was the only option and it OOMs ~8k. **flex_attention** is Ada-native, handles sinks+sliding, and + is O(n) — validated cosine 0.9996 vs eager. Plus **chunked prefill** (bounded per-chunk activations, + KV accumulates) and an N=4 split so 95k KV fits a 24GB card. Wired in `pipeline.run_block` + + `specpipe` (`--attn flex_attention`, `--prompt-file`, `--prefill-chunk`). +- **Open: generation speed at long ctx.** This run used the simplest decode (8-hop relay, plain greedy, + per-step flex) → 0.51 tok/s @95k (2.57 @2k). Fix path: route **decode** through the graphed eager + StaticKV path (eager attn with q=K+1 is cheap even at 95k → ~30 tok/s proven short-ctx) with flex + used only for prefill; + direct-return + spec-decode; + fp8/sliding-window-ring KV. Prefill is already fast. + +## Serving hardening (2026-06-20) — long-context + losslessness, real 4×4090 runs +A reported "output breaks past ~20k context / spec-decode degrades quality" sent us back to the rig +(4 scattered RTX 4090s: CA·NC·WA stages + Utah coord/draft). Findings, all from live runs +([receipt](docs/receipts/gpt-oss-120b-context-20260620.json)): +- **"Breaks past 2048" was TWO independent hard walls, both now fixed.** (1) The `--fast` path's static + KV cache was `maxlen=2048` with **no bounds check** → silent CUDA corruption past it. Fix: **`--max-ctx`** + sizes the cache + a **`ContextOverflow`** guard fails clean (proven: 2480-tok prompt at max_ctx=2048 → + named error, stage stays up). (2) The vLLM draft ran `max_model_len=2048` → generation past 2048 hung the + draft. Fix: launchers pass **`--max-len`**. **Proof:** with both lifted, the swarm sustained greedy decode to + **2633 context (585 past the old wall) at 23 tok/s** (sha `8a70cdee…`). +- **Two remaining ceilings (quantified, not fixed):** eager **prefill** of a >~2k-token *prompt* OOMs on a + 24GB/12-layer card (~3.2 GB transient, caught cleanly); and the per-step mask is **O(max_ctx)** so decode + slows with context (36.7 tok/s @2k → 22 @10k). KV cache ≈72 KB/token; safe `max_ctx`≈10k. **20k needs** + flash/chunked prefill + incremental mask + fp8 KV, or more stages (fewer layers/box). +- **Losslessness, adjudicated:** **fixed-K is deterministic** (K4 vs K4 bit-identical; matches the trusted-wire + receipt) — but **cross-K diverges at a floating-point near-tie** (K4 vs K8 first-diff @ token 92), FP + non-associativity in the batched CUDA-graph verify, **not** quality degradation. Each output is a valid greedy + decode. So the critic's claim is half-right and reframed: spec-decode ≠ lossy; it's cross-K FP non-determinism. +- **Also fixed:** the swarm bring-up's zombie-draft teardown (a `pkill -f draft_server` was self-matching the + kill shell → boxes never freed; now kills by port+GPU pid). Noted gap: ring **forward links don't auto-recover** + on coordinator churn (fault-tolerance, step 7). + ## Build steps (→ verb · status) | # | Step | Verb | Status | |---|------|------|--------| diff --git a/docs/receipts/gpt-oss-120b-100k-feasibility-20260620.json b/docs/receipts/gpt-oss-120b-100k-feasibility-20260620.json new file mode 100644 index 0000000..f259794 --- /dev/null +++ b/docs/receipts/gpt-oss-120b-100k-feasibility-20260620.json @@ -0,0 +1,41 @@ +{ + "run_id": "gpt-oss-120b-100k-feasibility-20260620", + "utc": "2026-06-20", + "goal": "100k-context prompts at usable speed on scattered consumer 4090s", + "question": "gpt-oss has attention sinks; can it do memory-efficient (O(n)) prefill on Ada (4090)? eager is O(n^2) and OOMs ~8k; flash-attn-3 sink kernel is Hopper-only.", + "findings": { + "eager": "OOMs at ~8k even for 4 layers (O(n^2) scores). Not viable for long context.", + "sdpa": "UNSUPPORTED for GptOssForCausalLM (sinks block the sdpa path).", + "flash_attention_2/vllm-flash-attn3": "'S aux only supported on Hopper' -> dead on Ada 4090.", + "flex_attention": "WORKS on Ada with sinks + sliding window. THE kernel." + }, + "flex_validated_on_real_weights": { + "correctness_vs_eager_9layers": { + "cosine": 0.999593, + "rel_err": 0.033, + "note": "max|d|=31 is a massive-activation outlier, not a bug; sinks handled (zeroing sinks doesn't change the gap). Faithful greedy decode." + }, + "chunked_prefill_real_9layer_stage": { + "reached_ctx": 100352, + "peak_gb": 20.8, + "seconds": 22, + "chunk": 2048, + "scaling": "~linear, ~40KB/tok for 9 layers (KV-bound); bounded chunk activations" + } + }, + "fit_math_24GB_card": { + "kv_per_tok_per_layer_KB": 2, + "sliding_layers_bounded_to_window_128": true, + "N4_9layers": "weights ~15.25GB + 100k KV ~1.8GB + chunk ~4GB = ~21GB FITS", + "N6_6layers": "fits with large headroom (~360k ctx)", + "conclusion": "100k-131k feasible at N=4; more nodes or fp8/int4 KV -> 400k+" + }, + "build_plan": [ + "run_block: build flex masks via transformers.masking_utils (create_causal_mask + create_sliding_window_causal_mask) when attn=flex; keep eager path unchanged", + "chunked prefill: coordinator sends the prompt in chunks of C; serve_spec runs each via run_block into a DynamicCache (bounded activations)", + "bring up N=4 (or N=6) swarm with --attn flex, non-fast serve_spec path (DynamicCache, grows dynamically)", + "prove a real 100k prompt end-to-end + tok/s receipt", + "later (speed): flex-ify the CUDA-graph fast decode path; incremental O(1) decode mask; fp8/int4 KV; sliding-window ring-buffer KV" + ], + "speed_expectation": "prefill 100k ~tens of seconds (chunked, pipelined); decode on the non-fast path ~5-6 tok/s (graph-less) -> usable for long-context; fast-path flex-ification is the follow-up to recover ~30 tok/s" +} \ No newline at end of file diff --git a/docs/receipts/gpt-oss-120b-95k-context-20260620.json b/docs/receipts/gpt-oss-120b-95k-context-20260620.json new file mode 100644 index 0000000..a864f6a --- /dev/null +++ b/docs/receipts/gpt-oss-120b-95k-context-20260620.json @@ -0,0 +1,33 @@ +{ + "run_id": "gpt-oss-120b-95k-context-20260620", + "utc": "2026-06-20", + "goal": "100k-context prompts at usable speed on scattered consumer 4090s", + "swarm": "N=4 scattered RTX 4090 stages (CA,NC,WA,CA) + coordinator (Utah), WAN, flex_attention, non-fast relay", + "achieved": { + "prompt_tokens": 95690, + "context_reached": 95722, + "no_oom": true, + "prefill_seconds": 463, + "prefill_tok_s": 206.7, + "comprehension": "CORRECT \u2014 the 120B identified the repeated content AND the trailing question", + "answer_preview": "analysisThe user repeated a long phrase many times. The question: \"In one sentence, what do these notes describe?\" The notes are the repeated sentence about", + "output_sha256": "47c03272c1c38e26958b0a66416a4dc7c5538b8d3a3b0a33fd6e791729f85334" + }, + "how": [ + "flex_attention (Ada-native, sinks+sliding) via transformers masking_utils in pipeline.run_block", + "chunked prefill (coordinator sends the prompt in 4096-tok chunks; each stage appends to a DynamicCache) -> per-chunk activations bounded, KV accumulates ~linearly", + "N=4 split (9 layers/stage) so weights + 95k KV fit a 24GB card" + ], + "gap": { + "decode_tok_s": 0.51, + "why": "this run used the SIMPLEST decode path: 8-hop non-direct relay return, plain greedy (1 tok/traversal), no spec-decode, and flex attention over 95k recomputed per step. ~2s/token.", + "fix_path": [ + "direct-return (8 hops -> 4)", + "spec-decode + pipelining (K tokens/traversal, amortize WAN)", + "graphed/bucketed flex decode (avoid per-step mask rebuild over 95k)", + "sliding-window ring-buffer KV (half the layers need only 128) + fp8 KV -> more headroom/speed" + ], + "estimate": "direct + spec-decode alone ~> 3-5 tok/s at 95k; graphed flex + sliding-KV target ~10+ tok/s" + }, + "verdict": "100k-class context PROMPTS proven on the swarm (prefill 207 tok/s, correct comprehension, no OOM). The memory dealbreaker is solved. Fast GENERATION at long context is the remaining optimization." +} \ No newline at end of file diff --git a/docs/receipts/gpt-oss-120b-context-20260620.json b/docs/receipts/gpt-oss-120b-context-20260620.json new file mode 100644 index 0000000..1e89312 --- /dev/null +++ b/docs/receipts/gpt-oss-120b-context-20260620.json @@ -0,0 +1,60 @@ +{ + "run_id": "gpt-oss-120b-context-20260620", + "utc": "2026-06-20", + "model": "gpt-oss-120b (mxfp4)", + "swarm": "3 scattered RTX 4090 stages over WAN + layerless coordinator/draft", + "topology": "CA(head, layers 0-12) -> NC(mid, 12-24) -> WA(tail, 24-36); coord+draft Utah", + "what": "Serving-hardening pass: found and fixed the long-context failure ('breaks past ~2k/20k') and adjudicated the spec-decode losslessness claim. Real runs on the live 4-box swarm.", + "headline": { + "tok_s_warm": 36.73, + "tok_s_note": "K=4 pipe depth=4 warm (launch_oss --compare) at default max_ctx=2048 -> matches prior ~40", + "context_breaks_past_2048": "TRUE, and had TWO independent hard walls, both fixed", + "losslessness": "fixed-K deterministic (bit-identical, matches trusted-wire receipt); cross-K diverges at FP near-ties (not degradation)" + }, + "context_findings": { + "wall_1_stage_static_cache": { + "cause": "phase0/fastverify.py FastVerify allocated a static KV cache maxlen=2048 with NO bounds check; writing past it = silent CUDA out-of-bounds corruption", + "fix": "--max-ctx sizes the cache to the request; new ContextOverflow guard fails clean before any OOB write", + "proven": "at max_ctx=2048 a 2480-token prompt -> clean ContextOverflow, stage logs it and STAYS UP (was: silent corruption)" + }, + "wall_2_draft_max_model_len": { + "cause": "the vLLM gpt-oss-20b draft ran with max_model_len=2048 (draft_server.py default); generation past 2048 exceeds it and the draft request hangs -> whole pipeline stalls", + "fix": "launchers now pass --max-len (== max_ctx) to the draft", + "proven": "draft.log showed 'max_model_len: 2048'; runs hung at the 2048 boundary until lifted" + }, + "ceiling_3_prefill_attention_memory": { + "finding": "eager prefill of a >~2k-token PROMPT OOMs on a 24GB card holding 12 layers (needs ~3.24 GiB transient, ~2 GiB free after 22.2 GiB weights). Caught cleanly (OutOfMemoryError -> stage resets, stays up).", + "implication": "long PROMPTS (conversations) are capped ~2k here; long GENERATION (decode) is the path that scales", + "next": "flash/chunked prefill, or fewer layers/box (more stages)" + }, + "ceiling_4_vram_and_speed": { + "kv_cache": "~72 KB/token; max_ctx=24576 (~1.8 GB) OOMs the prefill on 24GB/12-layer; safe ~10k", + "speed_tradeoff": "per-step mask is recomputed O(max_ctx): 36.7 tok/s @2k -> 22 @10k -> ~2 @24k", + "next": "incremental O(K+1) mask; fp8 KV; more stages -> reach 20k" + } + }, + "context_proof_decode_path": { + "claim": "swarm sustained greedy decode PAST the old 2048 wall, both walls lifted", + "max_ctx": 4096, + "draft_max_len": 4096, + "prompt_len": 133, + "tokens_generated": 2500, + "context_reached": 2633, + "crossed_2048_by": 585, + "tok_s": 23.04, + "seconds": 121, + "output_sha256": "8a70cdee4fe651300b4175fa4ef460121e7bc48efdc29a9c4185b86cf9f44f5d" + }, + "losslessness_proof": { + "method": "same prompt, compare output token ids by first-divergence index", + "fixed_K_K4_vs_K4": "first_diff = -1 (IDENTICAL, 0/221) -> deterministic at a fixed config", + "cross_K_K4_vs_K8": "first_diff = 92, 127/220 differ -> diverges at a floating-point near-tie, cascades", + "verdict": "spec-decode is bit-reproducible at a fixed config (matches trusted-wire receipt); cross-K differences are FP non-associativity in the batched CUDA-graph verify, NOT quality degradation. Each output is a valid greedy decode. True cross-K bit-exactness needs deterministic-reduction kernels." + }, + "fix_files": [ + "phase0/fastverify.py (ContextOverflow + bounds)", + "phase0/specpipe.py (--max-ctx plumbed to both serve paths; ignore_eos)", + "phase0/launch_oss.py (--max-ctx + draft --max-len passthrough)" + ], + "honest_status": "SERVE works at the demo regime and now past 2048 on the decode path with the fix. Production-grade long context (20k) is NOT there yet on 24GB/12-layer cards: bounded by eager-prefill memory and the O(max_ctx) decode-mask cost. Path is known (flash/chunked prefill, incremental mask, fp8 KV, more stages)." +} \ No newline at end of file diff --git a/phase0/fastverify.py b/phase0/fastverify.py index 3fdb1f5..8f0408f 100644 --- a/phase0/fastverify.py +++ b/phase0/fastverify.py @@ -19,6 +19,12 @@ from tree import tree_mask +class ContextOverflow(Exception): + """A request needs more positions than the static cache (max_ctx) holds. Raised + BEFORE any out-of-bounds write, so the cache is never corrupted — the serve loop + catches it, resets the edge, and the node stays up (vs a silent CUDA OOB).""" + + class StaticKV: """fixed [1, kv_heads, MAXLEN, head_dim] K/V per layer; update() writes at the cache's own index buffer (self.cp) and returns the full buffer for masked attention.""" @@ -60,6 +66,9 @@ def _layers(self, x, pos, pe, mf, mw): def prefill(self, h, start): # eager, any length n = h.shape[1] + if start + n > self.maxlen: # fail clean, never corrupt the cache + raise ContextOverflow(f"prefill needs {start + n} positions > max_ctx {self.maxlen} " + f"(raise --max-ctx)") self.cache.cp = torch.arange(start, start + n, device=self.dev) pos = self.cache.cp.unsqueeze(0) mf = _causal_mask(n, self.maxlen, start, 0, torch.bfloat16, self.dev) @@ -88,6 +97,9 @@ def _graph_body(self): self.mf_buf, self.mw_buf) def decode(self, h, start): # fixed q_len, graphed + if start + h.shape[1] > self.maxlen: # fail clean before any OOB write + raise ContextOverflow(f"decode needs {start + h.shape[1]} positions > max_ctx " + f"{self.maxlen} (raise --max-ctx)") if self.kp1 != h.shape[1]: # (re)build for this K+1, capture once self._build(h.shape[1]); self.graph = None self._set(h, start) diff --git a/phase0/launch_oss.py b/phase0/launch_oss.py index 870b5af..efc77b1 100644 --- a/phase0/launch_oss.py +++ b/phase0/launch_oss.py @@ -64,7 +64,7 @@ def fire(inst, cmd, timeout=25): pass -def launch_stage(inst, stage, nstages, nxt_ep, served_head): +def launch_stage(inst, stage, nstages, nxt_ep, served_head, max_ctx=2048): is_tail = stage == nstages - 1 nextarg = f" --next {nxt_ep}" if nxt_ep else "" head = " --served-head" if served_head else "" @@ -73,7 +73,7 @@ def launch_stage(inst, stage, nstages, nxt_ep, served_head): f"fuser -k {PORT}/tcp 2>/dev/null; sleep 2; rm -f /root/stage.log; cd /root && " f"SHARD_PSK={PSK} setsid bash -c 'python3 specpipe.py --stage {stage} --nstages {nstages} " f"--model {M120} --listen-port {PORT}{nextarg}{head} --fast --direct-return " - f"--timeout 300 > /root/stage.log 2>&1' /dev/null 2>&1 &") + f"--max-ctx {max_ctx} --timeout 300 > /root/stage.log 2>&1' /dev/null 2>&1 &") fire(inst, cmd) @@ -102,6 +102,7 @@ def main(): ap.add_argument("--tree", default="", help="run FAST graphed tree spec 'w,d' instead of the linear compare") ap.add_argument("--dump", action="store_true", help="write the warm run (ids+hash+sync-match) to /root/run.json for the receipt") ap.add_argument("--skip-draft", action="store_true", help="reuse a draft already running on coord") + ap.add_argument("--max-ctx", type=int, default=2048, help="fast-verify static cache size (prompt+gen ceiling)") a = ap.parse_args() sids = [int(x) for x in a.stages.split(",")] @@ -141,13 +142,13 @@ def main(): f"fuser -k {DRAFT_PORT}/tcp 2>/dev/null; sleep 2; rm -f /root/draft.log; cd /root && " f"SHARD_PSK={PSK} CUDA_VISIBLE_DEVICES=0 setsid bash -c " f"'/root/vllmenv/bin/python draft_server.py --model {M20} --port {DRAFT_PORT} " - f"> /root/draft.log 2>&1' /dev/null 2>&1 &") + f"--max-len {a.max_ctx} > /root/draft.log 2>&1' /dev/null 2>&1 &") print("[launch] stages tail-first; wait each to listen before launching its predecessor " "(120B partial load ~1-2min/node, so a predecessor never connects to a dead successor)...", flush=True) for k in range(nstages - 1, -1, -1): nxt = f"{eps[k+1][0]}:{eps[k+1][1]}" if k < nstages - 1 else None - launch_stage(stages[k], k, nstages, nxt, served_head=(k == 0)) + launch_stage(stages[k], k, nstages, nxt, served_head=(k == 0), max_ctx=a.max_ctx) label, ok = warm_stage(stages[k], f"stage{k} {stages[k]['id']}") print(f" {'OK ' if ok else 'FAIL '}{label}", flush=True) if not ok: diff --git a/phase0/pipeline.py b/phase0/pipeline.py index a2abea6..f129f66 100644 --- a/phase0/pipeline.py +++ b/phase0/pipeline.py @@ -47,11 +47,22 @@ def run_block(h, parts, cache, start, par=None, dep=None): q_len = h.shape[1] sliding = parts.get("sliding") win_sz = parts.get("window", 0) - if par is not None: # tree verify + flex = getattr(parts["_model"].config, "_attn_implementation", "eager") == "flex_attention" + if par is not None: # tree verify (eager additive mask) from tree import tree_mask pos = torch.tensor([[start + dep[i] for i in range(q_len)]], device=h.device) full = tree_mask(par, dep, start, 0, h.dtype, h.device) win = tree_mask(par, dep, start, win_sz, h.dtype, h.device) if (sliding and win_sz) else full + elif flex: # flex_attention: O(n) prefill on Ada, sinks+sliding ok + import transformers.masking_utils as _mu + cfg = parts["_model"].config + cp = torch.arange(start, start + q_len, device=h.device) + pos = cp.unsqueeze(0) + full = _mu.create_causal_mask(config=cfg, input_embeds=h, attention_mask=None, + cache_position=cp, past_key_values=cache, position_ids=pos) + win = (_mu.create_sliding_window_causal_mask(config=cfg, input_embeds=h, attention_mask=None, + cache_position=cp, past_key_values=cache, position_ids=pos) + if (sliding and win_sz) else full) else: kv_len = start + q_len pos = torch.arange(start, kv_len, device=h.device).unsqueeze(0) @@ -67,7 +78,7 @@ def run_block(h, parts, cache, start, par=None, dep=None): return h -def load_stage(model_id, stage, nstages, device="cuda", dtype="auto"): +def load_stage(model_id, stage, nstages, device="cuda", dtype="auto", attn="eager"): """load ONLY this stage's contiguous block of layers onto the GPU (+ embed on the head, norm/lm_head on the tail). every other component is mapped to "meta" so it is never loaded -- this is what lets a node hold a slice of a model far @@ -89,7 +100,7 @@ def load_stage(model_id, stage, nstages, device="cuda", dtype="auto"): dmap[f"model.layers.{j}"] = device if lo <= j < hi else "meta" print(f"[s{stage}] loading layers [{lo}:{hi}] of {model_id} ...", flush=True) model = AutoModelForCausalLM.from_pretrained(model_id, dtype=dtype, device_map=dmap, - attn_implementation="eager") + attn_implementation=attn) m = model.model parts = {"rotary": m.rotary_emb, "n_layers": n_layers, "lo": lo, "hi": hi, "_model": model} if is_head: diff --git a/phase0/specpipe.py b/phase0/specpipe.py index 75fe8c4..db33322 100644 --- a/phase0/specpipe.py +++ b/phase0/specpipe.py @@ -147,7 +147,7 @@ def serve_tail_direct(parts, listen_port, timeout, dev): break -def serve_spec_fast(parts, stage, nstages, listen_port, nxt, timeout, dev, direct=False): +def serve_spec_fast(parts, stage, nstages, listen_port, nxt, timeout, dev, direct=False, max_ctx=2048): """serve_spec with the FAST verify: a static-cache CUDA-graph stage forward (~5x cheaper than eager). LINEAR spec only (the graph is a fixed K+1 shape; tree is variable). first verify after reset = prefill (eager, prompt-length); every later @@ -161,8 +161,8 @@ def serve_spec_fast(parts, stage, nstages, listen_port, nxt, timeout, dev, direc print(f"[s{stage}] connected forward to stage {stage+1} at {nxt}", flush=True) srv = socket.socket(); srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", listen_port)); srv.listen(1) - fv = FastVerify(parts, dev=dev) - print(f"[s{stage}] listening on :{listen_port} (FAST verify, edge timeout {timeout:.0f}s)", flush=True) + fv = FastVerify(parts, maxlen=max_ctx, dev=dev) + print(f"[s{stage}] listening on :{listen_port} (FAST verify, max_ctx={max_ctx}, edge timeout {timeout:.0f}s)", flush=True) while True: conn, addr = srv.accept(); conn.settimeout(timeout) print(f"[s{stage}] stage {stage-1} connected from {addr}", flush=True) @@ -214,13 +214,13 @@ def serve_spec_fast(parts, stage, nstages, listen_port, nxt, timeout, dev, direc break -def serve_tail_fast(parts, listen_port, timeout, dev): +def serve_tail_fast(parts, listen_port, timeout, dev, max_ctx=2048): """direct-return tail with the FAST verify (see serve_spec_fast + serve_tail_direct).""" import select srv = socket.socket(); srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", listen_port)); srv.listen(2) - fv = FastVerify(parts, dev=dev) - print(f"[tail] listening on :{listen_port} (FAST verify, direct return, edge timeout {timeout:.0f}s)", flush=True) + fv = FastVerify(parts, maxlen=max_ctx, dev=dev) + print(f"[tail] listening on :{listen_port} (FAST verify, max_ctx={max_ctx}, direct return, edge timeout {timeout:.0f}s)", flush=True) while True: c1, _ = srv.accept(); c2, _ = srv.accept() ready, _, _ = select.select([c1, c2], [], [], timeout) @@ -380,7 +380,7 @@ def embed(tokens): # python list -> this node's block o def coordinate(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, - adaptive=False, k_min=1, k_max=12, ret_sock=None): + adaptive=False, k_min=1, k_max=12, ret_sock=None, prefill_chunk=0): """the in-house coordinator (c0mpute entry node): holds NO 120B layers. it tokenizes, queries the in-house draft for K tokens, sends token ids into the swarm's stage 0 (which embeds + runs), reads back the verify, greedy-accepts. @@ -397,8 +397,17 @@ def coordinate(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, out = [] try: send_msg(pipe_sock, {"op": "reset"}); recv_msg(rx) - send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids, "start": 0}) # prefill - cur = recv_msg(rx)[-1] + # prefill: one shot, or CHUNKED (long prompts) so each stage's per-chunk activation + # stays bounded (the KV cache accumulates). flex_attention keeps each chunk O(n) on Ada. + if prefill_chunk and len(prompt_ids) > prefill_chunk: + r = None + for i in range(0, len(prompt_ids), prefill_chunk): + send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids[i:i + prefill_chunk], "start": i}) + r = recv_msg(rx) + cur = r[-1] + else: + send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids, "start": 0}) # prefill + cur = recv_msg(rx)[-1] pos = len(prompt_ids) out = [cur] rounds, accepted_total = 0, 0 @@ -443,7 +452,8 @@ def coordinate(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, } -def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, depth, ret_sock=None): +def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, depth, ret_sock=None, + ignore_eos=False): """PIPELINED coordinator: keep `depth` verify chunks in flight over the ring, so throughput approaches the ring's per-chunk THROUGHPUT, not its full latency (the GLM-pipe lever, on the gpt-oss fast-verify path). Same K+1-token chunk as the @@ -503,7 +513,7 @@ def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, dep recv_msg(draft_sock) # outstanding draft is stale -> drop it dprefix = prompt_ids + out; send_pos = pos # re-draft from the corrected prefix send_msg(draft_sock, {"ids": dprefix, "k": K}) # re-prime from the corrected prefix - if len(out) >= max_new or cur == eos or eos in committed: + if len(out) >= max_new or (not ignore_eos and (cur == eos or eos in committed)): done = True recv_msg(draft_sock) # drain the outstanding draft request while inflight: # drain unread results -> sockets clean for next gen @@ -653,6 +663,14 @@ def main(): ap.add_argument("--K", type=int, default=6) ap.add_argument("--adaptive", action="store_true", help="tune K live from the running acceptance rate") ap.add_argument("--fast", action="store_true", help="serve node: static-cache CUDA-graph verify (~5x, fixed-K linear)") + ap.add_argument("--attn", default="eager", help="attention impl for stages: eager | flex_attention " + "(flex = O(n) prefill on Ada, needed for long context with gpt-oss sinks)") + ap.add_argument("--prompt-file", default="", help="read the prompt from this file (for 100k-token prompts)") + ap.add_argument("--prefill-chunk", type=int, default=0, help="chunk the prefill into this many tokens " + "(0=one shot); long prompts need chunking so per-chunk activations stay bounded") + ap.add_argument("--max-ctx", type=int, default=2048, help="fast-verify static cache size (prompt+gen ceiling); " + "sized to the request, not a hardware limit — the KV cache is ~tens of KB/token, so a 4090 " + "stage holds far more than the old 2048 default. overflow fails clean (ContextOverflow), never corrupts") ap.add_argument("--sweep", default="", help="comma K list to measure on one load, 0=adaptive (e.g. 2,3,4,0)") ap.add_argument("--pipe", action="store_true", help="coordinator: PIPELINED spec-decode (depth chunks in flight; needs --direct-return)") ap.add_argument("--depth", type=int, default=4, help="pipelined coordinator: verify chunks in flight") @@ -666,6 +684,8 @@ def main(): ap.add_argument("--timeout", type=float, default=120.0) args = ap.parse_args() wire.key_from_env() # shared swarm key (SHARD_PSK); fail fast before the model load + if args.prompt_file: # long (100k) prompts can't fit on the CLI + args.prompt = open(args.prompt_file).read() if args.coordinator: # in-house entry node: no 120B, just tokenizer + draft + swarm tok = AutoTokenizer.from_pretrained(args.model) # 20b tokenizer == 120b tokenizer @@ -754,7 +774,8 @@ def main(): for kv in ks: adaptive = (kv == 0) or (not args.sweep and args.adaptive) r = coordinate(draft_sock, pipe_sock, tok, args.prompt, (6 if kv == 0 else kv), - args.max_new, args.timeout, adaptive=adaptive, ret_sock=ret_sock) + args.max_new, args.timeout, adaptive=adaptive, ret_sock=ret_sock, + prefill_chunk=args.prefill_chunk) if args.sweep: print(f"[SWEEP K={kv}] {r['tok_s']:.2f} tok/s | {r['toks_per_traversal']:.2f} tok/traversal | " f"accept {r['mean_accept']:.2f} | draft {r['draft_ms']:.0f}ms + verify {r['verify_ms']:.0f}ms/round", flush=True) @@ -772,15 +793,18 @@ def main(): print(f"[coord] dumped run -> {args.dump} (sha256 {hashlib.sha256(json.dumps(ids).encode()).hexdigest()[:16]}..)", flush=True) return - parts = load_stage(args.model, args.stage, args.nstages, device=args.device) + parts = load_stage(args.model, args.stage, args.nstages, device=args.device, attn=args.attn) if args.stage != 0 or args.served_head: # swarm serve node (stage 0 embeds token ids) is_tail = args.stage == args.nstages - 1 if args.direct_return and is_tail: - (serve_tail_fast if args.fast else serve_tail_direct)(parts, args.listen_port, args.timeout, args.device) + if args.fast: + serve_tail_fast(parts, args.listen_port, args.timeout, args.device, max_ctx=args.max_ctx) + else: + serve_tail_direct(parts, args.listen_port, args.timeout, args.device) elif args.fast: serve_spec_fast(parts, args.stage, args.nstages, args.listen_port, args.next, args.timeout, - args.device, direct=args.direct_return) + args.device, direct=args.direct_return, max_ctx=args.max_ctx) else: serve_spec(parts, args.stage, args.nstages, args.listen_port, args.next, args.timeout, args.device, direct=args.direct_return) From 46c4a533a2247206657be6cee4365ada85514a1a Mon Sep 17 00:00:00 2001 From: leyten <86632281+leyten@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:58:19 +0000 Subject: [PATCH 09/10] usable decode at long context: flex prefill, graphed eager decode, windowed-draft spec-decode Long-context decode was unusably slow, about 0.5 tok/s at 95k, because flex recompiled every decode step as the KV grew. Two changes fix it. 1. Split prefill and decode. Prefill stays on flex (memory efficient, the only thing that survives a big query on Ada). Decode flips the layers back to eager and runs the existing CUDA graphed path: eager attention with q=K+1 is cheap even over 100k keys and the graph is a fixed-shape replay, so nothing recompiles. That alone took plain decode to 3.5 tok/s at 95k. 2. Spec-decode with a windowed draft. A full-context draft is ~800ms/round at 95k and kills spec-decode. The draft only needs recent context to predict the next tokens, so the draft query is windowed to the last few thousand tokens (--draft-ctx) while the full swarm still verifies with the complete context. That drops the draft to ~100ms and hides it behind the verify. With draft_ctx=2048, depth 4, spec-decode runs at 7.6 tok/s at 95k with correct output, about 15x the naive path. Mechanics: FastVerify does a chunked flex prefill into its static cache (mask via _compile=True so it doesn't materialise the dense mask and OOM); the fast serve loop routes prefill chunks vs decode by a flag and forwards it down the ring (else a middle stage treats a prefill chunk as a decode and tries to allocate a 49GB eager score matrix); coordinate_pipe chunks the prefill and windows the draft. The verify round-trip (about 250ms over four WAN-separated stages) is the floor now. Next lever for 10+ is sliding window KV: half the layers only need a 128 key window but currently read the whole buffer. Numbers in docs/receipts. --- STATE.md | 12 +++-- .../gpt-oss-120b-100k-decode-20260620.json | 48 +++++++++++++++++++ phase0/fastverify.py | 28 +++++++++-- phase0/specpipe.py | 36 ++++++++++---- 4 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 docs/receipts/gpt-oss-120b-100k-decode-20260620.json diff --git a/STATE.md b/STATE.md index d033fa9..533a85b 100644 --- a/STATE.md +++ b/STATE.md @@ -21,10 +21,14 @@ The "long context is a dealbreaker" worry, resolved on the real rig is O(n) — validated cosine 0.9996 vs eager. Plus **chunked prefill** (bounded per-chunk activations, KV accumulates) and an N=4 split so 95k KV fits a 24GB card. Wired in `pipeline.run_block` + `specpipe` (`--attn flex_attention`, `--prompt-file`, `--prefill-chunk`). -- **Open: generation speed at long ctx.** This run used the simplest decode (8-hop relay, plain greedy, - per-step flex) → 0.51 tok/s @95k (2.57 @2k). Fix path: route **decode** through the graphed eager - StaticKV path (eager attn with q=K+1 is cheap even at 95k → ~30 tok/s proven short-ctx) with flex - used only for prefill; + direct-return + spec-decode; + fp8/sliding-window-ring KV. Prefill is already fast. +- **Decode at long ctx — usable.** Naive decode was 0.51 tok/s @95k (flex recompiled every step as KV + grew). Fix: **flex for prefill, eager for decode.** Decode flips the layers to eager and uses the + existing CUDA-graphed path (q=K+1 eager attention is cheap even over 100k keys; fixed-shape replay, no + recompile); flex only does the big-query prefill. End to end on the N=4 swarm: **92k prompt, prefill + 157 tok/s, decode 3.5 tok/s** (7× the naive path), correct output + ([receipt](docs/receipts/gpt-oss-120b-100k-decode-20260620.json)). Usable for long-context (non-interactive). +- **More decode speed (next):** sliding-window ring-buffer KV (half the layers only need a 128-key read → + ~2×), spec-decode (needs a long-context-tuned draft; the vanilla 20B draft OOMs at 100k), fp8 KV, fewer stages. ## Serving hardening (2026-06-20) — long-context + losslessness, real 4×4090 runs A reported "output breaks past ~20k context / spec-decode degrades quality" sent us back to the rig diff --git a/docs/receipts/gpt-oss-120b-100k-decode-20260620.json b/docs/receipts/gpt-oss-120b-100k-decode-20260620.json new file mode 100644 index 0000000..b5c4050 --- /dev/null +++ b/docs/receipts/gpt-oss-120b-100k-decode-20260620.json @@ -0,0 +1,48 @@ +{ + "run_id": "gpt-oss-120b-100k-decode-20260620", + "utc": "2026-06-20", + "swarm": "N=4 scattered RTX 4090 stages, flex_attention, fast path, direct-return", + "what": "usable long-context decode: flex prefill + graphed eager decode hybrid", + "result": { + "prompt_tokens": 92202, + "context_reached": 92251, + "prefill_tok_s": 157.3, + "decode_before": 0.51, + "speedup": 6.8, + "answer_correct": true, + "answer_preview": "analysisThe user repeated a description many times, then asks: \"in one sentence, what do these notes describe?\" The notes describe distributed inference splitti", + "output_sha256": "9e65a03684ba104ecba29a2df75c3dc8ba950769e44a9394dfec79ae0187f14d", + "plain_decode_tok_s": 3.49 + }, + "single_stage_validation": { + "decode_ms_per_step_at_12k": 9.4, + "decode_ms_per_step_at_100k": 37.7, + "note": "eager graphed decode; cost scales with max_ctx (full-buffer attention), not actual ctx" + }, + "why_it_works": "flex recompiled per decode step as KV grew (0.5 tok/s). Decode now uses eager attention (q=K+1 is cheap even over 100k keys) on the existing CUDA-graph path (fixed shape, no recompile); flex is used only for the big-query prefill. config._attn_implementation is flipped per phase.", + "further_speed": [ + "sliding-window KV: ~half the layers (window 128) only need a 128-key read, not the full buffer -> ~2x decode", + "spec-decode: K tokens/traversal, but needs a long-context-tuned draft (vanilla 20B draft OOMs at 100k)", + "fewer/faster stages, fp8 KV" + ], + "spec_decode": { + "what": "windowed-draft spec-decode over the hybrid swarm at 95k", + "decode_tok_s": 7.57, + "config": "draft_ctx=2048, depth=4, K=4", + "tok_per_traversal": 3.18, + "accept": 2.18, + "draft_ms": 97, + "verify_recv_ms": 254, + "key_insight": "a full-context draft is ~800ms/round at 95k and kills spec-decode; windowing the draft query to recent tokens (--draft-ctx) drops it to ~100ms (hidden behind the verify), while the full swarm still verifies with complete context", + "sweep_best": "draft_ctx 2048/512 with depth 4 ~7.9 (short-prefill estimate); depth>=16 thrashes (verify can't keep up with fill)", + "speedup_vs_naive": 14.8, + "depth_K_sweep": { + "best": "depth=4 K=4 ~9.7 tok/s (short-prefill estimate; favourable prompt)", + "confirmed_real_95k": "7.57 tok/s (depth=4 K=4, repetitive prompt)", + "range": "~8-10 tok/s at 95k, prompt-dependent", + "note": "free params (K, depth, draft_ctx) tapped; verify round-trip ~270ms is the wall" + } + }, + "floor_now": "verify round-trip ~254ms (WAN + 4 stages eager-decode over 100k). 10+ needs sliding-window KV", + "path_to_reliable_10plus": "sliding-window KV: ~half the layers (window 128) read the full 100k buffer " +} \ No newline at end of file diff --git a/phase0/fastverify.py b/phase0/fastverify.py index 8f0408f..1460625 100644 --- a/phase0/fastverify.py +++ b/phase0/fastverify.py @@ -50,6 +50,7 @@ def __init__(self, parts, maxlen=2048, dev="cuda"): kvh = getattr(cfg, "num_key_value_heads", None) or cfg.num_attention_heads hd = getattr(cfg, "head_dim", None) or (self.hidden // cfg.num_attention_heads) self.cache = StaticKV(self.n_layers, kvh, hd, maxlen, dev) + self.cfg = cfg # flip _attn_implementation per phase self.kp1 = None; self.graph = None; self.out = None # decode buffers built lazily def reset(self): @@ -64,15 +65,29 @@ def _layers(self, x, pos, pe, mf, mw): x = o[0] if isinstance(o, tuple) else o return x - def prefill(self, h, start): # eager, any length + def _flex_masks(self, q, start): + """flex BlockMasks over [q, MAXLEN]. causal already ignores the unwritten cache tail + (keys > start+qi are masked), so attending over the full StaticKV buffer is correct. + KV_LEN is fixed at maxlen -> flex compiles per q only, not per context length.""" + from torch.nn.attention.flex_attention import create_block_mask + win = self.win + def causal(b, h, qi, ki): return (start + qi) >= ki + def swin(b, h, qi, ki): return ((start + qi) >= ki) & ((start + qi) - ki < win) + # _compile=True builds the block mask without materialising the dense [q, maxlen] mask + # (which is ~3GB at maxlen=100k and OOMs) -- this is the whole point of flex. + mf = create_block_mask(causal, 1, None, q, self.maxlen, device=self.dev, _compile=True) + mw = create_block_mask(swin, 1, None, q, self.maxlen, device=self.dev, _compile=True) if win else mf + return mf, mw + + def prefill(self, h, start): # FLEX, O(n) on Ada; chunk-friendly (q = a chunk) n = h.shape[1] if start + n > self.maxlen: # fail clean, never corrupt the cache raise ContextOverflow(f"prefill needs {start + n} positions > max_ctx {self.maxlen} " f"(raise --max-ctx)") + self.cfg._attn_implementation = "flex_attention" # memory-efficient for the big-q prefill self.cache.cp = torch.arange(start, start + n, device=self.dev) pos = self.cache.cp.unsqueeze(0) - mf = _causal_mask(n, self.maxlen, start, 0, torch.bfloat16, self.dev) - mw = _causal_mask(n, self.maxlen, start, self.win, torch.bfloat16, self.dev) if self.win else mf + mf, mw = self._flex_masks(n, start) return self._layers(h, pos, self.rotary(h, pos), mf, mw) def _build(self, kp1): @@ -96,10 +111,15 @@ def _graph_body(self): return self._layers(self.h_buf, self.pos_buf, self.rotary(self.h_buf, self.pos_buf), self.mf_buf, self.mw_buf) - def decode(self, h, start): # fixed q_len, graphed + def decode(self, h, start): # fixed q_len, graphed EAGER if start + h.shape[1] > self.maxlen: # fail clean before any OOB write raise ContextOverflow(f"decode needs {start + h.shape[1]} positions > max_ctx " f"{self.maxlen} (raise --max-ctx)") + # eager attention with q=K+1 is cheap even at 100k kv, and (unlike flex) the additive + # mask + CUDA graph give a fixed-shape replay -> no per-step recompile. flip back to eager + # after a flex prefill (rebuild the graph the first time this phase runs). + if self.cfg._attn_implementation != "eager": + self.cfg._attn_implementation = "eager"; self.graph = None if self.kp1 != h.shape[1]: # (re)build for this K+1, capture once self._build(h.shape[1]); self.graph = None self._set(h, start) diff --git a/phase0/specpipe.py b/phase0/specpipe.py index db33322..4ea56fc 100644 --- a/phase0/specpipe.py +++ b/phase0/specpipe.py @@ -188,12 +188,12 @@ def serve_spec_fast(parts, stage, nstages, listen_port, nxt, timeout, dev, direc if "par" in msg: # TREE verify (fixed-topology graph) h = fv.tree_decode(x, msg["start"], msg["par"], msg["dep"]) else: # LINEAR verify (prefill, then graphed decode) - h = fv.prefill(x, msg["start"]) if first else fv.decode(x, msg["start"]); first = False + h = fv.prefill(x, msg["start"]) if (first or msg.get("prefill")) else fv.decode(x, msg["start"]); first = False if is_tail: h = parts["norm"](h) send_msg(conn, parts["lm_head"](h).argmax(-1)[0].tolist()) else: - fwd = {"op": "verify", "h": h.cpu(), "start": msg["start"]} + fwd = {"op": "verify", "h": h.cpu(), "start": msg["start"], "prefill": msg.get("prefill")} if "par" in msg: fwd["par"] = msg["par"]; fwd["dep"] = msg["dep"] if g: fwd["gather"] = g send_msg(nxt_sock, fwd) @@ -261,7 +261,7 @@ def serve_tail_fast(parts, listen_port, timeout, dev, max_ctx=2048): if "par" in msg: # TREE verify h = fv.tree_decode(x, msg["start"], msg["par"], msg["dep"]) else: - h = fv.prefill(x, msg["start"]) if first else fv.decode(x, msg["start"]); first = False + h = fv.prefill(x, msg["start"]) if (first or msg.get("prefill")) else fv.decode(x, msg["start"]); first = False h = parts["norm"](h) send_msg(ret_conn, parts["lm_head"](h).argmax(-1)[0].tolist()); verifies += 1 except EDGE_ERRORS as e: @@ -453,7 +453,7 @@ def coordinate(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, depth, ret_sock=None, - ignore_eos=False): + ignore_eos=False, prefill_chunk=0, draft_ctx=0): """PIPELINED coordinator: keep `depth` verify chunks in flight over the ring, so throughput approaches the ring's per-chunk THROUGHPUT, not its full latency (the GLM-pipe lever, on the gpt-oss fast-verify path). Same K+1-token chunk as the @@ -472,8 +472,17 @@ def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, dep t_draft = t_recv = 0.0 try: send_msg(pipe_sock, {"op": "reset"}); recv_msg(rx) - send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids, "start": 0}) # prefill (eager) - cur = recv_msg(rx)[-1] + # prefill: one shot, or chunked (long prompts) with the prefill flag so stages run flex + if prefill_chunk and len(prompt_ids) > prefill_chunk: + rr = None + for i in range(0, len(prompt_ids), prefill_chunk): + send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids[i:i + prefill_chunk], + "start": i, "prefill": True}) + rr = recv_msg(rx) + cur = rr[-1] + else: + send_msg(pipe_sock, {"op": "verify", "token_ids": prompt_ids, "start": 0}) # prefill + cur = recv_msg(rx)[-1] pos = len(prompt_ids) out = [cur] inflight = [] # FIFO of (start_pos, drafts) sent but not yet read @@ -487,13 +496,17 @@ def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, dep # computes the next chunk WHILE the current verify chunks cross the WAN. Each fill # collects the ready draft, sends the verify chunk, then issues the next draft request # (which then runs concurrently with the verify read below). draft latency is hidden. - send_msg(draft_sock, {"ids": dprefix, "k": K}) # prime: one outstanding request + # the draft only needs RECENT context to predict the next tokens (next-token is dominated by + # recent tokens); the full swarm still verifies with the complete context. Windowing the draft + # query keeps the draft fast at long context (a full-95k draft is ~800ms/round and kills spec). + dq = lambda: (dprefix[-draft_ctx:] if draft_ctx else dprefix) + send_msg(draft_sock, {"ids": dq(), "k": K}) # prime: one outstanding request while not done: while len(inflight) < depth and not done: # FILL the pipeline td = time.time(); ds = recv_msg(draft_sock); t_draft += time.time() - td # ready (overlapped) send_msg(pipe_sock, {"op": "verify", "token_ids": [dprefix[-1]] + ds, "start": send_pos}) inflight.append((send_pos, ds)); dprefix = dprefix + ds; send_pos += K - send_msg(draft_sock, {"ids": dprefix, "k": K}) # issue next -> runs during the read below + send_msg(draft_sock, {"ids": dq(), "k": K}) # issue next -> runs during the read below tr = time.time(); r = recv_msg(rx); t_recv += time.time() - tr # READ one result sp, ds = inflight.pop(0) if discard > 0: # stale (post-divergence) -> skip @@ -512,7 +525,7 @@ def coordinate_pipe(draft_sock, pipe_sock, tok, prompt, K, max_new, timeout, dep discard = len(inflight) # every chunk still in flight is stale recv_msg(draft_sock) # outstanding draft is stale -> drop it dprefix = prompt_ids + out; send_pos = pos # re-draft from the corrected prefix - send_msg(draft_sock, {"ids": dprefix, "k": K}) # re-prime from the corrected prefix + send_msg(draft_sock, {"ids": dq(), "k": K}) # re-prime from the corrected prefix if len(out) >= max_new or (not ignore_eos and (cur == eos or eos in committed)): done = True recv_msg(draft_sock) # drain the outstanding draft request @@ -666,6 +679,8 @@ def main(): ap.add_argument("--attn", default="eager", help="attention impl for stages: eager | flex_attention " "(flex = O(n) prefill on Ada, needed for long context with gpt-oss sinks)") ap.add_argument("--prompt-file", default="", help="read the prompt from this file (for 100k-token prompts)") + ap.add_argument("--draft-ctx", type=int, default=0, help="window the draft query to the last N tokens " + "(0=full); keeps the draft fast at long context (a full-95k draft is ~800ms/round)") ap.add_argument("--prefill-chunk", type=int, default=0, help="chunk the prefill into this many tokens " "(0=one shot); long prompts need chunking so per-chunk activations stay bounded") ap.add_argument("--max-ctx", type=int, default=2048, help="fast-verify static cache size (prompt+gen ceiling); " @@ -755,7 +770,8 @@ def main(): ks = [int(x) for x in args.sweep.split(",")] if args.sweep else [args.K] for kv in ks: r = coordinate_pipe(draft_sock, pipe_sock, tok, args.prompt, kv, args.max_new, - args.timeout, args.depth, ret_sock=ret_sock) + args.timeout, args.depth, ret_sock=ret_sock, prefill_chunk=args.prefill_chunk, + draft_ctx=args.draft_ctx) print(f"[PIPE K={kv} depth={args.depth}] {r['tok_s']:.2f} tok/s | {r['toks_per_traversal']:.2f} tok/traversal | " f"accept {r['mean_accept']:.2f} | +{r['wasted']} stale | " f"draft {r['draft_ms']:.0f}ms recv {r['recv_ms']:.0f}ms/round", flush=True) From 382a2c99717de4d0ea1b162cf9a12ed204eeef63 Mon Sep 17 00:00:00 2001 From: dddabtc Date: Sat, 20 Jun 2026 21:48:20 +0000 Subject: [PATCH 10/10] Add mid-generation self-healing recovery logic When a stage node dies during a request the swarm currently fails the whole request. This adds the decision logic to recover instead: localize the failed stage, swap in a warm spare holding the same layer block, replay the committed prefix, and resume. Output stays token-identical because greedy decoding resumes from the exact committed tokens, and recovery aborts loudly if the replay does not reproduce the known driver token. shard/heal.py is pure decision logic (no torch, no sockets), so it is unit tested with the standard library (tests/test_heal.py, 13 cases). Engine wiring (phase0/specpipe.py control channel + reconnectable downstream socket) and the libp2p sidecar runtime forward-swap are follow-ups that need a multi-node GPU rig; docs/research/self-healing.md records the design, scope, and those steps. --- docs/research/self-healing.md | 106 ++++++++++++++++++++ shard/heal.py | 127 ++++++++++++++++++++++++ tests/test_heal.py | 182 ++++++++++++++++++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 docs/research/self-healing.md create mode 100644 shard/heal.py create mode 100644 tests/test_heal.py diff --git a/docs/research/self-healing.md b/docs/research/self-healing.md new file mode 100644 index 0000000..98f8581 --- /dev/null +++ b/docs/research/self-healing.md @@ -0,0 +1,106 @@ +# Mid-generation self-healing + +*Design record. Status: increment 1 (the decision logic) landed; engine + sidecar +wiring are the follow-ups. In the spirit of the other docs here — what it does, what +it deliberately leaves out, and how a skeptic checks it.* + +## The problem + +The swarm runs on consumer GPUs on home connections. They drop. Today, if any one +stage node dies in the middle of a request, the whole request dies with it: the +coordinator hits a transport error and gives up (`phase0/specpipe.py` raises +`TransportError`; `phase0/node_kv.py` calls `os._exit`). Fast *detection* is already +done — per-edge timeouts, fail-fast, no silent hang. What is missing is *recovery*. + +A pipeline of N home GPUs where any single hiccup kills every in-flight request is +not usable. So one node vanishing mid-request has to become a recoverable event, not +a fatal one. SERVE is fast; this is what makes that speed actually deliverable. + +## The shape of recovery: stop, swap, replay, continue + +When a stage dies, the coordinator already holds everything it needs to rebuild: the +prompt and the tokens committed so far. Recovery is four steps: + +1. **Stop.** Halt sending, drop the in-flight verify chunks (the coordinator already + discards stale chunks on a divergence — same machinery). +2. **Swap.** Find the dead stage, bring in a warm spare that already holds the same + block of layers, and re-wire the ring around it. +3. **Replay.** Reset every live stage's KV cache and re-feed the committed prefix, so + the rebuilt ring is back in the exact state it was in before the death. +4. **Continue.** Resume decoding from the committed position. + +Because decoding is greedy and we resume from the exact committed tokens, the output +is token-identical to a run that never failed. That is the whole correctness claim, +and it is checked directly (see "Why the output stays identical"). + +## What this PR contains + +Only the decision logic, as a pure module: `shard/heal.py`. No torch, no sockets, no +model — so it runs and is unit-tested anywhere (`tests/test_heal.py`, stdlib only). +The engine and the libp2p sidecar call into it; they are the next PRs (they touch the +GPU serve path and need a real multi-node rig to validate end to end). + +Three pieces: + +- `locate_failure(reports)` — the coordinator only sits on the stage-0 and tail edges, + so a middle-stage death shows up as a *result that never returns*, not a broken + coordinator socket. Each stage reports over a control channel: is it reachable, the + highest verify-chunk it received from upstream, the highest it forwarded downstream. + The failed node is the first unreachable stage, or the first *gap* — a stage whose + predecessor forwarded a chunk it never received. This also catches a frozen (not + dead) node, which a plain liveness ping would miss. +- `plan_replay(prompt_ids, committed_out)` — builds the prefix to replay. The safe + prefix is the prompt plus all committed output **except the current driver token**, + because re-feeding it must greedily *reproduce* that driver token. +- `run_recovery(ops, prompt_ids, committed_out)` — the ordered state machine + (localize → check spare → activate → re-wire → reset → replay → verify → resume) + over an injected `SwarmOps` interface, so the engine implements the real node + operations later and this stays testable with a fake. + +## Why the output stays identical + +`plan_replay` deliberately excludes the last committed token from the replay prefix. +After re-feeding `prompt + committed[:-1]`, the model's next-token argmax must equal +`committed[-1]` — because that token was itself produced as the greedy argmax of that +exact prefix. `run_recovery` checks this (`replay` result vs `expected_next`) and +**aborts the recovery if it does not match**, rather than silently producing a +different continuation. So recovery is either token-identical or it fails loudly; it +never quietly diverges. + +## Scope — what increment 1 does and does not do + +Handles: a single fail-stop or frozen stage during one request, when a warm spare for +that block exists. Middle-stage death is the hard case and is covered first; stage-0 +and tail are the same primitive with a small edge case (the coordinator re-wires). + +Explicitly deferred (named, not hidden): + +- **Spare coverage.** Increment 1 assumes one warm spare for the protected block. A + spare per block (so *any* single death is survivable), or cold-loading from a pool, + is scheduler work — it comes with automatic swarm formation. +- **Long context.** Replaying a very long committed prefix can exceed the request + budget; long-context recovery needs incremental/chunked replay. +- **Multiple simultaneous deaths, coordinator death, and Byzantine nodes** (a node + returning plausible-but-wrong activations) — all out of scope here. + +## Follow-up PRs (need a GPU rig) + +1. **Engine wiring** (`phase0/specpipe.py`): a per-stage control channel beside the + data socket; the missing primitive of re-dialing the downstream socket after + startup (stages today only ever re-accept upstream); and turning the coordinator's + "timeout → fail" into "timeout → `run_recovery`". +2. **Sidecar admin** (`sidecar/main.go`): the forward target is fixed at launch; add a + small local admin API so a forward route can be swapped to the spare at runtime + (no weight reload, no sidecar restart). + +## Acceptance (for the engine PR) + +Run a fixed prompt at fixed `K` and `depth`, record `output_ids`. Run again, kill the +protected middle stage after at least one committed chunk, recover, and assert the +final `output_ids` match the first run **exactly** (compare token ids, not decoded +text — a quantized model's text can match while ids differ at a near-tie). Also assert +no weight reload happened during recovery and the no-failure tok/s is unchanged. + +The hardest part is rebuilding the exact KV state after pipelined stale chunks; that +is why the test compares ids and why `run_recovery` verifies the replay before +resuming. diff --git a/shard/heal.py b/shard/heal.py new file mode 100644 index 0000000..fddfadf --- /dev/null +++ b/shard/heal.py @@ -0,0 +1,127 @@ +"""mid-generation recovery decision logic. + +recovery is stop, swap, replay, continue. the caller localizes the failed stage, +swaps in a warm spare for the same layer block, resets every live stage's kv +state, replays the committed prefix, checks that replay predicts the known driver +token, then resumes decoding. + +this module is pure decision logic: no torch, sockets, or network work lives +here. the phase0 engine and libp2p sidecar call these helpers and perform the +real node operations. +""" + +from dataclasses import dataclass +from typing import Protocol + + +class HealError(Exception): + """recovery cannot proceed safely.""" + + +@dataclass(frozen=True) +class StageReport: + stage_id: int + reachable: bool + last_recv_chunk: int | None + last_fwd_chunk: int | None + + +@dataclass(frozen=True) +class FailureLocation: + stage_id: int + reason: str + + +@dataclass(frozen=True) +class ReplayPlan: + replay_prefix: list[int] + expected_next: int | None + resume_pos: int + + +@dataclass(frozen=True) +class RecoveryOutcome: + failed_stage: int + reason: str + resume_pos: int + cur: int | None + + +class SwarmOps(Protocol): + def probe(self) -> list[StageReport]: ... + def has_spare(self, stage_id: int) -> bool: ... + def activate_spare(self, stage_id: int) -> str: ... + def rewire(self, predecessor_stage_id: int, new_endpoint: str) -> None: ... + def reset_all(self) -> None: ... + def replay(self, prefix: list[int]) -> int | None: ... + def resume(self, resume_pos: int, cur: int | None) -> None: ... + + +def locate_failure(reports: list[StageReport]) -> FailureLocation | None: + """find the first clear failed stage from control and verify progress reports.""" + for report in reports: + if not report.reachable: + return FailureLocation(report.stage_id, "unreachable") + + for upstream, downstream in zip(reports, reports[1:]): + sent = upstream.last_fwd_chunk + received = downstream.last_recv_chunk + if sent is not None and (received is None or received < sent): + return FailureLocation(downstream.stage_id, "gap") + + return None + + +def plan_replay(prompt_ids: list[int], committed_out: list[int]) -> ReplayPlan: + """build the prefix replay that must reproduce the committed driver token.""" + if not prompt_ids: + raise HealError("empty prompt") + + if not committed_out: + return ReplayPlan( + replay_prefix=list(prompt_ids), + expected_next=None, + resume_pos=len(prompt_ids), + ) + + return ReplayPlan( + replay_prefix=list(prompt_ids) + list(committed_out[:-1]), + expected_next=committed_out[-1], + resume_pos=len(prompt_ids) + len(committed_out) - 1, + ) + + +def run_recovery( + ops: SwarmOps, + prompt_ids: list[int], + committed_out: list[int], +) -> RecoveryOutcome: + """run the ordered recovery decisions against injected swarm operations.""" + reports = ops.probe() + loc = locate_failure(reports) + if loc is None: + raise HealError("no failure localized") + + if not ops.has_spare(loc.stage_id): + raise HealError(f"no warm spare for stage {loc.stage_id}") + + new_ep = ops.activate_spare(loc.stage_id) + predecessor = loc.stage_id - 1 + ops.rewire(predecessor, new_ep) + ops.reset_all() + + plan = plan_replay(prompt_ids, committed_out) + got = ops.replay(plan.replay_prefix) + if plan.expected_next is not None and got != plan.expected_next: + raise HealError( + f"replay mismatch: got {got!r} expected {plan.expected_next!r}" + ) + + cur = plan.expected_next if plan.expected_next is not None else got + ops.resume(plan.resume_pos, cur) + return RecoveryOutcome( + failed_stage=loc.stage_id, + reason=loc.reason, + resume_pos=plan.resume_pos, + cur=cur, + ) diff --git a/tests/test_heal.py b/tests/test_heal.py new file mode 100644 index 0000000..a1d3bb4 --- /dev/null +++ b/tests/test_heal.py @@ -0,0 +1,182 @@ +import unittest + +from shard.heal import ( + FailureLocation, + HealError, + RecoveryOutcome, + StageReport, + locate_failure, + plan_replay, + run_recovery, +) + + +class RecordingOps: + def __init__( + self, + reports, + *, + spare=True, + replay_result=None, + endpoint="spare-stage", + ): + self.reports = reports + self.spare = spare + self.replay_result = replay_result + self.endpoint = endpoint + self.calls = [] + + def probe(self): + self.calls.append(("probe",)) + return self.reports + + def has_spare(self, stage_id): + self.calls.append(("has_spare", stage_id)) + return self.spare + + def activate_spare(self, stage_id): + self.calls.append(("activate_spare", stage_id)) + return self.endpoint + + def rewire(self, predecessor_stage_id, new_endpoint): + self.calls.append(("rewire", predecessor_stage_id, new_endpoint)) + + def reset_all(self): + self.calls.append(("reset_all",)) + + def replay(self, prefix): + self.calls.append(("replay", list(prefix))) + return self.replay_result + + def resume(self, resume_pos, cur): + self.calls.append(("resume", resume_pos, cur)) + + +class LocateFailureTests(unittest.TestCase): + def test_first_unreachable_wins(self): + reports = [ + StageReport(2, True, 0, 0), + StageReport(7, False, 0, 0), + StageReport(8, False, None, None), + ] + + self.assertEqual(locate_failure(reports), FailureLocation(7, "unreachable")) + + def test_gap_detection_when_downstream_lags(self): + reports = [ + StageReport(0, True, 1, 4), + StageReport(1, True, 2, 2), + ] + + self.assertEqual(locate_failure(reports), FailureLocation(1, "gap")) + + def test_gap_detection_when_downstream_received_none(self): + reports = [ + StageReport(3, True, None, 5), + StageReport(4, True, None, None), + ] + + self.assertEqual(locate_failure(reports), FailureLocation(4, "gap")) + + def test_all_consistent_returns_none(self): + reports = [ + StageReport(0, True, None, 0), + StageReport(1, True, 0, 3), + StageReport(2, True, 3, None), + ] + + self.assertIsNone(locate_failure(reports)) + + def test_head_unreachable(self): + reports = [ + StageReport(0, False, None, None), + StageReport(1, True, None, None), + ] + + self.assertEqual(locate_failure(reports), FailureLocation(0, "unreachable")) + + +class PlanReplayTests(unittest.TestCase): + def test_normal_multi_token_output(self): + plan = plan_replay([10, 11], [20, 21, 22]) + + self.assertEqual(plan.replay_prefix, [10, 11, 20, 21]) + self.assertEqual(plan.expected_next, 22) + self.assertEqual(plan.resume_pos, 4) + + def test_single_token_output(self): + plan = plan_replay([10, 11], [20]) + + self.assertEqual(plan.replay_prefix, [10, 11]) + self.assertEqual(plan.expected_next, 20) + self.assertEqual(plan.resume_pos, 2) + + def test_empty_output(self): + plan = plan_replay([10, 11], []) + + self.assertEqual(plan.replay_prefix, [10, 11]) + self.assertIsNone(plan.expected_next) + self.assertEqual(plan.resume_pos, 2) + + def test_empty_prompt_raises(self): + with self.assertRaisesRegex(HealError, "empty prompt"): + plan_replay([], [20]) + + +class RunRecoveryTests(unittest.TestCase): + def test_happy_path_gap_at_middle_stage(self): + reports = [ + StageReport(0, True, None, 2), + StageReport(1, True, 1, 1), + StageReport(2, True, 1, None), + ] + ops = RecordingOps(reports, replay_result=31, endpoint="new-stage-1") + + out = run_recovery(ops, [10, 11], [30, 31]) + + self.assertEqual(out, RecoveryOutcome(1, "gap", 3, 31)) + self.assertEqual( + ops.calls, + [ + ("probe",), + ("has_spare", 1), + ("activate_spare", 1), + ("rewire", 0, "new-stage-1"), + ("reset_all",), + ("replay", [10, 11, 30]), + ("resume", 3, 31), + ], + ) + + def test_no_spare_raises_without_resume(self): + reports = [StageReport(0, False, None, None)] + ops = RecordingOps(reports, spare=False) + + with self.assertRaisesRegex(HealError, "no warm spare for stage 0"): + run_recovery(ops, [10], []) + + self.assertNotIn("resume", [call[0] for call in ops.calls]) + + def test_replay_mismatch_raises_without_resume(self): + reports = [ + StageReport(0, True, None, 4), + StageReport(1, True, 3, None), + ] + ops = RecordingOps(reports, replay_result=99) + + with self.assertRaisesRegex(HealError, "replay mismatch: got 99 expected 20"): + run_recovery(ops, [10], [20]) + + self.assertNotIn("resume", [call[0] for call in ops.calls]) + + def test_head_death_rewires_from_coordinator(self): + reports = [StageReport(0, False, None, None)] + ops = RecordingOps(reports, replay_result=None, endpoint="new-head") + + run_recovery(ops, [10], []) + + self.assertIn(("rewire", -1, "new-head"), ops.calls) + + +if __name__ == "__main__": + unittest.main()