upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/scripts/build-pool-index.py
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /scripts/build-pool-index.py
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parent66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff)
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git merge-base returns no result at all -- so anyone cloning this repository landed on a default branch that was neither current nor related to the running node. The 28 commits on main were audited before this merge: every one is already present in the tree by content, including the two that are hardest to see because they were rewritten afterwards -- the payout reorg-safety work (PayoutBlockStillOnChain, since evolved to re-check on every retry rather than once at maturity) and the nostr relay certificate verification (SSL_set1_host, now with a Windows root-store path). Nothing on main is lost by this merge. Recorded as a merge rather than a force-push: main only ever moves forward, its old history stays reachable as the first parent, and this is revertible. The tree is taken wholesale from the merged branch, so main's content is now exactly what was built and tested. What this brings onto main: - compactblock: BIP152-style compact block relay ported from upstream 1.2.23, primitives and unit test only, with the RAND_bytes failure path closed and the reconstruction allocation bounded. - upstream/main 17741d9: isolated testnet parameters and genesis, optional 2 MB large pages for RandomX. Mainnet genesis verified byte-identical through upstream's BuildGenesisBlock() refactor. - deploy: every image built on bookworm so binaries run on Debian 12, and the container build now honours the RandomX and libsecp256k1 pins it had been silently ignoring since before 1.2.15. Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus constants unchanged against e4f898c apart from the pre-existing MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'scripts/build-pool-index.py')
-rw-r--r--scripts/build-pool-index.py174
1 files changed, 174 insertions, 0 deletions
diff --git a/scripts/build-pool-index.py b/scripts/build-pool-index.py
new file mode 100644
index 0000000..d3eb386
--- /dev/null
+++ b/scripts/build-pool-index.py
@@ -0,0 +1,174 @@
1#!/usr/bin/env python3
2#
3# Build a static public pool directory from one or more Bitflash pool_status.json
4# files. Inputs may be local paths or http(s) URLs.
5
6import argparse
7import html
8import json
9import os
10import time
11import urllib.parse
12import urllib.request
13
14
15def load_json(source):
16 if source.startswith("http://") or source.startswith("https://"):
17 with urllib.request.urlopen(source, timeout=10) as r:
18 return json.loads(r.read().decode("utf-8"))
19 with open(source, "r", encoding="utf-8-sig") as f:
20 return json.load(f)
21
22
23def normalize_pool(source, obj, now):
24 operator = obj.get("operator", {})
25 pool = obj.get("pool", {})
26 node = obj.get("node", {})
27 rounds_file = operator.get("roundsFile") or "pool_rounds.json"
28 rounds_url = ""
29 if source.startswith("http://") or source.startswith("https://"):
30 rounds_url = urllib.parse.urljoin(source, rounds_file)
31 updated = int(obj.get("updatedAt") or 0)
32 age = max(0, now - updated) if updated else None
33 online = bool(node.get("poolRunning")) and age is not None and age <= 120
34 return {
35 "source": source,
36 "name": operator.get("name") or "Bitflash Pool",
37 "btfAddress": operator.get("btfAddress") or "",
38 "feePercent": float(operator.get("feePercent") or 0.0),
39 "dashboardUrl": operator.get("dashboardUrl") or "",
40 "height": int(node.get("height") or 0),
41 "online": online,
42 "ageSeconds": age,
43 "authorizedMiners": int(pool.get("authorizedMiners") or 0),
44 "blocksFoundSession": int(pool.get("blocksFoundSession") or 0),
45 "roundShares": int(pool.get("roundShares") or 0),
46 "hashRate": float(pool.get("hashRate") or 0.0),
47 "roundsUrl": rounds_url,
48 "recentRounds": obj.get("recentRounds", []),
49 }
50
51
52def rank_key(pool):
53 return (
54 1 if pool["online"] else 0,
55 pool["hashRate"],
56 pool["authorizedMiners"],
57 -pool["feePercent"],
58 pool["name"].lower(),
59 )
60
61
62def render_html(pools, generated_at):
63 rows = []
64 for p in pools:
65 dash = p["dashboardUrl"]
66 rounds = p.get("roundsUrl", "")
67 # Only linkify http(s). The dashboard URL comes from an operator's
68 # self-published status file, so a "javascript:" or "data:" scheme
69 # would become a clickable script-injection on this public page.
70 dash_lower = dash.lower()
71 if dash_lower.startswith("http://") or dash_lower.startswith("https://"):
72 dash_html = '<a href="{0}" rel="noopener noreferrer">dashboard</a>'.format(
73 html.escape(dash, quote=True))
74 else:
75 dash_html = "-"
76 rounds_lower = rounds.lower()
77 if rounds_lower.startswith("http://") or rounds_lower.startswith("https://"):
78 rounds_html = '<a href="{0}" rel="noopener noreferrer">proof</a>'.format(
79 html.escape(rounds, quote=True))
80 else:
81 rounds_html = "-"
82 rows.append(
83 "<tr>"
84 "<td>{status}</td>"
85 "<td><strong>{name}</strong><br><code>{addr}</code></td>"
86 "<td>{fee:.2f}%</td>"
87 "<td>{miners}</td>"
88 "<td>{hashrate:.2f}</td>"
89 "<td>{blocks}</td>"
90 "<td>{height}</td>"
91 "<td>{age}</td>"
92 "<td>{dash}</td>"
93 "<td>{rounds}</td>"
94 "</tr>".format(
95 status="online" if p["online"] else "stale",
96 name=html.escape(p["name"]),
97 addr=html.escape(p["btfAddress"]),
98 fee=p["feePercent"],
99 miners=p["authorizedMiners"],
100 hashrate=p["hashRate"],
101 blocks=p["blocksFoundSession"],
102 height=p["height"],
103 age="{}s".format(p["ageSeconds"]) if p["ageSeconds"] is not None else "-",
104 dash=dash_html,
105 rounds=rounds_html,
106 )
107 )
108
109 return """<!doctype html>
110<html lang="en">
111<head>
112 <meta charset="utf-8">
113 <meta name="viewport" content="width=device-width, initial-scale=1">
114 <title>Bitflash Pools</title>
115 <style>
116 body {{ font: 15px/1.45 system-ui, sans-serif; margin: 32px; color: #111; }}
117 table {{ border-collapse: collapse; width: 100%; }}
118 th, td {{ border-bottom: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top; }}
119 th {{ font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }}
120 code {{ font-size: 12px; word-break: break-all; }}
121 .note {{ color: #555; max-width: 780px; }}
122 </style>
123</head>
124<body>
125 <h1>Bitflash Pools</h1>
126 <p class="note">This directory is informational. Bitflash nodes discover pools through Nostr and .btf; this page only helps humans compare public operators.</p>
127 <table>
128 <thead>
129 <tr><th>Status</th><th>Pool</th><th>Fee</th><th>Miners</th><th>H/s</th><th>Blocks</th><th>Height</th><th>Age</th><th>Link</th><th>Proof</th></tr>
130 </thead>
131 <tbody>
132 {rows}
133 </tbody>
134 </table>
135 <p class="note">Generated at {generated_at}. JSON: <a href="pools.json">pools.json</a>.</p>
136</body>
137</html>
138""".format(rows="\n ".join(rows), generated_at=generated_at)
139
140
141def main():
142 ap = argparse.ArgumentParser()
143 ap.add_argument("--out-dir", default="pool-site")
144 ap.add_argument("sources", nargs="+", help="pool_status.json paths or URLs")
145 args = ap.parse_args()
146
147 now = int(time.time())
148 pools = []
149 errors = []
150 for source in args.sources:
151 try:
152 pools.append(normalize_pool(source, load_json(source), now))
153 except Exception as e:
154 errors.append({"source": source, "error": str(e)})
155
156 pools.sort(key=rank_key, reverse=True)
157 os.makedirs(args.out_dir, exist_ok=True)
158
159 generated_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now))
160 with open(os.path.join(args.out_dir, "pools.json"), "w", encoding="utf-8") as f:
161 json.dump({
162 "schema": "bitflash-pool-directory-1",
163 "generatedAt": generated_at,
164 "pools": pools,
165 "errors": errors,
166 }, f, indent=2, sort_keys=True)
167 f.write("\n")
168
169 with open(os.path.join(args.out_dir, "index.html"), "w", encoding="utf-8") as f:
170 f.write(render_html(pools, generated_at))
171
172
173if __name__ == "__main__":
174 main()