Trustless Coordination-Free Mining Pools

for more detail on decentralized mining pools in a more recent article read here

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.

Using 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. OP_CHECKTEMPLATEVERIFY is 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[0].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[0].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)
comments powered by Disqus