c2pool: a Monero pool with no operator and no daemon (mainnet milestone, 25 XMR)
What this is
I've been building a mining pool for Monero that has no operator and needs no daemon. Several nodes run it together, and anyone can run another. Each node is its own Monero node, keeps the same ledger of who is owed what, and pays miners straight from the coinbase of the blocks the pool finds. There is no wallet held by anyone, so there is nothing to seize and nothing to run off with.
I'm asking for one milestone: run it on mainnet with several nodes, find a block, and pay the miners it owes in that block. 25 XMR, about eight weeks from funding. The rest of this post explains what is different about it, what works today, and what does not work yet.
I don't run a public persona. The code and the chain are the record. Judge the work, not the name.
For now every node is mine: the three in the stagenet runs below and the two the program dials first on mainnet. Anyone can run another and join without asking me. docs/xmr-lane/RUN-A-NODE.md in the repository says how. The milestone counts my three nodes. If someone else runs one too, the report will say who ran what.
Why another pool
p2pool removed the operator, and that was the right idea. But it has costs. Each p2pool node needs a synced monerod, usually beside it, and the usual setup adds a third program, xmrig, to mine. A share below the sidechain difficulty earns nothing, so small miners were pushed onto mini and nano chains. A share that arrives late becomes an uncle and is paid less. And a sidechain can only grow so far before it has to split again, and each split pays only its own miners.
I wanted to see how many of those costs could be removed without bringing the operator back.
What is different
No daemon. The pool node is a Monero node. It follows the chain over Monero's own peer-to-peer network, checks proof of work, and keeps its own index. It rebuilds the transaction pool from the network's relay, so its blocks carry fee-paying transactions, and it sends a found block out over the same peer-to-peer links. Templates, the tip, the RandomX seed and finality all come from its own index.
It doesn't check everything monerod checks. For each block from the network it checks the proof of work, the difficulty, the weight and the coinbase amount, and that each transaction is the one the block commits to. It doesn't check the signatures inside other miners' blocks; for those it relies on the network's proof of work. Every transaction it puts in its own blocks it checks in full, against an output set built from a snapshot of the chain at a fixed height: ring signatures, key images against the spent set, range proofs and amounts. It also only dials out. It takes blocks and transactions from Monero peers and sends its own blocks, but it serves the chain to no one, so on Monero's network it is a client, not a full peer.
It is not an archival node. It starts from the snapshot, not from genesis, and before it serves, it checks that snapshot against several live peers. The code now pins the snapshot by sha256, for stagenet at height 2213803 and for mainnet at height 3765865, and a node that boots from the pin refuses a file that doesn't match. Anyone with their own monerod can make the snapshot again and compare. Publishing the files with a tagged release is part of this milestone. On mainnet the snapshot's output set is 18 GB, less than even a pruned monerod keeps, because pruning keeps every output and key image. On stagenet a node ran in about 1.5 GB of memory, and a node following mainnet in about 1.8 GB plus the mapped output set.
It runs on Linux on x86-64 today. A node wants 4 threads and 4 GB of memory, more on mainnet, where the output set also needs about 20 GB of disk. Until a release carries that file, you make it with your own monerod, which took about four hours for mainnet, or copy it from someone who has it. The node checks its hash either way.
A built-in miner. The same program can mine on the machine's processor, so
a small miner runs one program: Monero node, pool node and miner. It is started
with --mine and a payout address. It uses the same RandomX library the node
verifies with, mines the node's own template, and its shares enter the pool
exactly like shares from xmrig, credited to that address. Light mode needs a
256 MB cache. Fast mode builds the 2 GB dataset and does about nine times the
hashes per thread: about 730 a second against 77 on an Intel i7-14700T, with
plain 4 KB pages. It uses huge pages and pins threads when it can, and MSR
tuning is there as an option, though on the one machine I measured it gave
nothing above noise. On 20 September one process, with no monerod and no
xmrig, followed live stagenet and its own miner found stagenet block 2211627,
which two public nodes show in the main chain. That block carried no
transactions, because the run booted without the output set and could not check
rings; with the output set the node includes them. The miner is off by
default, and anyone with real hardware points xmrig at the stratum port as
usual.
No uncles. A share is a work receipt: evidence of work that stands on its own, not a position in a chain. Each share carries the hashing blob, enough of its coinbase for any node to rebuild and check it, and the branch that ties the two together, a few hundred bytes in all. The payout address is bound into what the miner hashed, so nobody can move a share to another address, and a share sent twice counts once. Late, concurrent or reordered shares count in full. Work is credited to the Monero height it was mined on, in bins with a grace window, not to when it arrived, so a well-connected miner gets nothing extra over a slow one. I measured the grace on stagenet with one node in Singapore. On a healthy link the median was under half a second and 0 to 4% of receipts took longer than 4 s, so the grace stays at 4 s. While a link was down some came tens of seconds late. A receipt that misses the grace is still counted, at the end of the order; only one more than about an hour late is dropped.
No sidechain. There is no second chain to sync or to fork. Pool nodes pass shares and raindrops to each other over their own relay, and each node puts the shares in order by a fixed rule: by the Monero height they were mined on, then by id. Each block the pool finds records in its coinbase where it cut that order, and a hash of the order up to the cut. A node whose order differs, after a restart or a network split, fetches the finder's order from its peers and checks it against that hash before it books the block. If more than half the work in the pool's recent blocks went into blocks built on a ledger other than its own, a node rebuilds its ledger to match them. The cost: most of the failed stagenet runs below broke in this repair, and a node that can't repair stops mining until it can. Nothing is booked twice, but the pool loses that node's hashrate. A finder also decides which shares its own cut counts. The other nodes check the proof of work of every share in it, but they can't tell if the finder left some out. I don't have a full answer yet for how much a finder can gain that way.
One ledger, checked by every node. Every node keeps the same record of who is owed what, and every block the pool finds commits a digest of it. A node that disagrees shows up at once. Every node also rebuilds the coinbase of every pool block from that record and compares it byte for byte (#1884). A block that doesn't match earns nothing: its payouts are debited from whoever they paid, and its credit is dropped. A share has to carry the same coinbase, so a miner can't mine on a template that pays itself and still keep the credit for its shares. I call it Work Receipt Settlement: you are paid for the receipts of the work you did.
A block pays the miners of the window in its own coinbase, small amounts included, while there is room. The window is read at the cut of the last pool block that is already final, at least 60 Monero blocks (about two hours) back, so every node pays from the same data. That means a new miner's first payout comes about two hours after it starts, and a miner who stops keeps being paid while its work is in the window. The window is the last 8640 shares, and older shares weigh less, halving every 2160. A miner who mines in bursts, or hops in and out, gets the same per hash on average as a steady one; only the spread changes.
Monero sets no count limit on coinbase outputs today. The pool caps them at 2700, about 113 kB. With a coinbase that large next to today's transactions, about one block in six would leave its lowest-fee transactions for a later block, because the node keeps its blocks inside Monero's penalty-free size. But no p2pool block I have counted had more than 875 outputs. When a block has more payees than slots, payouts worth spending go first, oldest first. A payee left without a slot loses its share of that block to the payees paid in it, as p2pool does. Its work stays in the window and earns in the next blocks.
Small amounts are paid as they are, even below what they cost to spend. At today's fees one input costs up to about 0.000013 XMR to spend, and p2pool's smallest payout is about 0.00027. A miner who mines a little will collect many tiny outputs. Merging them for free is later work. A balance too small to spend, owed to a miner who has stopped and found no room in a block, waits 12 days and then halves every 3 days until it is gone. That only lowers what the pool owes. It is never paid to anyone else.
Balances can still exist, for example after a migration or a seeded start. Until this week a block paid those first, oldest first, and its own miners carried the difference as new balances, so the total owed moved from miner to miner and never shrank. Now each block pays its own miners first and pays old balances only from a capped slice of the block (#1895), so no new balance is created and the old ones drain. It is on for the test networks; mainnet switches over separately.
The digest is built from a Merkle tree over the balances, so a miner that holds only Monero's block headers can check its own balance from a short proof built on the block's coinbase, with a path of 10 hashes for 1000 miners. It needs no sidechain and no trust in whoever hands it the proof. The code that builds and checks that proof is in the tree with its tests, but the node doesn't hand proofs out yet. A log of owed events in a Merkle Mountain Range, so any entry can be proved, is built and switched off.
Raindrops into a bucket. A share below the pool target is not thrown away. The near-misses a miner submits are raindrops: hashes that meet 1/64 of the share difficulty. Each one counts as 1/64 of a share of work. It is a plain count, with no estimate from the deepest hashes, so it is unbiased for every size of miner, and splitting yourself into many names changes neither what you get nor how steadily you get it. Who is counted is fixed before the work is done: a miner counts from the interval after its first share or raindrop, and that record is in the ledger, so every node has the same list. The bucket is the window. Raindrop work sits in it next to the shares and is paid in every block found while it is there, small amounts included when the block has room. Per hash, raindrop work and share work earn the same to within 0.02% in the test that compares them. That lets one pool serve a phone and a farm with no mini or nano chain.
Where this stands: it is on by default in the Monero build. My first version estimated the work from the deepest near-misses. It was biased for small miners and paid for splitting, so I replaced it with the count. In one interval the count can still come out above or below the shares alone; over many it matches the work. The three problems I listed in the first version of this post are fixed in the code. The credit rule is the count. The list of miners who take part is in the ledger. After a network split a node fetches the raindrops it missed, and the block's finder names the exact set it used, so every node counts the same ones (#1857). That set travels beside the block, not inside it yet, so the finder decides which raindrops count for its block. If two different sets show up for one block, the first one wins and the node logs the other. Putting the set in the block is later work. What is missing is a long live run with all of it on; one started on 2 October (see below).
Raindrops have a cost. The pool has one share difficulty, the same on every node. Every node checks every raindrop with RandomX, and a raindrop is 1/64 of a share, so a node checks 64 hashes for each share's worth of work. One verify thread does about 45 checks a second. At a share difficulty of 10 million, a 1 kH/s laptop finds a raindrop every 2.6 minutes, and one thread keeps up with about 7 MH/s of pool hashrate. In the first stagenet run with raindrops one thread fell behind, and I had to raise the share difficulty. Nodes now check on several threads. Eight do about 285 checks a second, enough for about 45 MH/s at that share difficulty, and the smallest node in the group sets the limit. Past that, the share difficulty has to rise or the pool has to split into roundabouts, which are not wired in yet.
Roundabouts. When the pool grows past what one group of nodes should check, it splits into roundabouts, groups that pass shares among themselves. A roundabout splits in two above twice a reference hashrate, and two join again below it, with a margin so they don't flap. Each stays between about 0.7 and 2.5 times the reference. The map is computed from the finalized chain, so there is no vote, and miners are placed by a hash of their identity, so nobody picks their roundabout. The important part is that balances don't live in a roundabout. They live in the one ledger every node keeps. A block found by any roundabout pays whoever is owed longest across the whole pool. When roundabouts split or join, no balance moves, gets divided or gets lost, and a miner keeps its place in the queue. So a small or unlucky roundabout can't starve its miners, and the work a small miner needs to be seen would stay the same as the pool grows. p2pool split by miner size and each piece paid only its own. This splits by load and stays one pool with one queue. It is designed and built as separate modules with tests, switched off. Wiring it into the node comes later.
The payout rule is checked by machine. It is written as a formal specification and checked mechanically, so the accounting can be audited without trusting me. TLC checks a model of the rule in small configurations, not the C++ itself, so it catches mistakes in the design, not in the code. The model of the rule after every node started recomputing the coinbase, and of the drain rule, is in the repository (#1892).
Put together: a pool like this at about a tenth of Monero's hashrate finds around 72 blocks a day and pays about as regularly as a centralized pool of the same size. With raindrops on, every miner down to one small device is credited for its work and paid in the blocks found while that work is in the window. A tenth is more than all of p2pool today, which is a little under 8% of the network across its three chains. At the size of p2pool mini, about half a percent, it would find about three blocks a day, and each would pay every miner in its window. At launch it will be far smaller than either.
What it does not do
It does not remove the pool's own luck. A lucky day pays more, an unlucky day less. Only someone holding a float can promise a fixed payment per share, and this pool holds nothing for anyone.
It does not stop block withholding. A miner can send its shares and keep any block it finds. The pool can't see that, and neither can any other pool, and the withholder is paid for its shares like everyone else while the pool loses the blocks. A finder can also mine a block that pays only itself. Every node then books that block as paying nothing: its credit is dropped, what it paid is charged to whoever it paid, and the shares mined on that template earn nothing. But the finder keeps that block's reward on chain. That is the same power a finder has in p2pool.
It is not private for the miner, the same as p2pool. Every node rebuilds every coinbase, so every node can work out which address each output pays. The pool's blocks also carry the pool's records in the coinbase extra field, so anyone can pick them out on chain. Mine to a wallet you use for nothing else. Payouts go to a main address, the one that starts with 4. Subaddresses and integrated addresses are not supported, and the node doesn't refuse them at login yet, so check the address you give it. p2pool can pay a subaddress, and this pool can't.
It doesn't merge-mine Tari. With p2pool a miner can mine Tari blocks alongside, solo. This pool uses Monero's merge-mining field for its own ledger commitment, and the code has no merged mining yet, so a miner who moves from p2pool gives that up for now.
Today the pool is one group, and like p2pool's sidechain it is only as safe as the majority of its hashrate. A single roundabout is cheaper to attack than the whole pool, because it holds only part of the hashrate. How far a majority inside one roundabout can go is still open, and I don't have a full answer yet.
Each node keeps a row per miner. I've measured the ledger to 100,000 miners, where its part of each block's work stays well under a second. I haven't measured past that.
Fees
There is no pool fee. As in the original p2pool and in my earlier engine, each block carries one output to the project's donation address, which every node checks. Its amount is the donation's share of the work: each node credits a small part of every share mined on its jobs to the donation, 0.1% by default, and 0 turns it off. A node runner can take a fee only by sending some of its miners' jobs to its own address, chosen at random per job and bound into that job's proof of work. It is never a fixed output. Every node puts both numbers in its reply to a miner's login, where a proxy or a script can read them, but xmrig doesn't print them. There is no cap on the runner's fee in the protocol: no other node can tell those jobs from the runner's own mining, so a cap would be a rule nobody could check. A miner can't yet check each job's payee for itself either, so it takes the stated fee on trust; showing the payee with each job is planned. The miner's protection is that the fee is stated, and the miner picks the node or runs its own. There is no operator key to steal. The donation address is mine. Monero's P2Pool has no donation output at all.
One thing I found in review is fixed. In the first version, while no miner was owed anything yet (a new pool's first block, and any block found before that one was confirmed), the whole block reward went to the donation address, and the miners were paid from later blocks. That left the pool one block behind for good, with that amount sitting in the donation wallet. Now a block pays the miners of its window in the same block, and in practice the donation output keeps only its zero-amount marker plus the donation's own share. Until a new pool's first block is final there is no window to read, so each block pays its finder.
Where it stands
The code is public at https://github.com/frstrtr/c2pool under the AGPL-3.0. The design paper is at https://frstrtr.github.io/c2pool/.
Working now, on the main branch:
- The node follows stagenet over Monero's peer-to-peer network with no daemon call on the chain view.
- It refuses to mine transactions with forged ring signatures or double spends. Its verdicts match monerod's on the same bytes, tested on regtest.
- Three nodes ran together on stagenet for three hours. They found nine blocks. Eight are in the main chain, and one lost a race and was never counted. All three finalized the same blocks and agreed on the ledger at every one. Every block carried the transactions waiting in the pool, and every coinbase paid exactly the reward plus fees.
- The stagenet and mainnet snapshots are pinned by sha256. The mainnet one was made from my own synced monerod.
- In a read-only test on mainnet a node started from that snapshot, caught up 4,480 blocks over the peer-to-peer network in about five minutes, then matched a synced monerod's tip in 48 of 48 samples, made 0 RPC calls, and resumed from its saved state after a restart. It has not mined on mainnet.
- A node restarts from its saved state instead of re-reading from the snapshot. On stagenet it re-read 22 blocks instead of 1300, and in the longer runs below nodes were back up in 35 to 120 seconds.
- One program mines too: the built-in CPU miner found a real stagenet block with no monerod and no xmrig beside it.
- Every node recomputes the coinbase of every pool block and every share, and books a block that doesn't match as debit-only (#1884).
- Old balances drain from a capped slice of each block, and no block creates a new one (#1895, on the test networks).
- Every rule that changes what a node books is part of the pool's identity (#1894).
- Raindrops are on by default and credited by count.
- Each node states its fees in its reply to every miner's login.
- At a fork it doesn't understand, the node stops building templates without banning the peers that moved on.
Since the first version of this post I ran the longer stagenet test four times: three nodes on three machines, one of them a server in Singapore with no monerod at all (the other two used a local monerod only as a peer-to-peer link), with restarts, a relay stall and a network split on a schedule, and a small generator sending stagenet transactions. None has passed yet. In every run no node made a monerod RPC call, and every coinbase the pool put on chain summed exactly to the reward plus fees.
- The 24-hour run found 57 blocks. 52 are in the main chain, and the other 5 lost same-height races and were never credited. It paid 31.2 stagenet XMR, and the three ledgers never disagreed. But nodes got stuck rebuilding blocks the others had found, and at the end two of the three had stopped mining for hours. That is fixed.
- The next run had raindrops on. It came through a relay stall, three restarts and a partition with no node stuck, and 28 of its 29 blocks are in the main chain, all exact. But from one block on the ledgers split, because the nodes did not count the same raindrops. That is fixed in the code and in local three-node runs; the run on three machines started on 2 October.
- The run after that was stopped soon after the start on a relay deadlock, fixed in the same change and tested the same way.
The pool's blocks from these runs are on the stagenet chain for anyone to check, each with the pool's records in its coinbase: between heights 2215796 and 2215986 in the first of the four, an eight-hour run, and between 2216101 and 2216913 in the 24-hour run. Not all of them carry transactions: 6 of the 14 in the eight-hour run have none, 12 of the 52 in the 24-hour run, and 6 of the 28 in the run with raindrops. In the eight-hour run each empty block came seconds after a block that had just taken the waiting transactions, or the pool was empty, and stagenet traffic is thin. I haven't checked the others yet.
A day-long run on the current code, with raindrops and the drain rule on, started on 2 October on the same three machines. I'll post it in this thread.
SChernykh, who wrote P2Pool, published a review of the payout ledger on 19 September: https://gist.github.com/SChernykh/636eca0b73181277ea85401b6b1e24ba. It listed nine findings. On the Monero lane the fixes are merged, and the paper was corrected where it was wrong. My answer to each finding is a comment under that review, and docs/xmr-lane/review-criticals.md in the repository goes through them too.
To decide how old balances should be paid, I replayed real data through a simulator that books every block with the node's own ledger and coinbase code: 1.35 million Monero block headers (heights 2,420,000 to 3,774,700), a census of the coinbases in that range with 85,216 p2pool blocks, and p2pool's real block times. Under today's rule an old backlog is never paid down, only moved: in a four-year replay a 1.2 XMR backlog was still 1.2 XMR at the end. The new rule pays old balances from 1/256 of the block for each Monero block since the pool's previous one, at most a quarter of a block, never more than is owed. At p2pool main's block times it cleared 1.2 XMR in about 0.7 days and 6 XMR in about 3.6, taking about 6% of each block while anything was owed, and no block created a new balance.
Every rule and setting that changes what a node books, 29 fields in all, is now part of the pool's identity (#1894). Two nodes that differ refuse each other at the handshake and name the field, and each treats the other's blocks as ordinary Monero blocks, instead of quietly splitting the ledger. The other side of this is that a change to the payout rules is a flag day: every node has to move to it together, or the pool becomes two pools, each paying only its own. p2pool's sidechain forks the same way. A TLA+ model of the ledger under the recompute and the drain rule is in the repository too (#1892). TLC checks about 3 million states in its main configuration, and every mistake planted in the model is caught.
Not done yet: the mainnet run, which is what I'm asking to fund; a day-long stagenet run on the current code that passes; a long parity soak next to monerod; and FCMP++.
The milestone
At least three nodes on separate machines run against Monero mainnet, on the current consensus rules (before the FCMP++ upgrade), with no daemon. They follow the tip, verify blocks as above and every transaction they mine, build templates that carry fee-paying transactions, find a block, send it to the network, and pay the miners it owes in that block's coinbase. The nodes agree on the ledger.
The mainnet run will use the same rules as stagenet, the drain rule included. A new pool starts with nothing owed, and with nothing owed the drain rule builds the same coinbase as the old rule, so it only matters once a balance appears. Turning it on later would mean a new pool identity on a live pool, which is why it starts on.
Finding a block takes hashrate. At today's difficulty, about 6.4 GH/s across the whole network, a pool with 1 MH/s finds a block every nine days on average, and one with 100 kH/s every three months. For this run I'll ask a few miners who know it is new code to point some hashrate at it. If no block is found within the eight weeks, I'll say so here and keep the nodes running. Nothing is paid until a block is found and its miners are paid in it. I won't ask the public to move hashrate to it before the soak and the review listed below.
What you get: a running pool any miner can connect to, a release that pins the snapshot by hash so anyone with a monerod can reproduce it, and a short report with the heights found, the coinbase payouts, and each node's ledger digest at those heights. Target is eight weeks from funding. The funds are earned on completion, and the report goes in this thread.
What comes after
Each of these would be its own proposal, judged on what gets delivered.
- A long parity soak next to monerod on mainnet, and another outside review of the payout accounting, before anyone is asked to point real hashrate at it.
- Roundabouts wired into the node.
- Testnet and regtest. Stagenet is where it runs today, and mainnet is this milestone. Regtest works by hand with a local monerod as the hub, and should run three nodes in CI. Testnet has no pinned snapshot yet and has never been run.
- FCMP++ and Carrot. The planned upgrade changes more than transaction and address formats: coinbase outputs, two new block header fields including a curve-tree root the node must compute itself, RandomX v2, and the weight rules. As of 2 October every Monero network is still at version 16, and upstream sets no version 17 height for any of them, so this milestone runs on today's rules. The node already stops cleanly at a fork it doesn't understand. It doesn't ban peers for sending newer blocks, and once two or more peers send them and the old chain has stopped for 30 minutes, it stops building templates and taking transactions until the old chain grows again. If the fork comes before that work is done, the pool stops at the fork and miners have to point their hashrate somewhere else until it runs again. What the pool owes at that moment stays in its ledger, and nothing pays it until the pool runs again.
Why fund it
It lets a small machine take part in a pool without a daemon. It keeps the pool without an operator across several nodes. It pays late work in full. And it is built to pay small work and to grow without splitting the pool's income. It wants what p2pool wants; it is not trying to replace it.
Track record
Verify by the work. c2pool is a C++ reimplementation of the original p2pool, the one written for Bitcoin in 2011.
- My earlier engine and my maintained p2pool forks run real pools. The public Dash pool at dash.voidbind.com has mined mainnet blocks you can check on any Dash explorer, for example 2520018 and 2519922. The Litecoin pool with Dogecoin merged mining runs on the public p2pool network (https://github.com/frstrtr/p2pool-merged-v36), where the v36 share format is now active.
- The new engine this proposal builds on ran two nodes on the public Dash testnet. They followed the chain together, and the testnet chain-locked three blocks they found. That run had the payout ledger switched off and both nodes on one machine, and the engine has not paid anyone on Dash yet.
- The Monero lane is newer than all of that, and I'm treating it that way. The engine is built for several coins, and Monero is the first on which it has paid miners, so far on stagenet.
Tooling
I use Anthropic's Claude models through their Claude for Open Source Program, which gives open-source maintainers six months of free access. That is tooling, not money, and it doesn't fund this milestone. I can share the acceptance notice with anyone who wants to check it.
Licence
AGPL-3.0. Anyone who distributes it, or runs a modified version as a network service, has to publish the source, so any miner can see what the pool they join actually runs. The block-template assembler is ported from p2pool under the GPL-3.0 and combined under the AGPL-3.0's section 13. The Monero cryptography it uses is BSD-3-Clause.
If you find a hole in any of this, tell me. I'd rather hear it now than after someone points hashrate at it.