Documentation
PowMachina stratum
Status: the client is internal/stratum/client.go (powmachina-miner --pool), the
server internal/stratum/server.go (powmachinad -stratum). The port follows the scheme: mainnet
7699, testnet 17699, regtest 27699.
What it is for
- Several devices on your own node:
powmachinad -stratum 0.0.0.0:7699and every rig runspowmachina-miner --pool stratum+tcp://node:7699 --address powm1… --worker rig1. Solo mining without a pool;getstratuminfoshows the devices and their hashrate. - A pool: the same protocol can be implemented by a pool (the miner does not distinguish a node from a pool).
The transport
TCP (or TLS: stratum+ssl://), one JSON message per line, at most 16 KiB. A request has an id,
the response has the same id and either result or error {code, message}. The server sends
notifications without an id.
The messages
login (client → server)
{"id":1,"method":"login","params":{"login":"powm1….rig1","pass":"x","agent":"powmachina-miner/0.1","lanes":["C","G"],
"finality_key":"<32 B hex, optional>"}}
→ {"id":1,"result":{"id":"<session>","network":"mainnet"}}
login is the address for the rewards, optionally with .device_name. lanes are the lanes the
client has devices for. When the node runs with -stratum-address, every block pays there and the
login is just a device name. The jobs arrive right after logging in.
finality_key is the miner’s own finality key (Ed25519, see finality). With it,
the blocks the device finds carry the miner’s key instead of the server’s, so their seats in the
finality quorum are the miner’s, and the miner signs for them itself (attest below). Without it
the server’s key is used and the server signs. powmachina-miner sends one by default
(--finality-key).
job (server → client) — separately for each lane
{"method":"job","params":{"job_id":"C1a","lane":"C","height":1234,"epoch":0,
"header":"<80 B hex — the header without the nonce>","target":"<32 B hex, big-endian>"}}
The client looks for a 64-bit nonce for which PoW(header ‖ nonce LE ‖ lane) ≤ target (comparing
bytes from the start, as mh.MeetsTarget does). A new job of the same lane replaces the old one; the
server also accepts the last 8 jobs of a lane (late shares).
attest (server → client) — the miner’s key has a seat in the quorum of the block at the
finality depth; sign it.
{"method":"attest","params":{"height":5120,"block":"<32 B hex — the block id>"}}
attestation (client → server) — the signature.
{"id":11,"method":"attestation","params":{"height":5120,"block":"<32 B hex>","signature":"<64 B hex>"}}
→ {"id":11,"result":{"status":"OK"}}
The client never signs two different blocks at one height, nor anything below the last height it signed, whatever a server asks — the record is on disk before the signature exists. A server that asks for that is told nothing, and the miner logs it. The server takes only a signature by the key the client logged in with, of the block it has at that height, and relays it to the network.
What a miner trusts its server for: which block is at that height. A miner without a node of its own cannot check it; what it can be sure of is that the server cannot sign in its name, cannot make it sign twice, and cannot keep its seat when it moves to another server.
pause (server → client): {"lane":"G","reason":"…"} — there is no work for that lane right now.
A node that takes Sharepool shares does not pause a lane for the rotation rule (after two blocks of
one lane the other one has to come): a share does not depend on the rotation, so the lane goes on
with a job for shares only, and a hash at the block’s target counts as a share. A light node, which
takes no shares, pauses the lane until the other lane’s block. A new tip whose body the node has
not applied yet pauses nothing either: the job on the block below goes on until there is one on the
new tip.
submit (client → server)
{"id":7,"method":"submit","params":{"job_id":"C1a","nonce":"00000000deadbeef"}}
→ {"id":7,"result":{"status":"OK","block":false}}
nonce is a number as 16 hex digits. block: true = the share was also a block and the node
accepted it.
keepalived (client → server) — every minute; the server disconnects after 10 min of silence.
hashrate (client → server) — every 30 s per lane: how fast the client hashes that lane while it
has work for it, in hashes per second.
{"id":9,"method":"hashrate","params":{"lane":"G","hashrate":531010}}
→ {"id":9,"result":{"status":"OK"}}
It is the miner’s own claim, for showing the owner of a rig what the rig says. A server must not derive anything a reward depends on from it — shares are the only measure of work. Seconds in which the lane had no job (the rotation rule) are left out, which is why it can be several times the rate a pool computes from shares over the same minute. A client must carry on when the server answers that it does not know the method.
The error codes
| code | meaning |
|---|---|
| 21 | an old (stale) job — not the miner’s fault |
| 22 | a duplicate share |
| 23 | the share does not meet the target (low difficulty) |
| 24 | log in first |
| 25 | invalid parameters |
| 26 | too fast (the rate limit) — it does not count as invalid |
| 27 | the login was rejected (an address from another network) — the client does not reconnect |
attestation answers with 25 when the signature does not verify, is by another key, or is not of
the server’s block at that height.
The server in the node
- The node assembles the coinbase separately for every connection (the address from the login, a random extranonce) — every device has its own header and the whole 64-bit nonce space.
- Vardiff: the share target is per connection and lane, aiming at ~1 share every 15 s, stepping
×4/÷4 after 30 s or 20 shares. Never harder than a block (a share that meets the block target goes
straight into
submitblock), never easier than the lightest Sharepool share (weight 0, 3,000 times easier than the block) — on a node that takes no shares, than the network’s easiest target. On a chain whose difficulty sits near its floor that floor is only a few times easier than a block, and a small CPU held to it would find a share every few minutes. Computing the target is the server’s policy, not consensus. - Sharepool shares: a device’s job carries the heaviest share weight (Sharepool §2.2) whose target its stratum target still meets, so every stratum share that is not a block goes to the node’s share pool as a Sharepool share paying the device’s address. A node running light takes no shares, and its devices are paid by the blocks they find alone.
- Share verification computes the PoW with the same code as block verification (the node’s epoch cache).
- Protection: at most 1,024 connections, 64 from one IP, a line of ≤ 16 KiB, a login within 30 s, a rate of 4 shares/s (40 at once), a disconnect after 50 invalid shares (low difficulty, a duplicate, bad parameters).
- New jobs: as soon as the node applies a new tip (the node tells the server; a 500 ms check stays as a fallback), and every 30 s. A device that keeps hashing on the old tip only finds stale blocks, so the push on a new tip matters most for the fast lane G devices — a polling interval alone would cost them a noticeable share of their blocks.
- The template is one state.
Node.Templatebuilds everything — the tip, the next difficulty, the selected transactions — underapplyMu.RLock, the same lock that applying a block holds. Without it a template could catch the moment between “the block is applied” (the tip moves) and “the mempool is cleaned”, and then carry a transaction the new tip has already confirmed. The miner would mine it, and the node would reject the finished block withinvalid body: the input does not exist or is already spent— the reward lost, and the log pointing at a transaction instead of at the template.