Stratum V2 for Dinero, in Rust. Protocol messages, wire codec, Noise NX transport, reference pool server, Template Provider binary, and miner-side Job Declaration — the sovereignty loop that lets a miner (including a phone) pick its own coinbase outputs and let the pool independently verify every byte, including the header's Utreexo root.
Pure Rust, no FFI. Works anywhere a Rust crate works: DineroDPI (iOS),
future pool operators, TP processes running beside dinerod, tests.
Install with one pasted command, then launch directly with your Dinero reward address. No node, configuration file, or chain sync is required.
macOS / Linux / Chromebook:
curl -fsSL https://raw.githubusercontent.com/DineroLabs/dinero-sv2/main/scripts/install.sh | shWindows (PowerShell):
irm https://raw.githubusercontent.com/DineroLabs/dinero-sv2/main/scripts/install.ps1 | iexThen start shared CPU mining on macOS or Linux (replace the placeholder with
your own complete din1p… reward address):
cd "$HOME/.local/bin" && ./dinero-miner --address "YOUR_DIN1_ADDRESS" --reward-mode shared --threads 2On Windows PowerShell:
cd "$env:LOCALAPPDATA\DineroMiner\bin"; .\dinero-miner.exe --address "YOUR_DIN1_ADDRESS" --reward-mode shared --threads 2--threads 2 uses two CPU threads; choose a different positive number for
more or less CPU usage. The miner validates and saves the address, connects to
pool.dinerolabs.org:4444, and opens a dedicated full-screen terminal with:
- a pinned Dinero header and complete reward address;
- the Noise NX security, channel, worker, target, and uptime state;
- 31 rolling rows of real nonces and complete 256-bit candidate hashes;
- accepted/rejected share telemetry, session work, and reconnect health; and
- a timestamped network feed.
Ctrl-Crestores the original shell screen.
Running dinero-miner without --address remains supported and prompts for a
reward address interactively.
No node in either reward mode: the pool's node serves templates.
shared = PPLNS split; solo = miner-owned coinbase via the pool
(whole reward if your hash wins).
Your address is validated up front and saved to
~/.config/dinero-miner/config.json; the next run offers it back with
Enter-to-reuse. Flags > config file > built-in defaults, so --pool,
--server-pubkey, --reward-mode, and --threads all override.
Chromebook: enable Linux (Settings → Advanced → Developers → "Linux development environment"), open the Terminal app, and paste the same Linux one-liner.
Behavior change in
miner-v0.1.0: an unspecified--reward-modenow resolves toshared(it used to default tosolo). Pass--reward-mode soloexplicitly to keep miner-owned all-or-nothing coinbases.
Current public releases ship the CPU miner (dinero-miner). The GPU worker
(dinero-sv2-gpu-miner, Metal/OpenCL) has the same interactive UX and
builds from source today; prebuilt GPU release binaries come later.
crates/
├── dinero-sv2-common messages + header layout + Utreexo invariants
├── dinero-sv2-codec strict wire codec (fixed + var-len)
├── dinero-sv2-jd Job Declaration primitives + pure-Rust Utreexo
├── dinero-sv2-transport Noise NX + SV2-shaped framing
├── dinero-sv2-tp-sim in-memory Template Provider simulator
├── dinero-tp TP binary — binds dinerod, serves miners
├── dinero-sv2-pool reference pool server (two-tier target,
│ vardiff, submitblock, JD acceptance)
├── dinero-sv2-miner CPU SV2 pool worker
└── dinero-sv2-gpu-miner GPU SV2 pool worker (Metal/OpenCL)
| Binary | Role | Who runs it |
|---|---|---|
dinero-sv2-pool |
Reference Stratum V2 pool server. Accepts workers, assigns share targets, validates shares, and submits found blocks. | Pool operators |
dinero-tp |
Template Provider. Talks to dinerod RPC and emits block templates to the SV2 side. |
Pool operators |
dinero-sv2-miner |
CPU pool worker. Connects to an SV2 pool over Noise/SV2 and submits shares. | Pool miners |
dinero-sv2-gpu-miner |
GPU pool worker. Uses Metal on Apple Silicon and OpenCL on Linux/Windows. | GPU pool miners |
In casual language, "SV2 pool worker" means the miner-side process:
dinero-sv2-miner for CPU or dinero-sv2-gpu-miner for GPU. The pool
it connects to is dinero-sv2-pool.
Phase 5 is the finish line. With a dinerod running at 127.0.0.1:20998
and the daemon's cookie in ~/.dinero/.cookie:
# Terminal 1 — pool
ADDR=din1p6zd8mnxf3fz0kcsjrmsrtjkymnmfjz9gkrpqhed07g3n4hdfn4pq6nxmch
cargo run --release -p dinero-sv2-pool -- \
--bind 127.0.0.1:4444 \
--payout-address "$ADDR" \
--share-leading-bits 0
# Terminal 2 — print the pool's static Noise pubkey for pinning
cargo run --release -p dinero-sv2-pool -- \
--payout-address "$ADDR" --print-pubkey
# → 47e4c132fefc04bce63f87b2d5d0a70575541da47d4dbe23247ea135eafcfb58
# Terminal 3 — a miner that picks its own payout and runs the full JD loop
cargo run --release -p dinero-tp --example testclient -- \
--server-pubkey 47e4c132fefc04bce63f87b2d5d0a70575541da47d4dbe23247ea135eafcfb58 \
--jdThe miner prints:
handshake ok; server static pubkey = 47e4c132…
SetupConnection.Success: used_version=2
OpenStandardMiningChannel.Success: channel_id=1 target=FF..FF
SetNewPrevHash: prev_hash=b07ae72d… nbits=0x1d00ffd2
UtreexoStateAnnouncement: num_leaves=15993 num_roots=10
pre_block_commitment=2af38633…
CoinbaseContext: height=5332 value_una=10000000000 …
NewMiningJob: template_id=1 utreexo_root=5cec2b43…
JD locally computed: coinbase_txid=c78077d7…
utreexo_root=ceded42b7… (pool's was 5cec2b43…)
SubmitShares.Success: accepted=1
The pool prints:
accepted extended share
hash=46bc4e92…
template_id=1 utreexo_root=ceded42b7…
That match — ceded42b7… — is the whole point. The miner chose its
payout, built its own coinbase, derived the header's utreexo_root
locally. The pool reassembled the candidate from the miner's submitted
outputs, applied the same Utreexo math, and got the same 32 bytes.
Neither side's result relied on the other.
The pool can serve a second, unauthenticated, read-only HTTP listener
for aggregator sites (MiningPoolStats, minerstat) and for a human landing
on the pool's hostname. It is OFF unless you pass --public-stats-bind:
dinero-sv2-pool ... --public-stats-bind 127.0.0.1:8080 --public-stratum-addr pool.example.org:4444--public-stratum-addr is required alongside it (the pool refuses to start
without it — the internal --bind is not something to advertise). Bind on
loopback and put a TLS reverse proxy in front; a port that is already taken
fails startup rather than silently running without the page.
| Route | Returns |
|---|---|
GET / |
A small HTML page: hashrate, miners, last block, recent blocks, install one-liners. Fetches /api/stats from the browser. |
GET /api/stats |
{ pool_hashrate_hs, miners, workers, blocks_found_24h, blocks_found_total, last_block_height, last_block_hash, last_block_time, fee_bps, payout_scheme: "PPLNS", min_payout_una, stratum, network_height, network_difficulty, updated_at } |
GET /api/blocks?limit=N |
Newest first, N clamped to 1..100 (default 50): [{ height, hash, time, reward_una, status }] |
GET /api/miner/<din1p…> |
{ address, hashrate_hs, shares_1h, window_bps, est_next_block_una, paid_una, last_share_time } — 404 for any address that never submitted a share. There is no miner listing. |
Field notes: pool_hashrate_hs, hashrate_hs and miners describe
shared-mode (PPLNS) work only — expected hashes per second from shares
in the PPLNS window over the last 10 minutes (share weight is calibrated to
expected hashes), and distinct payout addresses with such a share. Solo-mode
miners are not credited to the window and do not appear in those numbers.
workers is Stratum sessions that completed the Noise handshake, shared
and solo alike, so it can exceed what miners accounts for.
est_next_block_una is an estimate of what the address would receive if
the next block were found right now (reward less fee, times window_bps);
PPLNS holds no balance, so it is a standing, not a debt. paid_una sums
coinbase outputs to that address in blocks the pool built and dinerod
accepted. network_difficulty uses the Bitcoin 0x1d00ffff convention.
Times are Unix seconds; amounts are una.
Safe to expose: every route is GET/HEAD only, nothing takes a body, and
the listener shares no token, route, or code path with the operator
endpoint. It cannot show the payout address, fee controls, ban controls,
daemon endpoint, or any configuration; /status, /payout-address,
/fee-bps, /ban are 404 on it, and /api/* is 404 on the ops listener
(crates/dinero-sv2-pool/tests/public_stats.rs pins both). Responses carry
Access-Control-Allow-Origin: * on GET and are served from one sample
rendered every 10 s. Abuse limits: 60 requests/minute per client (429 +
Retry-After beyond that; when the peer is loopback — a local reverse
proxy — the client is X-Real-IP, else the last hop of
X-Forwarded-For (the one the proxy appended; earlier hops are
client-supplied), otherwise the peer itself, so no client can pick its own bucket;
IPv6 is bucketed by /64), at most 256 concurrent connections (extra ones
are closed on accept), and a 10 s deadline per connection. It speaks plain
HTTP; put a TLS reverse proxy in front of it for
https://pool.example.org/api/stats and make sure the proxy sets
X-Real-IP $remote_addr (nginx) or appends to X-Forwarded-For.
Found blocks are appended to found-blocks.jsonl next to the PPLNS
journal so blocks_found_total and /api/blocks survive a restart.
Each phase is one commit. git log --oneline reads like a story from
messages-on-paper to a running sovereignty loop.
| Phase | Commit | Deliverable |
|---|---|---|
| 1 | b0796aa |
Codec foundation: 128-byte header, NewTemplateDinero, SubmitSharesDinero, HeaderAssembly, byte-for-byte parity against 9 live mainnet headers |
| 2.1 | 7277201 |
dinero-tp binds real daemon via HTTP JSON-RPC, emits real templates |
| 2.2 | 8064bc9 |
Noise NX wrapping (ChaChaPoly + BLAKE2s), static key pinning |
| 3 | 2af044a |
JD message + coinbase assembly + merkle path computation |
| 4 | b657531 |
Reference pool server: two-tier target + submitblock on block-target shares |
| A | c7220cc |
SV2-shaped framing: u24 outer length prefix, 6-byte inner header (ext_type u16 + msg_type u8 + msg_length u24) |
| B | 229278c |
Setup / OpenStandardMiningChannel / SubmitShares.Success+Error |
| C | 9e4e478 |
Explicit SetNewPrevHash before every NewMiningJob |
| 3b | 63a12b7 |
Utreexo accumulator state + delta-addition primitives |
| 3c | 07a1098 |
Pure-Rust port of Dinero's Utreexo primitives; byte-for-byte verified against the live getutreexocommitment RPC |
| 4b | c1b7e46 |
Pre-block Utreexo state on the wire (UtreexoStateAnnouncement) |
| 5 | 24cf530 |
Miner-owned coinbase end-to-end: CoinbaseContext + SubmitSharesExtended + pool-side reassembly |
99 tests across the workspace at Phase 5 head, all green. cargo clippy -- -D warnings clean.
- 128-byte header with
reserved[12] = 0. Not extranonce, not pool scratch, not miner entropy. Enforced at encode and decode indinero-sv2-common::HeaderAssembly. timestamp: u64. Dinero dodged 2038; no u32 shortcuts anywhere.utreexo_rootat offset 0x44 is a first-class header field — Dinero-specific. Miners compute it locally in JD mode.- Leaf hash =
SHA256("DINERO-UTXO-LEAF-v1" || txid || vout_LE32 || amount_LE64 || CompactSize(script_len) || script). MatchesHashUTXOindinerod/src/consensus/utreexo_accumulator.cpp:216-266. - Node hash =
SHA256("DINERO-UTREEXO-NODE-v1" || left || right). MatchesHashNodein the same file, 201-214. - Commitment =
SHA256(num_leaves_LE64 || slot[0] || … || slot[63])with 32-byte zeros for empty slots. Fixed 2056-byte preimage. MatchesUtreexoForest::getCommitmentat 1845-1874. Cross-verified byte-for-byte against the livegetutreexocommitmentRPC (cb8592403b46beee…atnum_leaves=15939).
After Noise NX:
| u24 LE ciphertext_len | ChaChaPoly ciphertext |
plaintext inside the cipher:
| u16 LE ext_type | u8 msg_type | u24 LE msg_length | payload |
Pool → miner message order per tip:
SetNewPrevHash 0x20
→ UtreexoStateAnnouncement 0x21 (Dinero ext; JD-capable pools)
→ CoinbaseContext 0x17 (Dinero ext; JD-capable pools)
→ NewMiningJob 0x15
Miner → pool shares:
Standard: SubmitSharesStandard 0x1A → SubmitShares.Success 0x1C / .Error 0x1D
JD: SubmitSharesExtended 0x1B → same ack messages
docs/DINERO-UTREEXO-SPEC.md §3.5 in the Dinero repo publishes three
golden leaf-hash vectors computed before the C++ added a CompactSize
varint before the script. Current consensus code (and this Rust port)
include the varint, so the spec's goldens are stale. See the Phase 3c
commit for details.
MIT.