upleb.uk

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

summaryrefslogtreecommitdiff
path: root/scripts/bitflash-mining-calculator.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/bitflash-mining-calculator.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/bitflash-mining-calculator.py')
-rw-r--r--scripts/bitflash-mining-calculator.py206
1 files changed, 206 insertions, 0 deletions
diff --git a/scripts/bitflash-mining-calculator.py b/scripts/bitflash-mining-calculator.py
new file mode 100644
index 0000000..1fcc1a9
--- /dev/null
+++ b/scripts/bitflash-mining-calculator.py
@@ -0,0 +1,206 @@
1#!/usr/bin/env python3
2"""Estimate Bitflash mining rewards and electricity cost.
3
4This is a local WhatToMine-style calculator for Bitflash. It does not fetch
5prices, does not talk to the node, and does not affect consensus.
6"""
7
8import argparse
9import json
10import math
11import sys
12
13
14HASHES_AT_DIFF_1 = 4096.0
15DEFAULT_BLOCK_TIME = 120.0
16DEFAULT_BLOCK_REWARD = 50.0
17
18
19UNITS = {
20 "h": 1.0,
21 "h/s": 1.0,
22 "hs": 1.0,
23 "kh": 1_000.0,
24 "kh/s": 1_000.0,
25 "khs": 1_000.0,
26 "mh": 1_000_000.0,
27 "mh/s": 1_000_000.0,
28 "mhs": 1_000_000.0,
29 "gh": 1_000_000_000.0,
30 "gh/s": 1_000_000_000.0,
31 "ghs": 1_000_000_000.0,
32}
33
34
35def parse_hashrate(value, unit):
36 try:
37 n = float(value)
38 except ValueError:
39 raise SystemExit("invalid hashrate: %s" % value)
40 mul = UNITS.get(unit.lower())
41 if mul is None:
42 raise SystemExit("unknown hashrate unit: %s" % unit)
43 if n < 0:
44 raise SystemExit("hashrate must not be negative")
45 return n * mul
46
47
48def fmt_hashrate(hps):
49 units = [("GH/s", 1_000_000_000.0), ("MH/s", 1_000_000.0), ("kH/s", 1_000.0), ("H/s", 1.0)]
50 for name, div in units:
51 if hps >= div:
52 return "%.4g %s" % (hps / div, name)
53 return "%.4g H/s" % hps
54
55
56def fmt_money(x):
57 return "%.8f" % x
58
59
60def estimate(args):
61 miner_hps = parse_hashrate(args.hashrate, args.hashrate_unit)
62 if args.network_hashrate is not None:
63 network_hps = parse_hashrate(args.network_hashrate, args.network_hashrate_unit)
64 difficulty = network_hps * args.block_time / HASHES_AT_DIFF_1
65 network_source = "network_hashrate"
66 else:
67 if args.difficulty is None:
68 raise SystemExit("provide either --network-hashrate or --difficulty")
69 difficulty = float(args.difficulty)
70 if difficulty <= 0:
71 raise SystemExit("difficulty must be positive")
72 network_hps = difficulty * HASHES_AT_DIFF_1 / args.block_time
73 network_source = "difficulty"
74
75 if network_hps <= 0:
76 raise SystemExit("network hashrate must be positive")
77
78 block_reward = float(args.block_reward)
79 block_time = float(args.block_time)
80 pool_fee_percent = float(args.pool_fee)
81 price = float(args.price)
82 watts = float(args.watts)
83 kwh_cost = float(args.kwh_cost)
84 if block_reward < 0 or block_time <= 0 or pool_fee_percent < 0 or price < 0 or watts < 0 or kwh_cost < 0:
85 raise SystemExit("numeric inputs must be non-negative; block time must be positive")
86
87 share = miner_hps / network_hps
88 blocks_per_day = 86400.0 / block_time
89 gross_btf_day = blocks_per_day * block_reward * share
90 net_btf_day = gross_btf_day * max(0.0, 1.0 - pool_fee_percent / 100.0)
91 revenue_day = net_btf_day * price
92 cost_day = watts / 1000.0 * 24.0 * kwh_cost
93 profit_day = revenue_day - cost_day
94 expected_seconds_per_block = math.inf if miner_hps <= 0 else difficulty * HASHES_AT_DIFF_1 / miner_hps
95
96 periods = {
97 "hour": 1.0 / 24.0,
98 "day": 1.0,
99 "week": 7.0,
100 "month_30d": 30.0,
101 }
102 rows = {}
103 for name, days in periods.items():
104 rows[name] = {
105 "btf": net_btf_day * days,
106 "revenue": revenue_day * days,
107 "electricity_cost": cost_day * days,
108 "profit": profit_day * days,
109 }
110
111 return {
112 "schema": "bitflash-mining-calculator-1",
113 "network_source": network_source,
114 "inputs": {
115 "miner_hashrate_hps": miner_hps,
116 "miner_hashrate": fmt_hashrate(miner_hps),
117 "network_hashrate_hps": network_hps,
118 "network_hashrate": fmt_hashrate(network_hps),
119 "difficulty": difficulty,
120 "block_time_seconds": block_time,
121 "block_reward_btf": block_reward,
122 "pool_fee_percent": pool_fee_percent,
123 "price_per_btf": price,
124 "power_watts": watts,
125 "electricity_per_kwh": kwh_cost,
126 },
127 "estimates": {
128 "miner_network_share": share,
129 "blocks_per_day": blocks_per_day,
130 "expected_time_to_solo_block_seconds": expected_seconds_per_block,
131 "expected_time_to_solo_block_hours": expected_seconds_per_block / 3600.0,
132 "gross_btf_per_day": gross_btf_day,
133 "net_btf_per_day": net_btf_day,
134 "periods": rows,
135 },
136 }
137
138
139def print_text(result):
140 i = result["inputs"]
141 e = result["estimates"]
142 print("Bitflash mining calculator")
143 print("schema: %s" % result["schema"])
144 print()
145 print("Network")
146 print(" source: %s" % result["network_source"])
147 print(" hashrate: %s" % i["network_hashrate"])
148 print(" difficulty: %.8f" % i["difficulty"])
149 print(" block time: %.0fs" % i["block_time_seconds"])
150 print(" reward: %s BTF" % fmt_money(i["block_reward_btf"]))
151 print()
152 print("Miner")
153 print(" hashrate: %s" % i["miner_hashrate"])
154 print(" share: %.8f%%" % (e["miner_network_share"] * 100.0))
155 if math.isinf(e["expected_time_to_solo_block_seconds"]):
156 print(" solo block: never")
157 else:
158 print(" solo block: %.2f hours expected average" % e["expected_time_to_solo_block_hours"])
159 print()
160 print("Economics")
161 print(" price: $%.8f / BTF" % i["price_per_btf"])
162 print(" power: %.2f W" % i["power_watts"])
163 print(" kWh cost: $%.4f" % i["electricity_per_kwh"])
164 print(" pool fee: %.2f%%" % i["pool_fee_percent"])
165 print()
166 print("Estimated rewards")
167 print(" %-9s %16s %12s %12s %12s" % ("period", "BTF", "revenue", "power", "profit"))
168 for name in ["hour", "day", "week", "month_30d"]:
169 row = e["periods"][name]
170 print(" %-9s %16s $%11.4f $%11.4f $%11.4f" % (
171 name,
172 fmt_money(row["btf"]),
173 row["revenue"],
174 row["electricity_cost"],
175 row["profit"],
176 ))
177 print()
178 print("Note: this is an expectation, not a promise. Solo mining variance can be huge.")
179
180
181def main(argv):
182 ap = argparse.ArgumentParser(description=__doc__)
183 ap.add_argument("--hashrate", required=True, help="miner hashrate number")
184 ap.add_argument("--hashrate-unit", default="kh/s", choices=sorted(UNITS), help="miner hashrate unit")
185 ap.add_argument("--network-hashrate", help="network hashrate number")
186 ap.add_argument("--network-hashrate-unit", default="kh/s", choices=sorted(UNITS), help="network hashrate unit")
187 ap.add_argument("--difficulty", type=float, help="Bitflash difficulty; diff 1 expects 4096 hashes")
188 ap.add_argument("--block-time", type=float, default=DEFAULT_BLOCK_TIME, help="target block time in seconds")
189 ap.add_argument("--block-reward", type=float, default=DEFAULT_BLOCK_REWARD, help="block reward in BTF")
190 ap.add_argument("--pool-fee", type=float, default=0.0, help="pool fee percent")
191 ap.add_argument("--price", type=float, default=0.0, help="price per BTF in USD or your chosen fiat unit")
192 ap.add_argument("--watts", type=float, default=0.0, help="miner or whole-system power in watts")
193 ap.add_argument("--kwh-cost", type=float, default=0.0, help="electricity cost per kWh")
194 ap.add_argument("--json", action="store_true", help="print JSON")
195 args = ap.parse_args(argv)
196
197 result = estimate(args)
198 if args.json:
199 print(json.dumps(result, sort_keys=True, indent=2))
200 else:
201 print_text(result)
202 return 0
203
204
205if __name__ == "__main__":
206 raise SystemExit(main(sys.argv[1:]))