In [1]:
%pylab inline
mpl.rcParams['figure.figsize'] = [12,8]
import graph_tool.all as gt
from joblib import Parallel, delayed, cpu_count
import scipy
import scipy.optimize
import scipy.special
import time
from braids import *        # All the good stuff is in braids.py
Populating the interactive namespace from numpy and matplotlib

Create a random braid with a 25 node network, and a probability that any given node will produce a block in a given tick of $$\frac{2^{246}}{2^{256}-1} \simeq 0.1\%.$$ With 25 nodes this means that each "tick" there is a $\sim 2.4\%$ chance that the network will generate a new bead. This is small, and the resulting graph is close to a blockchain.

The network has a random topology of nodes, distributed on the surface of a sphere. Each node is connected to 4 other nodes, with a latency given by their arc distance on the sphere. A network with a low target (high dificulty) is blockchain-like, with an occasional diamond in it, which Bitcoin would orphan. TICKSIZE=0.1 so time is incremented by a tenth of a second with each iteration, and beads are propagated to connected nodes and delivered after a time interval given by their latency.

In [2]:
try: del n                         # this is here so that if you re-execute this block, it will create a new network
except: pass
n = Network(25, target=2**246)     # A smaller network or lower target makes thinner braids
for dummy in range(500): n.tick(mine=False)
b = n.nodes[0].braids[0]
b.plot(numbervertices=True);

