In a typical mining pool, miners works together to create a shared block reward. The shared reward is distributed among participants based on difficulty shares, that is, partial work proofs that “prove” a miner was working on an appropriate block.
An appropriate block pays the reward to the pool operator, who then distributes (less a fee) the rewards to the other miners.
OP_CHECKTEMPLATEVERIFY, miners can instead look at a set of qualified historical blocks
deterministically (e.g., the last N, or all blocks not older than some time T, or blocks between T1
and T2) and share their block reward with the miner behind those blocks.
an enabling technology here because it is cheap to issue remittences to all of these miners. A block
would be considered qualified if it also followed the same pooling rule.
To participate in such a pool, a miner has to have enough hashpower to mine at least one block to enter the pool. Then, the system can reduce the variance that a miner is exposed to, as demonstrated in the picture. Trusted mining pools can still exist to get miners up to a reasonable level of hashrate to participate in the trustless pools effectively, but these pools would be smaller and bear less risk.
This type of mining pool has no trusted operator and requires no coordination. This eliminates various types of mining pool attacks, such as block witholding.
The below code shows a simple simulation of experienced variances among a set of miners with a fixed sharing policy versus a set of miners without any sharing.
import numpy as np from matplotlib import pyplot as plt # TODO: Handle halvings? BLOCK_REWARD = 12.5 STANDARD_SHARE = 0.75 AVG_INTERVAL = 10.0*60.0 class Block: def __init__(self, miner_id, height, shares, percent_shares, reward, lookback, time): self.miner_id = miner_id self.height = height self.shares = shares # Compute our personal take of the revenue self.revenue = (1-percent_shares)*reward if shares else reward self.percent_shares = percent_shares self.shares =  self.time = time if percent_shares != 0 and shares: total_avail = percent_shares * reward total_percent = sum(block.percent_shares for block in lookback) # If the prior blocks were in the pool if total_percent != 0: # Pay past miners something for their help # based on the percent they shared of the total percent shared to us. # TODO: Maybe EWMA is better than straight average? self.shares = [(block.miner_id, block.percent_shares/total_percent * total_avail) for block in lookback] else: # Before the pool exists... # We still give them something (to solve the bootstrap problem...) # This could be done by issuing forward-looking block height locked payments # to future miners or by one-time coordination self.shares = [(block.miner_id, 1.0/len(lookback) * total_avail) for block in lookback] def sim(n_miners = 100, p_shares = 0.5, n_blocks = 10000, n_blocks_lookback=144, share_amt = 143.0/144, power=1.5): # Make an array of miners IDs miners = np.array(range(n_miners)) # Distribute hashrate on some power law and normalize so probabilities sum to 1 miner_hashrate = np.random.power(power, n_miners) miner_hashrate /= np.sum(miner_hashrate) # Flip a biased coin to determine if a miner is in the pool or not miner_shares = np.random.random(n_miners) <= p_shares # A list of all blocks (a... block... chain) blocks =  # A queue for just the last n_blocks_lookback sharing blocks sharing_blocks =  block_time = 0 # Initialize the sharing blocks queue while len(sharing_blocks) == 0 or sharing_blocks[-1].time-sharing_blocks.time >= n_blocks_lookback*AVG_INTERVAL: miner = np.random.choice(miners, p = miner_hashrate) block_time += np.random.poisson(AVG_INTERVAL) # we share 0 percent here because we don't know how many there are... blocks.append(Block(miner, len(blocks), miner_shares[miner], 0.0, BLOCK_REWARD, sharing_blocks, block_time)) if miner_shares[miner]: sharing_blocks.append(blocks[-1]) # Now mine n_blocks for x in range(n_blocks): miner = np.random.choice(miners, p = miner_hashrate) block_time += np.random.poisson(AVG_INTERVAL) blocks.append(Block(miner, len(blocks), miner_shares[miner], share_amt, BLOCK_REWARD, sharing_blocks, block_time)) if miner_shares[miner]: sharing_blocks.append(blocks[-1]) if sharing_blocks[-1].time-sharing_blocks.time >= n_blocks_lookback*AVG_INTERVAL: sharing_blocks.pop(0) tabulate_and_show(n_miners, miner_shares, miner_hashrate, blocks) # Compute the revenues def tabulate_and_show(n_miners, miner_shares, miner_hashrate, blocks): # have a column for each miner with space for an entry per block revenues = np.zeros((n_miners, len(blocks))) for block in blocks: h = block.height # give the miner their revenue revenues[block.miner_id, h] += block.revenue # if the block shares, assign the shares to each miner for (to, amt) in block.shares: revenues[to, h] += amt # Compute the column variance on the revenues norm = np.outer(miner_hashrate, BLOCK_REWARD* np.ones((len(blocks)))).clip(1) var = np.var(revenues/norm , axis=1) # Show the averages for just the sharing miners # and then just the non sharing miners # TODO: better measure of difference than average? # Perhaps weighted average by hashrate? print("Avg var, sharing miners %f"% (np.sqrt(np.average(var*miner_shares))/70.0)) print("Avg var, solo miners %f"%(np.sqrt(np.average(var*(miner_shares ^ 1)))/70.0)) # cumsum the instantaneous revenues to get something nice for charting chart = np.cumsum(revenues, axis=1) fig, host = plt.subplots() host.set_title("Decentralized Pool Visualization ") host.set_xlabel("Blocks") host.set_ylabel("Adjusted Normalized Revenue:\n(R / max(E[R], 1))") host.set_ylim(0.8,1.2) for (i,col) in enumerate(chart): # Show pooled miners as dotted, not pooled as unbroken earnings = (np.array(range(len(blocks)))*miner_hashrate[i]*BLOCK_REWARD).clip(1) if miner_shares[i]: host.plot(np.array(range(10000)), ((col/earnings)- 0.1)[-10001:-1], 'k') else: host.plot(range(10000), ((col/earnings)+0.1)[-10001:-1], 'r') plt.show() plt.rcParams["font.size"] = "30" sim(100, 0.5, 100000, 144*3, 1.0, 2)