igneum/sim/difficulty/records/pull_live.py
igneum-labs e17d3816ba Difficulty: the live oscillation of 4 October, its cause, the DAG replay and rule v2 (reference window 600 behind a height switch)
Live devnet v4: a second RTX 5090 joining 7 minutes into an epoch left the whole-epoch reference lane polluted for the hour; the short lane read 11 to 25% above it and the 25% trigger flipped between the two for 40 minutes (102M to 164M, 54 to 81 blocks a minute). Record and hash-rate truth under sim/difficulty/records/. sim.py gains a DAG model (miners on nodes with igneum-miner's template staleness, GHOSTDAG, the rule as the node runs it) and --live replay: std of log difficulty 0.115 against the record's 0.134, 4.3 peaks of 1.31x against 4 of 1.37x. The brief's candidates (short lane 240/360, ease clamp 3%, clamp once per DAA second, hysteresis, median of three) leave 0.09 to 0.13; capping the reference lane at the newest 600 blocks of the epoch gives 0.026 with no flips. Rule v2 = that cap, epoch lane only, behind difficulty_v2_activation_daa (devnet-v4 fork). Attack suite and synthetic set before and after, 3-node test network of the switch (testnet_v2.py), analysis document, spec 2.3, bench-log entry, ledger M24, fast-time file carries the new field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 13:26:23 +00:00

74 lines
3.1 KiB
Python

#!/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()