This post is syndicated from rubin.io.
Welcome to day 15 of my Bitcoin Advent Calendar. You can see an index of all the posts here or subscribe at judica.org/join to get new posts in your inbox
Let’s talk mining pools.
First, let’s define some things. What is a pool? A pool is a way to take a strongly discontinuous income stream and turn it into a smoother income stream.
For example, suppose you are a songwriter. You’re a dime a dozen, there are 1000 songwriters. If you get song of the year, you get $1M Bonus. However, all the other songwriters are equally pretty good, it’s a crapshoot. So you and half the other songwriters agree to split the prize money whoever wins. Now, on average, every other year you get $2000, instead of once every thousand years. Since you’re only going to work about 50 years, your “expected” amount of winnings would be $50,000 if you worked alone. But expected winnings don’t buy bread. By pooling, your expected winnings are $2000 every other year for 50 years, so also $50,000. But you expect to actually have some spare cash laying around. However, if you got lucky and won the contest the year you wrote a hit, you’d end up way richer! but the odds are 1:20 of that ever happening in your life, so there aren’t that many rich songwriters (50 out of your 1000 peers…).
Mining is basically the same as our songwriter contest, just instead of silver tongued lyrics, it’s noisy whirring bitcoin mining rigs. Many machines will never mine a block. Many miners (the people operating it) won’t either! However, by pooling their efforts together, they can turn a once-in-a-million-years chance into earning temperatureless immaterial bitcoin day in and day out.
The problem with pooling is that they take an extremely decentralized process and add a centralized coordination layer on top. This layer has numerous issues including but not limited to:
People are working on a lot of these issues with upgrades like “Stratum V2” which aspire to give the pools less authority.
In theory, mining pool operators should be against things that limit their business operations. However, we’re in a bit “later stage bitcoin mining” whereas pooling is seen more as a necessary evil, and most pools are anchored by big mining operations. And getting rid of pools would be great for Bitcoin, which would increase the value of folks holdings/mining rigs. So while it might seem irrational, it’s actually perfectly incentive compatible that mining pools operators consider mining pools to be something to make less of a centralization risk. Even if pools don’t exist in their current form, mining service providers can still make really good business offerring all kinds of support. Forgive me if i’m speaking out of turn, pool ops!
To make mining pools better, we can set some ambitious goals:
But if you work with me here, you’ll see how we can nail every last one of them. And in doing so, we can clear up some major Privacy hurdles and Decentralization issues.
We’ll build this up step by step. We probably won’t look at any Sapio code today, but as a precursor I really must insist read the last couple posts first:
You read them, right?
Right?
Ok.
The idea is actually really simple, but we’ll build it up piece by piece by piece.
A window function is a little program that operates over the last “N” things and computes something.
E.g., a window function could operate over the last 5 hours and count how many carrots you ate. Or over the last 10 cars that pass you on the road.
A window function of bitcoin blocks could operate over a number of different things.
A window function could compute lot of different things too:
A last note: window functions need, for something like Bitcoin, a start height where we exclude things prior (e.g., last 100 blocks since block 500,000)
Let’s do a window function over the last 100 Blocks and collect the 1st address in the output of the coinbase transaction.
Now, in our block, instead of paying ourselves a reward, let’s divvy it up among the last 100 blocks and pay them out our entire block reward, split up.
We’re so nice!
What if instead of paying everyone, we do a window function over the last 100 blocks and filter for only blocks that followed the same rule that we are following (being nice). We take the addresses of each of them, and divvy up our award to them too like before.
We’re so nice to only our nice friends!
Now stop and think a minute. All the “nice” blocks in the last 100 didn’t get a reward directly, but they got paid by the future nice blocks handsomely. Even though we don’t get any money from the block we mined, if our nice friends keep on mining then they’ll pay us too returning the favor.
Re-read the above till it makes sense. This is the big idea. Now onto the “small” ideas.
This is all kinda nice, but now our blocks get really really big since we’re paying all our friends. Maybe we can be nice, but a little mean too and tell them to use their own block space to get their gift.
So instead of paying them out directly, we round up all the nice block addresses like before and we toss it in a Congestion Control Tree.
Now our friends do likewise too. Since the Congestion Control Module is deterministic, everyone can generate the same tree and both verify that our payout was received and generate the right transaction.
Now this gift doesn’t take up any of our space!
But it still takes up space for someone, and that blows.
So let’s do our pals a favor. Instead of just peeping the 1st address (which really could be anything) in the coinbase transaction, let’s use a good ole fashioned OP_RETURN (just some extra metadata) with a Taproot Public Key we want to use in it.
Now let’s collect all the blocks that again follow the rule defined here, and take all their taproot keys.
Now we gift them into a Payment Pool, instead of into just a Congestion Control tree with musig aggregated keys at every node. It’s a minor difference – a Congestion Control tree doesn’t have a taproot key path – but that difference means the world.
Now instead of having to expand to get everyone paid, they can use it like a Payment Pool! And Pools from different runs can even do a many-to-one transaction where they merge balances.
For example, imagine two pools:
UTXO A from Block N: 1BTC Alice, 1BTC Carol, 1BTC Dave
UTXO B Block N+1: 1BTC Alice, 1BTC Carol, 1BTC Bob
We can do a transaction as follows to merge the balances:
Spends:
UTXO A, B
Creates:
UTXO C: 2BTC Alice, 2BTC Carol, 1BTC Dave, 1BTC Bob
Compared to doing the payments directly, fully expanding this creates only 4 outputs instead of 6! It gets even better the more miners are involved.
We could even merge many pools at the same time, and in the future, benefit from something like cross-input-signature aggregation to make it even cheaper and create even fewer outputs.
But wait, there’s more!
We can even make the terminal leafs of the Payment Pool be channels instead of direct UTXOs.
This has a few big benefits.
How channel balancing might look.
This should be opt-in (with a tag field to opt-in/out) since if you didn’t want a channel it could be annoying to have the extra timeout delays, especially if you wanted e.g. to deposit directly to cold storage.
What’s the best window function?
I got no freakin’ clue. We can window over time, blocks, fee amounts, participating blocks, non participating blocks, etc.
Picking a good window function is an exercise in itself, and needs to be scrutinized for game theoretic attacks.
Earlier we showed the rewards as being just evenly split among the last blocks, but we could also reward people differently. E.g., we could reward miners who divided more reward to the other miners more (incentivize collecting more fees), or really anything deterministic that we can come up with.
Again, I don’t know the answer here. It’s a big design space!
One last idea: if we had some sort of parameter space for the window functions, we could perhaps vote on-chain for tweaking it. E.g., each miner could vote to +1 or -1 from the window length.
I don’t particularly think this is a good idea, because it brings in all sorts of weird attacks and incentives, but it is a cool case of on-chain governance so worth thinking more on.
No more steps. Now we think a bit more about the implications of this.
Well the bad news about this design is that we can’t really do solo mining. Remember, most miners probably will never mine a block. So they would never be able to enter the pool.
We could mess around with including things like partial work shares (just a few!) into blocks, but I think the best bet is to instead to focus on micro-pools. Micro-pools would be small units of hashrate (say, 1%?) that are composed of lots of tiny miners.
The tiny miners can all connect to each other and gossip around their work shares, use some sort of conesnsus algorithm, or use a pool operator. The blocks that they mine should use a taproot address/key which is a multisig of some portion of the workshares, that gets included in the top-level pool as a part of Payment Pool.
So while we don’t quite make solo mining feasible, the larger the window we use the tinier the miners can be while getting better de-risking.
A little out of scope for here, but it should work conceptually!
A while back I analyzed this kind of setup, read more here. Feel free to experiment with window and payout functions and report back!
chart showing that the rewards are smoother over time
Well we are not gonna do that here, since this is kinda a mangum opus of Sapio and it would be wayyyy too long. But it should be somewhat conceptually straightforward if you paid close attention to the “precursor” posts. And you can see some seeds of progress for an implementation on github, although I’ve mostly been focused on simpler applications (e.g. the constituent components of payment pools and channels) for the time being… contributions welcome!
TL;DR: Sapio + CTV makes pooled mining more decentralized and more private.