Keeping the same network n, let's increase the target (decrease the difficulty) to produce a thicker braid. Also, this time let's actually mine, to see that the graphs are the same. The number of iterations until this node mines a bead is given by the geometric distribution. This may give a computational speedup for graphing under the right circumstances. (It's no speedup with this example)

In [3]:
n.reset(target=2**249)
for dummy in range(200): n.tick(mine=True)
b = n.nodes[0].braids[0]
b.plot(numbervertices=True);

Let's turn to how to evaluate a rewards algorithm on a network. Let's choose one bead to focus on and examine its sibling structure. Siblings are beads in the same cohort that cannot be decided to come before or after one another, since the network is asychronous (they were mined by miners on opposite sides of the planet at nearly the same time). These might contain the same transaction.

Siblings are labeled by the number of beads in the (ancestor, descendant) direction (called "rank") one must travel from the sibling to find a common ancestor. The black-circled bead is the one under consideratoin, and its siblings are labeled by their rank (m,n).

This quantity, or something similar, might be used in evaluating rewards for miners. It gives a measure based on graph structure alone of which beads might have been withheld.

In [4]:
b.plot(focusbead=b.vertex(b.num_vertices()/2+2));

Now let's play with the reward model. (Modify Braid.rewards(...) to try a different model)

If we assume a fixed reward per cohort, and that within the cohort we distribute rewards proportionally, we end up with a graph like the following. (Note that this is not a very good reward model) The area of each cohort is equal, if you sum the areas of the constituent beads. Area proportionality is done purely for an intuitive visual representation.

In [5]:
b.plot(rewards=True, K=1.8);

Now let us examine the behavior of cohorts as we vary the target difficulty.

This can be fairly time consuming. We're essentially going to simulate an entire network with 25 nodes for many millions of beads, in order to get enough resolution on the graph. We use joblib to parallelize creation of the following graphs using a map/reduce algorithm. Each job will create ncohorts/cpu_count cohorts.

The relative vertical error in this graph is $\frac{1}{\sqrt{N}}$ with $N$ cohorts as data points, so for a very smooth graph with error ~1% we need $N=10000$. This takes a couple days on a beefy computer. Instead let's just do $N=100$. The resulting curve will be noisy but it's enough to extract the salient parts. Determining the cohorts is roughly $\mathcal{O}(B_C^2)$ in the number of beads per cohort $B_C$, so we will stop adding data points when the computation time rises for large $B_C$.

Go get a coffee now...this takes a few minutes.

In [6]:
dctimes = [] # Initialize our results
In [7]:
def job(n, difficulty, ncohorts, njob):
    begin = time.process_time()
    np.random.seed((int(time.time())*njob)%(2**32-1))# We have to seed the random number generator with something
    n.reset(target=difficulty*(1<<255))              # different for each job or they will all generate exactly 
    times = []                                       # the same results!
    lastcohort = frozenset({n.nodes[0].braids[0].vertex(0)})
    while len(list(n.nodes[0].braids[0].cohorts())) < ncohorts:  # 1% error = sqrt(N)/N
        for dummy in range(100): n.tick()
        for c in n.nodes[0].braids[0].cohorts(lastcohort): pass
        lastcohort = c
    b = n.nodes[0].braids[0]
    times.append(b.cohort_time())
    #print(difficulty, times, b.ncohorts)
    return((difficulty,numpy.mean([m for (m,s) in times]), len(b.beads), b.ncohorts, time.process_time()-begin))

def parmap(f, *args):
    """ Map the function f with arguments <args>, adding an extra argument which is its job number. """
    return Parallel(n_jobs=cpu_count())(delayed(f)(*args, job) for job in range(cpu_count()))

def gettimes(n, difficulty, ncohorts):
    def reduce(result):    
        return (difficulty, numpy.mean([m[1] for m in result]), sum([m[2] for m in result]), 
                sum([m[3] for m in result]), sum([m[4] for m in result]))
    return reduce(parmap(job, n, difficulty, ncohorts/cpu_count()))

print("(target difficulty,      mean cohort time, nbeads, ncohorts, CPU time)")
for difficulty in exp(arange(-8.5, -3.375, 0.0625)):
    if(any([x[0]==difficulty for x in dctimes])): continue
    dctimes.append(gettimes(n, difficulty, 100))
    print(dctimes[-1], ",")
    if(dctimes[-1][-1] > 5*60): break # If it takes longer than 5 CPU-minutes stop
                                      # re-run this block to add another row to dctimes.
(target difficulty,      mean cohort time, nbeads, ncohorts, CPU time)
(0.00020346836901064417, 40.953571428572019, 113, 108, 13.711949014999998) ,
(0.00021659095137688503, 40.035714285714903, 112, 110, 12.546000011000002) ,
(0.00023055986759244163, 36.341666666666512, 112, 110, 12.245561545999999) ,
(0.00024542970150098948, 31.770833333333247, 110, 110, 10.293698898999999) ,
(0.00026125855730166754, 37.711458333333042, 113, 110, 12.090987142999998) ,
(0.00027810828659249914, 32.927827380952131, 114, 112, 11.426153682000001) ,
(0.00029604473005685538, 26.604613095237337, 114, 112, 8.6993844300000003) ,
(0.00031513797473735599, 25.547916666666158, 118, 109, 8.4456352809999995) ,
(0.00033546262790251185, 24.365740740740048, 114, 110, 8.356936978000002) ,
(0.00035709810857624758, 25.705357142856425, 112, 109, 8.9886564840000016) ,
(0.00038012895786946368, 18.975148809523329, 113, 110, 6.8836070649999996) ,
(0.00040464516932626452, 19.130803571428071, 120, 112, 6.8962560840000009) ,
(0.00043074254057568753, 17.001636904761529, 116, 111, 6.3394208490000015) ,
(0.00045852304766302063, 17.044940476190089, 118, 113, 6.4229536080000003) ,
(0.000488095243523415, 16.511458333333003, 117, 109, 5.805155611) ,
(0.00051957468215483844, 15.05272817460291, 115, 111, 5.2129735660000014) ,
(0.00055308437014783363, 15.460416666666378, 120, 111, 5.6311192490000002) ,
(0.00058875524733644315, 13.56770833333313, 121, 109, 5.2218813630000005) ,
(0.00062672669844845757, 12.367261904761783, 122, 110, 4.8432527299999997) ,
(0.00066714709775426734, 11.949702380952276, 128, 113, 4.8265476600000001) ,
(0.00071017438884254903, 12.763624338624183, 124, 115, 4.7221101210000009) ,
(0.00075597670178827073, 10.283283730158679, 122, 115, 4.0087447020000004) ,
(0.00080473301012461325, 10.47499999999998, 113, 108, 3.6811790599999998) ,
(0.00085663383018594048, 10.023363095238084, 116, 111, 3.7040830800000002) ,
(0.00091188196555451624, 8.7635416666666739, 118, 111, 3.5079543880000008) ,
(0.00097069329951990889, 9.9859623015872412, 122, 113, 3.9722080750000002) ,
(0.001033297638647637, 8.5245535714285765, 122, 111, 3.4049499160000001) ,
(0.0010999396107533182, 7.0005291005291364, 132, 116, 3.093990324) ,
(0.0011708796207911744, 6.6443452380952861, 117, 109, 2.7248150150000003) ,
(0.0012463948683920495, 6.2375000000000442, 121, 112, 2.6476649120000002) ,
(0.0013267804310269915, 6.2913161375661915, 124, 115, 2.743952975) ,
(0.0014123504170288816, 5.988376322751372, 125, 115, 2.6655478330000002) ,
(0.0015034391929775724, 6.6085648148148719, 133, 114, 2.7983427070000002) ,
(0.0016004026902445643, 5.1056746031746405, 150, 119, 2.7219056260000003) ,
(0.001703619795802574, 4.5077579365079652, 141, 116, 2.1541199790000003) ,
(0.0018134938327346152, 4.5193452380952657, 137, 113, 2.2046879989999999) ,
(0.0019304541362277093, 4.3915773809524064, 139, 113, 2.227940985) ,
(0.0020549577312094589, 4.9924437830688175, 145, 114, 2.3515297030000002) ,
(0.0021874911181828851, 5.0740906084656441, 144, 114, 2.3839007520000002) ,
(0.0023285721742377138, 4.3619576719576996, 151, 123, 2.1088908170000003) ,
(0.0024787521766663585, 4.4788558201058501, 149, 122, 2.2952507050000004) ,
(0.0026386179570919216, 4.3890839947090221, 141, 116, 2.2133819350000001) ,
(0.0028087941945255128, 4.0462632275132497, 140, 114, 1.9292303510000002) ,
(0.0029899458563130603, 3.8155919312169497, 146, 116, 2.0325619110000002) ,
(0.0031827807965096669, 3.0606355218855317, 164, 124, 1.9920090250000002) ,
(0.0033880525218347116, 2.7642962361712411, 164, 120, 1.9466812570000005) ,
(0.0036065631360157305, 3.2351190476190586, 172, 118, 1.9618991850000003) ,
(0.0038391664740261636, 3.0018187830687921, 176, 121, 1.929512753) ,
(0.0040867714384640666, 2.5729894179894215, 170, 123, 1.6697422200000001) ,
(0.0043503455511087691, 2.5633862433862462, 181, 122, 1.7428650479999999) ,
(0.0046309187335332458, 2.5975264550264594, 198, 125, 1.9756078410000004) ,
(0.0049295873315450519, 2.2076623376623377, 202, 122, 1.863291748) ,
(0.0052475183991813846, 2.2793109668109683, 222, 128, 1.982576039) ,
(0.0055859542589980996, 2.6669044612794668, 206, 123, 2.0378469490000004) ,
(0.0059462173564720942, 2.5611504930254969, 213, 122, 2.0412285210000003) ,
(0.006329715427485747, 2.2414880952380969, 256, 134, 2.0470457680000003) ,
(0.006737946999085467, 2.1992334054834068, 259, 134, 2.1555312170000005) ,
(0.0071725072450086989, 2.3787698412698433, 263, 124, 2.2359277020000006) ,
(0.0076350942188599617, 2.3455056517556527, 267, 123, 2.3268665430000004) ,
(0.008127515489292211, 2.1956737012987002, 289, 121, 2.4039459980000002) ,
(0.0086516952031206341, 2.3432738095238097, 318, 120, 2.502804346) ,
(0.0092096816039681402, 2.3371872340622364, 365, 132, 2.9981506549999999) ,
(0.0098036550358218278, 2.430574494949497, 392, 127, 3.2660192060000002) ,
(0.010435936462774504, 2.0925616281866284, 371, 131, 2.8894727230000004) ,
(0.011108996538242306, 2.2318121693121697, 382, 127, 2.909438218) ,
(0.011825465259096618, 2.2447351491101508, 418, 129, 3.2633478170000001) ,
(0.012588142242433998, 2.9987065018315113, 538, 123, 4.8581686100000008) ,
(0.013400007665140828, 2.4886724386724413, 464, 127, 3.7856504129999999) ,
(0.014264233908999256, 2.594062650312658, 551, 140, 4.7453124639999995) ,
(0.015184197956837946, 3.0291810966811092, 663, 132, 6.0630030030000013) ,
(0.016163494588165874, 3.6778499278499481, 904, 129, 9.6502649260000002) ,
(0.017205950425851383, 4.1098677248677484, 948, 119, 11.762376265) ,
(0.018315638888734179, 3.2164012145262268, 836, 123, 7.7698625710000009) ,
(0.019496896108597995, 4.5723538961039258, 1305, 124, 23.426272244) ,
(0.020754337873699742, 3.9909027777777895, 1097, 120, 18.242702635000001) ,
(0.022092877665062443, 4.542857142857172, 1314, 117, 24.463348388000004) ,
(0.023517745856009107, 4.8600925925926264, 1532, 121, 33.374147303000001) ,
(0.025034510149960148, 7.2952215608465778, 2198, 113, 89.717241684000015) ,
(0.026649097336355485, 7.1255621693121869, 2330, 116, 156.204994817) ,
(0.028367816449713101, 8.5776785714285797, 2826, 112, 198.87512822599999) ,
(0.030197383422318501, 13.268154761904567, 4501, 110, 1985.5644709999997) ,

This resulting curve (see below) is extremely well approximated by $$ T(x) = \frac{1}{\lambda x} + a e^{a \lambda x} $$ where $T(x)=T_C$ is the cohort time as a function of target $x$ and is measured at constant x within a window of time known as the "retarget window". We assume that an algorithm will select a new target $x^\prime$ based on the data accumulated at $x$. We assume that $\lambda$ and $a$ are constant over the window, and we measure $T_C, T_B, N_C, N_B$ for the chosen $x$.

The parameters can be understood intuitively, $\frac{1}{\lambda x} = T_B$ is the bead time. The parameter $a$ has dimensions of time and can be thought of as the "size" of the network -- the amount of time required for a bead to traverse the network. It is directly related to the orphan or uncle rate, in the blockchain limit, as well as what has been called "miner utilization" by other authors.

The parameter $\lambda$ is the hash rate and can be obtained along with $a$: $$ \lambda = \frac{N_B}{x T_C N_C}; \qquad a = T_C W\left(\frac{T_C}{T_B} - 1\right) $$ where $N_B$ is the number of beads and $N_C$ is the number of cohorts. $W(z)$ is the Lambert W function. With these in hand we can compute the location of the minimum $$ x_0 = \frac{2 W\left(\frac12\right)}{a \lambda} = \frac{0.7035}{a\lambda} $$ This is independent of network topology. (See the topology parameter of class Network -- this graph is for miners distributed on the surface of a sphere, but setting topology to something else generates a random network)

In [8]:
x = np.array([p[0] for p in dctimes])
def f(x,lam,a): return 1/lam/x+a*np.exp(a*lam*x)
y = np.array([p[1] for p in dctimes])
((lam,a),dummy) = scipy.optimize.curve_fit(f, x, y, [100, 1])
plt.loglog(x, y, label='simulation')
plt.loglog(x, f(x, lam, a), label='fit $\lambda=%3.2f$ hashes/s; $a=%1.4f$s'%(lam,a))
plt.xlabel("target difficulty $x$")
plt.ylabel("cohort time $T(x)$")
x0 = numpy.real(2*scipy.special.lambertw(1/2)/a/lam)
plt.axvline(x0, label="Fastest cohort time $T_C^\prime=%1.3f$s at target $x_0=%f$"%(f(x0,lam,a),x0), color='red')
plt.xlim(x[0], x[-1])
plt.legend(loc='lower left', frameon=False);

In the blockchain limit, $x \to 0$, the cohort time is approximately $$ T(x) = \frac{1}{\lambda x} + a + \mathcal{O}(x) $$ which allows us to see that the quantity $a$ is the increase in effective block time due to network latency effects. This is to say that the actual cohort time is slightly longer than expected from the hashrate.

The right side of the above graph can be understood intuitively as well: for large targets (low difficulty) we are producing beads so fast that there is nearly always one in flight. The only way a cohort can form is when there is accidentally a quiescent period where no miner produces a bead. The probability of that happening is exponentially suppressed.

The use of a braid allows for a zero parameter retarget algorithm which instead of targeting a fixed block time, targets the fastest possible block time. Evaluating the above for the Bitcoin network, which has had an orphan rate of 1.16 orphans/day on average over the last 2 years with a block time of $600$s, we find that $a=4.79$s and the cohort time at the minimum is $11.64$s.

It should be noted that even if a coin desires to have a fixed-block time for other reasons, it is still desirable to allow the merging of blocks, whether they would be orphans, or are due to a network split. The above analysis still applies to such a scenario. The extra blocks are called "orphans" or "uncles" in those scenarios.

In [ ]: