#!/usr/bin/env python3 """Export every header of the live devnet from the observer node's read-only wRPC JSON endpoint. python3 pull_live.py [ws://127.0.0.1:28640] [out.csv] Columns: hash, daa_score, blue_score, timestamp_ms, bits, difficulty (the node's 2^255 / target convention), target_log2, epoch (3,600 DAA), is_chain_block, parents, selected_parent, blue_work (hex), mergeset_blues, mergeset_reds, vote_key_hash. Read only: getBlockDagInfo and getBlocks from the pruning point, nothing else. """ import asyncio import csv import json import math import sys async def rpc(url, method, params=None): import websockets async with websockets.connect(url, max_size=None) as ws: await ws.send(json.dumps({"id": 1, "method": method, "params": params or {}})) m = json.loads(await ws.recv()) if m.get("error"): raise RuntimeError(m["error"]) return m["params"] def bits_to_target(bits): exp = bits >> 24 mant = bits & 0x7FFFFF return mant >> (8 * (3 - exp)) if exp <= 3 else mant << (8 * (exp - 3)) def main(): url = sys.argv[1] if len(sys.argv) > 1 else "ws://127.0.0.1:28640" out = sys.argv[2] if len(sys.argv) > 2 else "live-2026-10-04.csv" info = asyncio.run(rpc(url, "getBlockDagInfo")) low = info["pruningPointHash"] seen = set() rows = [] while True: r = asyncio.run(rpc(url, "getBlocks", {"lowHash": low, "includeBlocks": True, "includeTransactions": False})) blocks = r.get("blocks") or [] new = 0 for b in blocks: h = b["header"] v = b.get("verboseData") or {} hs = h.get("hash") or v.get("hash") if hs in seen: continue seen.add(hs) new += 1 bits = int(h["bits"]) target = bits_to_target(bits) rows.append({ "hash": hs, "daa_score": h["daaScore"], "blue_score": h["blueScore"], "timestamp_ms": h["timestamp"], "bits": "0x%08x" % bits, "difficulty": v.get("difficulty"), "target_log2": "%.3f" % math.log2(target) if target else "", "epoch": int(h["daaScore"]) // 3600, "is_chain_block": 1 if v.get("isChainBlock") else 0, "parents": len((h.get("parentsByLevel") or [[]])[0]), "selected_parent": v.get("selectedParentHash"), "blue_work": h.get("blueWork"), "mergeset_blues": len(v.get("mergeSetBluesHashes") or []), "mergeset_reds": len(v.get("mergeSetRedsHashes") or []), "vote_key_hash": h.get("voteKeyHash"), }) if not blocks or new == 0 or len(blocks) < 2: break low = blocks[-1]["header"].get("hash") or blocks[-1]["verboseData"]["hash"] rows.sort(key=lambda r: (int(r["blue_score"]), int(r["daa_score"]), int(r["timestamp_ms"]))) with open(out, "w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys())) w.writeheader() w.writerows(rows) print("wrote %d headers to %s (sink daa %s, difficulty %s)" % (len(rows), out, info.get("virtualDaaScore"), info.get("difficulty"))) if __name__ == "__main__": main()