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>
74 lines
3.1 KiB
Python
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()
|